From 9729c4479fe23554eae6e6dd1f30ff488f470c84 Mon Sep 17 00:00:00 2001 From: Allan Sandfeld Jensen Date: Wed, 4 May 2022 11:03:41 +0200 Subject: Add sources for push-notifications Change-Id: Ibfaa78cdc3790b5936b2ae683328274ffc89c3bc Reviewed-by: Szabolcs David Reviewed-by: Peter Varga --- chromium/chrome/browser/gcm/COMMON_METADATA | 4 + chromium/chrome/browser/gcm/DIR_METADATA | 1 + chromium/chrome/browser/gcm/OWNERS | 4 + chromium/chrome/browser/gcm/gcm_product_util.cc | 57 + chromium/chrome/browser/gcm/gcm_product_util.h | 30 + .../browser/gcm/gcm_profile_service_factory.cc | 174 ++ .../browser/gcm/gcm_profile_service_factory.h | 57 + .../browser/gcm/gcm_profile_service_unittest.cc | 269 ++ .../instance_id_profile_service_factory.cc | 60 + .../instance_id_profile_service_factory.h | 44 + .../chrome/browser/profiles/incognito_helpers.cc | 28 + .../chrome/browser/profiles/incognito_helpers.h | 29 + .../chrome/browser/push_messaging/DIR_METADATA | 1 + chromium/chrome/browser/push_messaging/OWNERS | 1 + .../chrome/browser/push_messaging/budget.proto | 30 + .../browser/push_messaging/budget_database.cc | 400 +++ .../browser/push_messaging/budget_database.h | 182 ++ .../push_messaging/budget_database_unittest.cc | 351 +++ .../push_messaging_app_identifier.cc | 322 ++ .../push_messaging/push_messaging_app_identifier.h | 152 + .../push_messaging_app_identifier_unittest.cc | 310 ++ .../push_messaging/push_messaging_browsertest.cc | 3227 ++++++++++++++++++++ .../push_messaging/push_messaging_constants.cc | 11 + .../push_messaging/push_messaging_constants.h | 25 + .../push_messaging/push_messaging_features.cc | 15 + .../push_messaging/push_messaging_features.h | 21 + .../push_messaging_notification_manager.cc | 353 +++ .../push_messaging_notification_manager.h | 122 + ...push_messaging_notification_manager_unittest.cc | 87 + .../push_messaging/push_messaging_refresher.cc | 121 + .../push_messaging/push_messaging_refresher.h | 99 + .../push_messaging_refresher_unittest.cc | 84 + .../push_messaging_service_factory.cc | 82 + .../push_messaging_service_factory.h | 40 + .../push_messaging/push_messaging_service_impl.cc | 1684 ++++++++++ .../push_messaging/push_messaging_service_impl.h | 475 +++ .../push_messaging_service_unittest.cc | 480 +++ .../browser/push_messaging/push_messaging_utils.cc | 44 + .../browser/push_messaging/push_messaging_utils.h | 34 + chromium/chrome/browser/signin/DEPS | 3 + chromium/chrome/browser/signin/DIR_METADATA | 1 + chromium/chrome/browser/signin/OWNERS | 5 + .../signin/about_signin_internals_factory.cc | 58 + .../signin/about_signin_internals_factory.h | 40 + .../signin/account_consistency_mode_manager.cc | 208 ++ .../signin/account_consistency_mode_manager.h | 97 + .../account_consistency_mode_manager_factory.cc | 53 + .../account_consistency_mode_manager_factory.h | 35 + .../account_consistency_mode_manager_unittest.cc | 259 ++ .../browser/signin/account_id_from_account_info.cc | 24 + .../browser/signin/account_id_from_account_info.h | 18 + .../account_id_from_account_info_unittest.cc | 20 + .../browser/signin/account_investigator_factory.cc | 57 + .../browser/signin/account_investigator_factory.h | 44 + .../browser/signin/account_reconcilor_factory.cc | 212 ++ .../browser/signin/account_reconcilor_factory.h | 57 + .../browser/signin/chrome_device_id_helper.cc | 87 + .../browser/signin/chrome_device_id_helper.h | 36 + .../signin/chrome_device_id_helper_unittest.cc | 40 + .../chrome/browser/signin/chrome_signin_client.cc | 369 +++ .../chrome/browser/signin/chrome_signin_client.h | 115 + .../browser/signin/chrome_signin_client_factory.cc | 34 + .../browser/signin/chrome_signin_client_factory.h | 37 + .../signin/chrome_signin_client_test_util.cc | 21 + .../signin/chrome_signin_client_test_util.h | 26 + .../signin/chrome_signin_client_unittest.cc | 438 +++ .../chrome/browser/signin/chrome_signin_helper.cc | 712 +++++ .../chrome/browser/signin/chrome_signin_helper.h | 129 + .../signin/chrome_signin_helper_unittest.cc | 182 ++ .../chrome_signin_proxying_url_loader_factory.cc | 551 ++++ .../chrome_signin_proxying_url_loader_factory.h | 100 + ..._signin_proxying_url_loader_factory_unittest.cc | 359 +++ ...rome_signin_status_metrics_provider_delegate.cc | 141 + ...hrome_signin_status_metrics_provider_delegate.h | 58 + ...in_status_metrics_provider_delegate_unittest.cc | 61 + .../signin/chrome_signin_url_loader_throttle.cc | 189 ++ .../signin/chrome_signin_url_loader_throttle.h | 72 + .../chrome_signin_url_loader_throttle_unittest.cc | 281 ++ ...omeos_mirror_account_consistency_browsertest.cc | 174 ++ .../browser/signin/cookie_reminter_factory.cc | 37 + .../browser/signin/cookie_reminter_factory.h | 30 + chromium/chrome/browser/signin/dice_browsertest.cc | 1236 ++++++++ .../dice_intercepted_session_startup_helper.cc | 179 ++ .../dice_intercepted_session_startup_helper.h | 102 + .../chrome/browser/signin/dice_response_handler.cc | 426 +++ .../chrome/browser/signin/dice_response_handler.h | 187 ++ .../signin/dice_response_handler_unittest.cc | 820 +++++ .../signin/dice_signed_in_profile_creator.cc | 211 ++ .../signin/dice_signed_in_profile_creator.h | 70 + .../dice_signed_in_profile_creator_unittest.cc | 285 ++ chromium/chrome/browser/signin/dice_tab_helper.cc | 117 + chromium/chrome/browser/signin/dice_tab_helper.h | 97 + .../browser/signin/dice_tab_helper_unittest.cc | 253 ++ .../browser/signin/dice_web_signin_interceptor.cc | 851 ++++++ .../browser/signin/dice_web_signin_interceptor.h | 411 +++ .../dice_web_signin_interceptor_browsertest.cc | 1200 ++++++++ .../signin/dice_web_signin_interceptor_factory.cc | 45 + .../signin/dice_web_signin_interceptor_factory.h | 37 + .../signin/dice_web_signin_interceptor_unittest.cc | 953 ++++++ .../browser/signin/e2e_tests/live_sign_in_test.cc | 776 +++++ .../chrome/browser/signin/e2e_tests/live_test.cc | 61 + .../chrome/browser/signin/e2e_tests/live_test.h | 33 + .../browser/signin/e2e_tests/test_accounts_util.cc | 75 + .../browser/signin/e2e_tests/test_accounts_util.h | 46 + .../e2e_tests/test_accounts_util_unittest.cc | 100 + .../chrome/browser/signin/force_signin_verifier.cc | 195 ++ .../chrome/browser/signin/force_signin_verifier.h | 95 + .../signin/force_signin_verifier_unittest.cc | 416 +++ .../browser/signin/header_modification_delegate.h | 38 + .../signin/header_modification_delegate_impl.cc | 154 + .../signin/header_modification_delegate_impl.h | 68 + .../browser/signin/identity_manager_factory.cc | 171 ++ .../browser/signin/identity_manager_factory.h | 67 + .../browser/signin/identity_manager_provider.cc | 38 + .../browser/signin/identity_manager_provider.h | 32 + .../signin/identity_services_provider_android.cc | 39 + .../identity_test_environment_profile_adaptor.cc | 103 + .../identity_test_environment_profile_adaptor.h | 101 + .../signin/investigator_dependency_provider.cc | 14 + .../signin/investigator_dependency_provider.h | 33 + .../chrome/browser/signin/logout_tab_helper.cc | 34 + chromium/chrome/browser/signin/logout_tab_helper.h | 36 + .../browser/signin/logout_tab_helper_unittest.cc | 24 + .../chrome/browser/signin/mirror_browsertest.cc | 277 ++ .../signin/process_dice_header_delegate_impl.cc | 146 + .../signin/process_dice_header_delegate_impl.h | 77 + .../process_dice_header_delegate_impl_unittest.cc | 375 +++ chromium/chrome/browser/signin/reauth_result.h | 38 + .../chrome/browser/signin/reauth_tab_helper.cc | 96 + chromium/chrome/browser/signin/reauth_tab_helper.h | 66 + .../browser/signin/reauth_tab_helper_unittest.cc | 184 ++ chromium/chrome/browser/signin/reauth_util.cc | 40 + chromium/chrome/browser/signin/reauth_util.h | 24 + .../chrome/browser/signin/reauth_util_unittest.cc | 33 + .../signin/remove_local_account_browsertest.cc | 143 + .../chrome/browser/signin/services/DIR_METADATA | 1 + chromium/chrome/browser/signin/services/OWNERS | 2 + .../signin/services/ProfileDataCacheUnitTest.java | 120 + .../signin/services/WebSigninBridgeTest.java | 89 + .../signin/signin_error_controller_factory.cc | 48 + .../signin/signin_error_controller_factory.h | 37 + chromium/chrome/browser/signin/signin_features.cc | 16 + chromium/chrome/browser/signin/signin_features.h | 15 + .../chrome/browser/signin/signin_global_error.cc | 170 ++ .../chrome/browser/signin/signin_global_error.h | 64 + .../browser/signin/signin_global_error_factory.cc | 46 + .../browser/signin/signin_global_error_factory.h | 40 + .../browser/signin/signin_global_error_unittest.cc | 166 + chromium/chrome/browser/signin/signin_manager.cc | 200 ++ chromium/chrome/browser/signin/signin_manager.h | 71 + .../signin/signin_manager_android_factory.cc | 41 + .../signin/signin_manager_android_factory.h | 32 + .../browser/signin/signin_manager_factory.cc | 48 + .../chrome/browser/signin/signin_manager_factory.h | 33 + .../browser/signin/signin_manager_unittest.cc | 421 +++ .../signin/signin_profile_attributes_updater.cc | 72 + .../signin/signin_profile_attributes_updater.h | 56 + .../signin_profile_attributes_updater_factory.cc | 53 + .../signin_profile_attributes_updater_factory.h | 42 + .../signin_profile_attributes_updater_unittest.cc | 224 ++ chromium/chrome/browser/signin/signin_promo.cc | 143 + chromium/chrome/browser/signin/signin_promo.h | 83 + .../chrome/browser/signin/signin_promo_unittest.cc | 56 + .../chrome/browser/signin/signin_promo_util.cc | 49 + chromium/chrome/browser/signin/signin_promo_util.h | 18 + chromium/chrome/browser/signin/signin_ui_util.cc | 608 ++++ chromium/chrome/browser/signin/signin_ui_util.h | 188 ++ .../browser/signin/signin_ui_util_browsertest.cc | 85 + .../browser/signin/signin_ui_util_unittest.cc | 736 +++++ chromium/chrome/browser/signin/signin_util.cc | 382 +++ chromium/chrome/browser/signin/signin_util.h | 73 + .../chrome/browser/signin/signin_util_unittest.cc | 127 + chromium/chrome/browser/signin/signin_util_win.cc | 332 ++ chromium/chrome/browser/signin/signin_util_win.h | 31 + .../browser/signin/signin_util_win_browsertest.cc | 698 +++++ .../browser/signin/test_signin_client_builder.cc | 18 + .../browser/signin/test_signin_client_builder.h | 26 + .../browser/signin/token_revoker_test_utils.cc | 36 + .../browser/signin/token_revoker_test_utils.h | 42 + chromium/components/gcm_driver/DEPS | 27 + chromium/components/gcm_driver/DIR_METADATA | 4 + chromium/components/gcm_driver/OWNERS | 2 + chromium/components/gcm_driver/account_tracker.cc | 147 + chromium/components/gcm_driver/account_tracker.h | 78 + .../gcm_driver/account_tracker_unittest.cc | 539 ++++ chromium/components/gcm_driver/android/OWNERS | 2 + .../components/gcm_driver/GCMMessageTest.java | 267 ++ .../gcm_driver/LazySubscriptionsManagerTest.java | 267 ++ .../gcm_driver/common/gcm_driver_export.h | 29 + .../components/gcm_driver/common/gcm_message.cc | 29 + .../components/gcm_driver/common/gcm_message.h | 56 + chromium/components/gcm_driver/crypto/DEPS | 7 + .../gcm_driver/crypto/encryption_header_parsers.cc | 156 + .../gcm_driver/crypto/encryption_header_parsers.h | 92 + .../crypto/encryption_header_parsers_unittest.cc | 374 +++ .../gcm_driver/crypto/gcm_crypto_test_helpers.cc | 84 + .../gcm_driver/crypto/gcm_crypto_test_helpers.h | 25 + .../gcm_driver/crypto/gcm_decryption_result.cc | 50 + .../gcm_driver/crypto/gcm_decryption_result.h | 71 + .../gcm_driver/crypto/gcm_encryption_provider.cc | 412 +++ .../gcm_driver/crypto/gcm_encryption_provider.h | 157 + .../crypto/gcm_encryption_provider_unittest.cc | 672 ++++ .../gcm_driver/crypto/gcm_encryption_result.h | 33 + .../components/gcm_driver/crypto/gcm_key_store.cc | 425 +++ .../components/gcm_driver/crypto/gcm_key_store.h | 150 + .../gcm_driver/crypto/gcm_key_store_unittest.cc | 775 +++++ .../gcm_driver/crypto/gcm_message_cryptographer.cc | 477 +++ .../gcm_driver/crypto/gcm_message_cryptographer.h | 167 + .../crypto/gcm_message_cryptographer_unittest.cc | 906 ++++++ .../gcm_driver/crypto/message_payload_parser.cc | 77 + .../gcm_driver/crypto/message_payload_parser.h | 97 + .../crypto/message_payload_parser_unittest.cc | 124 + .../components/gcm_driver/crypto/p256_key_util.cc | 95 + .../components/gcm_driver/crypto/p256_key_util.h | 43 + .../gcm_driver/crypto/p256_key_util_unittest.cc | 158 + .../crypto/proto/gcm_encryption_data.proto | 58 + .../components/gcm_driver/fake_gcm_app_handler.cc | 85 + .../components/gcm_driver/fake_gcm_app_handler.h | 75 + chromium/components/gcm_driver/fake_gcm_client.cc | 335 ++ chromium/components/gcm_driver/fake_gcm_client.h | 139 + .../gcm_driver/fake_gcm_client_factory.cc | 28 + .../gcm_driver/fake_gcm_client_factory.h | 41 + chromium/components/gcm_driver/fake_gcm_driver.cc | 110 + chromium/components/gcm_driver/fake_gcm_driver.h | 71 + .../gcm_driver/fake_gcm_profile_service.cc | 246 ++ .../gcm_driver/fake_gcm_profile_service.h | 79 + chromium/components/gcm_driver/features.cc | 41 + chromium/components/gcm_driver/features.h | 24 + .../components/gcm_driver/gcm_account_mapper.cc | 386 +++ .../components/gcm_driver/gcm_account_mapper.h | 136 + .../gcm_driver/gcm_account_mapper_unittest.cc | 955 ++++++ .../components/gcm_driver/gcm_account_tracker.cc | 343 +++ .../components/gcm_driver/gcm_account_tracker.h | 172 ++ .../gcm_driver/gcm_account_tracker_unittest.cc | 534 ++++ chromium/components/gcm_driver/gcm_activity.cc | 62 + chromium/components/gcm_driver/gcm_activity.h | 90 + chromium/components/gcm_driver/gcm_app_handler.cc | 21 + chromium/components/gcm_driver/gcm_app_handler.h | 67 + .../components/gcm_driver/gcm_backoff_policy.cc | 46 + .../components/gcm_driver/gcm_backoff_policy.h | 17 + chromium/components/gcm_driver/gcm_client.cc | 38 + chromium/components/gcm_driver/gcm_client.h | 366 +++ .../components/gcm_driver/gcm_client_factory.cc | 23 + .../components/gcm_driver/gcm_client_factory.h | 30 + chromium/components/gcm_driver/gcm_client_impl.cc | 1481 +++++++++ chromium/components/gcm_driver/gcm_client_impl.h | 430 +++ .../gcm_driver/gcm_client_impl_unittest.cc | 1966 ++++++++++++ .../gcm_driver/gcm_connection_observer.cc | 18 + .../gcm_driver/gcm_connection_observer.h | 34 + .../gcm_driver/gcm_delayed_task_controller.cc | 39 + .../gcm_driver/gcm_delayed_task_controller.h | 44 + .../gcm_delayed_task_controller_unittest.cc | 63 + .../components/gcm_driver/gcm_desktop_utils.cc | 101 + chromium/components/gcm_driver/gcm_desktop_utils.h | 49 + chromium/components/gcm_driver/gcm_driver.cc | 373 +++ chromium/components/gcm_driver/gcm_driver.h | 395 +++ .../components/gcm_driver/gcm_driver_android.cc | 275 ++ .../components/gcm_driver/gcm_driver_android.h | 115 + .../components/gcm_driver/gcm_driver_constants.cc | 15 + .../components/gcm_driver/gcm_driver_constants.h | 20 + .../components/gcm_driver/gcm_driver_desktop.cc | 1333 ++++++++ .../components/gcm_driver/gcm_driver_desktop.h | 259 ++ .../gcm_driver/gcm_driver_desktop_unittest.cc | 1139 +++++++ .../components/gcm_driver/gcm_driver_unittest.cc | 269 ++ .../gcm_driver/gcm_internals_constants.cc | 41 + .../gcm_driver/gcm_internals_constants.h | 47 + .../components/gcm_driver/gcm_internals_helper.cc | 190 ++ .../components/gcm_driver/gcm_internals_helper.h | 30 + .../components/gcm_driver/gcm_profile_service.cc | 205 ++ .../components/gcm_driver/gcm_profile_service.h | 111 + .../gcm_driver/gcm_stats_recorder_android.cc | 148 + .../gcm_driver/gcm_stats_recorder_android.h | 99 + .../gcm_stats_recorder_android_unittest.cc | 116 + .../gcm_driver/gcm_stats_recorder_impl.cc | 514 ++++ .../gcm_driver/gcm_stats_recorder_impl.h | 171 ++ .../gcm_driver/gcm_stats_recorder_impl_unittest.cc | 536 ++++ chromium/components/gcm_driver/instance_id/DEPS | 4 + .../instance_id/FakeInstanceIDWithSubtype.java | 198 ++ .../instance_id/fake_gcm_driver_for_instance_id.cc | 121 + .../instance_id/fake_gcm_driver_for_instance_id.h | 80 + .../gcm_driver/instance_id/instance_id.cc | 58 + .../gcm_driver/instance_id/instance_id.h | 182 ++ .../gcm_driver/instance_id/instance_id_android.cc | 229 ++ .../gcm_driver/instance_id/instance_id_android.h | 104 + .../gcm_driver/instance_id/instance_id_driver.cc | 40 + .../gcm_driver/instance_id/instance_id_driver.h | 58 + .../instance_id/instance_id_driver_unittest.cc | 366 +++ .../gcm_driver/instance_id/instance_id_impl.cc | 275 ++ .../gcm_driver/instance_id/instance_id_impl.h | 98 + .../instance_id/instance_id_profile_service.cc | 23 + .../instance_id/instance_id_profile_service.h | 38 + .../scoped_use_fake_instance_id_android.cc | 25 + .../scoped_use_fake_instance_id_android.h | 31 + .../components/gcm_driver/registration_info.cc | 286 ++ chromium/components/gcm_driver/registration_info.h | 137 + chromium/components/gcm_driver/resources/OWNERS | 2 + .../gcm_driver/resources/gcm_internals.css | 47 + .../gcm_driver/resources/gcm_internals.html | 210 ++ .../gcm_driver/resources/gcm_internals.js | 222 ++ chromium/components/gcm_driver/system_encryptor.cc | 23 + chromium/components/gcm_driver/system_encryptor.h | 27 + 301 files changed, 57748 insertions(+) create mode 100644 chromium/chrome/browser/gcm/COMMON_METADATA create mode 100644 chromium/chrome/browser/gcm/DIR_METADATA create mode 100644 chromium/chrome/browser/gcm/OWNERS create mode 100644 chromium/chrome/browser/gcm/gcm_product_util.cc create mode 100644 chromium/chrome/browser/gcm/gcm_product_util.h create mode 100644 chromium/chrome/browser/gcm/gcm_profile_service_factory.cc create mode 100644 chromium/chrome/browser/gcm/gcm_profile_service_factory.h create mode 100644 chromium/chrome/browser/gcm/gcm_profile_service_unittest.cc create mode 100644 chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.cc create mode 100644 chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h create mode 100644 chromium/chrome/browser/profiles/incognito_helpers.cc create mode 100644 chromium/chrome/browser/profiles/incognito_helpers.h create mode 100644 chromium/chrome/browser/push_messaging/DIR_METADATA create mode 100644 chromium/chrome/browser/push_messaging/OWNERS create mode 100644 chromium/chrome/browser/push_messaging/budget.proto create mode 100644 chromium/chrome/browser/push_messaging/budget_database.cc create mode 100644 chromium/chrome/browser/push_messaging/budget_database.h create mode 100644 chromium/chrome/browser/push_messaging/budget_database_unittest.cc create mode 100644 chromium/chrome/browser/push_messaging/push_messaging_app_identifier.cc create mode 100644 chromium/chrome/browser/push_messaging/push_messaging_app_identifier.h create mode 100644 chromium/chrome/browser/push_messaging/push_messaging_app_identifier_unittest.cc create mode 100644 chromium/chrome/browser/push_messaging/push_messaging_browsertest.cc create mode 100644 chromium/chrome/browser/push_messaging/push_messaging_constants.cc create mode 100644 chromium/chrome/browser/push_messaging/push_messaging_constants.h create mode 100644 chromium/chrome/browser/push_messaging/push_messaging_features.cc create mode 100644 chromium/chrome/browser/push_messaging/push_messaging_features.h create mode 100644 chromium/chrome/browser/push_messaging/push_messaging_notification_manager.cc create mode 100644 chromium/chrome/browser/push_messaging/push_messaging_notification_manager.h create mode 100644 chromium/chrome/browser/push_messaging/push_messaging_notification_manager_unittest.cc create mode 100644 chromium/chrome/browser/push_messaging/push_messaging_refresher.cc create mode 100644 chromium/chrome/browser/push_messaging/push_messaging_refresher.h create mode 100644 chromium/chrome/browser/push_messaging/push_messaging_refresher_unittest.cc create mode 100644 chromium/chrome/browser/push_messaging/push_messaging_service_factory.cc create mode 100644 chromium/chrome/browser/push_messaging/push_messaging_service_factory.h create mode 100644 chromium/chrome/browser/push_messaging/push_messaging_service_impl.cc create mode 100644 chromium/chrome/browser/push_messaging/push_messaging_service_impl.h create mode 100644 chromium/chrome/browser/push_messaging/push_messaging_service_unittest.cc create mode 100644 chromium/chrome/browser/push_messaging/push_messaging_utils.cc create mode 100644 chromium/chrome/browser/push_messaging/push_messaging_utils.h create mode 100644 chromium/chrome/browser/signin/DEPS create mode 100644 chromium/chrome/browser/signin/DIR_METADATA create mode 100644 chromium/chrome/browser/signin/OWNERS create mode 100644 chromium/chrome/browser/signin/about_signin_internals_factory.cc create mode 100644 chromium/chrome/browser/signin/about_signin_internals_factory.h create mode 100644 chromium/chrome/browser/signin/account_consistency_mode_manager.cc create mode 100644 chromium/chrome/browser/signin/account_consistency_mode_manager.h create mode 100644 chromium/chrome/browser/signin/account_consistency_mode_manager_factory.cc create mode 100644 chromium/chrome/browser/signin/account_consistency_mode_manager_factory.h create mode 100644 chromium/chrome/browser/signin/account_consistency_mode_manager_unittest.cc create mode 100644 chromium/chrome/browser/signin/account_id_from_account_info.cc create mode 100644 chromium/chrome/browser/signin/account_id_from_account_info.h create mode 100644 chromium/chrome/browser/signin/account_id_from_account_info_unittest.cc create mode 100644 chromium/chrome/browser/signin/account_investigator_factory.cc create mode 100644 chromium/chrome/browser/signin/account_investigator_factory.h create mode 100644 chromium/chrome/browser/signin/account_reconcilor_factory.cc create mode 100644 chromium/chrome/browser/signin/account_reconcilor_factory.h create mode 100644 chromium/chrome/browser/signin/chrome_device_id_helper.cc create mode 100644 chromium/chrome/browser/signin/chrome_device_id_helper.h create mode 100644 chromium/chrome/browser/signin/chrome_device_id_helper_unittest.cc create mode 100644 chromium/chrome/browser/signin/chrome_signin_client.cc create mode 100644 chromium/chrome/browser/signin/chrome_signin_client.h create mode 100644 chromium/chrome/browser/signin/chrome_signin_client_factory.cc create mode 100644 chromium/chrome/browser/signin/chrome_signin_client_factory.h create mode 100644 chromium/chrome/browser/signin/chrome_signin_client_test_util.cc create mode 100644 chromium/chrome/browser/signin/chrome_signin_client_test_util.h create mode 100644 chromium/chrome/browser/signin/chrome_signin_client_unittest.cc create mode 100644 chromium/chrome/browser/signin/chrome_signin_helper.cc create mode 100644 chromium/chrome/browser/signin/chrome_signin_helper.h create mode 100644 chromium/chrome/browser/signin/chrome_signin_helper_unittest.cc create mode 100644 chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.cc create mode 100644 chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h create mode 100644 chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory_unittest.cc create mode 100644 chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.cc create mode 100644 chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.h create mode 100644 chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate_unittest.cc create mode 100644 chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.cc create mode 100644 chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.h create mode 100644 chromium/chrome/browser/signin/chrome_signin_url_loader_throttle_unittest.cc create mode 100644 chromium/chrome/browser/signin/chromeos_mirror_account_consistency_browsertest.cc create mode 100644 chromium/chrome/browser/signin/cookie_reminter_factory.cc create mode 100644 chromium/chrome/browser/signin/cookie_reminter_factory.h create mode 100644 chromium/chrome/browser/signin/dice_browsertest.cc create mode 100644 chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.cc create mode 100644 chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.h create mode 100644 chromium/chrome/browser/signin/dice_response_handler.cc create mode 100644 chromium/chrome/browser/signin/dice_response_handler.h create mode 100644 chromium/chrome/browser/signin/dice_response_handler_unittest.cc create mode 100644 chromium/chrome/browser/signin/dice_signed_in_profile_creator.cc create mode 100644 chromium/chrome/browser/signin/dice_signed_in_profile_creator.h create mode 100644 chromium/chrome/browser/signin/dice_signed_in_profile_creator_unittest.cc create mode 100644 chromium/chrome/browser/signin/dice_tab_helper.cc create mode 100644 chromium/chrome/browser/signin/dice_tab_helper.h create mode 100644 chromium/chrome/browser/signin/dice_tab_helper_unittest.cc create mode 100644 chromium/chrome/browser/signin/dice_web_signin_interceptor.cc create mode 100644 chromium/chrome/browser/signin/dice_web_signin_interceptor.h create mode 100644 chromium/chrome/browser/signin/dice_web_signin_interceptor_browsertest.cc create mode 100644 chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.cc create mode 100644 chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.h create mode 100644 chromium/chrome/browser/signin/dice_web_signin_interceptor_unittest.cc create mode 100644 chromium/chrome/browser/signin/e2e_tests/live_sign_in_test.cc create mode 100644 chromium/chrome/browser/signin/e2e_tests/live_test.cc create mode 100644 chromium/chrome/browser/signin/e2e_tests/live_test.h create mode 100644 chromium/chrome/browser/signin/e2e_tests/test_accounts_util.cc create mode 100644 chromium/chrome/browser/signin/e2e_tests/test_accounts_util.h create mode 100644 chromium/chrome/browser/signin/e2e_tests/test_accounts_util_unittest.cc create mode 100644 chromium/chrome/browser/signin/force_signin_verifier.cc create mode 100644 chromium/chrome/browser/signin/force_signin_verifier.h create mode 100644 chromium/chrome/browser/signin/force_signin_verifier_unittest.cc create mode 100644 chromium/chrome/browser/signin/header_modification_delegate.h create mode 100644 chromium/chrome/browser/signin/header_modification_delegate_impl.cc create mode 100644 chromium/chrome/browser/signin/header_modification_delegate_impl.h create mode 100644 chromium/chrome/browser/signin/identity_manager_factory.cc create mode 100644 chromium/chrome/browser/signin/identity_manager_factory.h create mode 100644 chromium/chrome/browser/signin/identity_manager_provider.cc create mode 100644 chromium/chrome/browser/signin/identity_manager_provider.h create mode 100644 chromium/chrome/browser/signin/identity_services_provider_android.cc create mode 100644 chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.cc create mode 100644 chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.h create mode 100644 chromium/chrome/browser/signin/investigator_dependency_provider.cc create mode 100644 chromium/chrome/browser/signin/investigator_dependency_provider.h create mode 100644 chromium/chrome/browser/signin/logout_tab_helper.cc create mode 100644 chromium/chrome/browser/signin/logout_tab_helper.h create mode 100644 chromium/chrome/browser/signin/logout_tab_helper_unittest.cc create mode 100644 chromium/chrome/browser/signin/mirror_browsertest.cc create mode 100644 chromium/chrome/browser/signin/process_dice_header_delegate_impl.cc create mode 100644 chromium/chrome/browser/signin/process_dice_header_delegate_impl.h create mode 100644 chromium/chrome/browser/signin/process_dice_header_delegate_impl_unittest.cc create mode 100644 chromium/chrome/browser/signin/reauth_result.h create mode 100644 chromium/chrome/browser/signin/reauth_tab_helper.cc create mode 100644 chromium/chrome/browser/signin/reauth_tab_helper.h create mode 100644 chromium/chrome/browser/signin/reauth_tab_helper_unittest.cc create mode 100644 chromium/chrome/browser/signin/reauth_util.cc create mode 100644 chromium/chrome/browser/signin/reauth_util.h create mode 100644 chromium/chrome/browser/signin/reauth_util_unittest.cc create mode 100644 chromium/chrome/browser/signin/remove_local_account_browsertest.cc create mode 100644 chromium/chrome/browser/signin/services/DIR_METADATA create mode 100644 chromium/chrome/browser/signin/services/OWNERS create mode 100644 chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/ProfileDataCacheUnitTest.java create mode 100644 chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/WebSigninBridgeTest.java create mode 100644 chromium/chrome/browser/signin/signin_error_controller_factory.cc create mode 100644 chromium/chrome/browser/signin/signin_error_controller_factory.h create mode 100644 chromium/chrome/browser/signin/signin_features.cc create mode 100644 chromium/chrome/browser/signin/signin_features.h create mode 100644 chromium/chrome/browser/signin/signin_global_error.cc create mode 100644 chromium/chrome/browser/signin/signin_global_error.h create mode 100644 chromium/chrome/browser/signin/signin_global_error_factory.cc create mode 100644 chromium/chrome/browser/signin/signin_global_error_factory.h create mode 100644 chromium/chrome/browser/signin/signin_global_error_unittest.cc create mode 100644 chromium/chrome/browser/signin/signin_manager.cc create mode 100644 chromium/chrome/browser/signin/signin_manager.h create mode 100644 chromium/chrome/browser/signin/signin_manager_android_factory.cc create mode 100644 chromium/chrome/browser/signin/signin_manager_android_factory.h create mode 100644 chromium/chrome/browser/signin/signin_manager_factory.cc create mode 100644 chromium/chrome/browser/signin/signin_manager_factory.h create mode 100644 chromium/chrome/browser/signin/signin_manager_unittest.cc create mode 100644 chromium/chrome/browser/signin/signin_profile_attributes_updater.cc create mode 100644 chromium/chrome/browser/signin/signin_profile_attributes_updater.h create mode 100644 chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.cc create mode 100644 chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.h create mode 100644 chromium/chrome/browser/signin/signin_profile_attributes_updater_unittest.cc create mode 100644 chromium/chrome/browser/signin/signin_promo.cc create mode 100644 chromium/chrome/browser/signin/signin_promo.h create mode 100644 chromium/chrome/browser/signin/signin_promo_unittest.cc create mode 100644 chromium/chrome/browser/signin/signin_promo_util.cc create mode 100644 chromium/chrome/browser/signin/signin_promo_util.h create mode 100644 chromium/chrome/browser/signin/signin_ui_util.cc create mode 100644 chromium/chrome/browser/signin/signin_ui_util.h create mode 100644 chromium/chrome/browser/signin/signin_ui_util_browsertest.cc create mode 100644 chromium/chrome/browser/signin/signin_ui_util_unittest.cc create mode 100644 chromium/chrome/browser/signin/signin_util.cc create mode 100644 chromium/chrome/browser/signin/signin_util.h create mode 100644 chromium/chrome/browser/signin/signin_util_unittest.cc create mode 100644 chromium/chrome/browser/signin/signin_util_win.cc create mode 100644 chromium/chrome/browser/signin/signin_util_win.h create mode 100644 chromium/chrome/browser/signin/signin_util_win_browsertest.cc create mode 100644 chromium/chrome/browser/signin/test_signin_client_builder.cc create mode 100644 chromium/chrome/browser/signin/test_signin_client_builder.h create mode 100644 chromium/chrome/browser/signin/token_revoker_test_utils.cc create mode 100644 chromium/chrome/browser/signin/token_revoker_test_utils.h create mode 100644 chromium/components/gcm_driver/DEPS create mode 100644 chromium/components/gcm_driver/DIR_METADATA create mode 100644 chromium/components/gcm_driver/OWNERS create mode 100644 chromium/components/gcm_driver/account_tracker.cc create mode 100644 chromium/components/gcm_driver/account_tracker.h create mode 100644 chromium/components/gcm_driver/account_tracker_unittest.cc create mode 100644 chromium/components/gcm_driver/android/OWNERS create mode 100644 chromium/components/gcm_driver/android/junit/src/org/chromium/components/gcm_driver/GCMMessageTest.java create mode 100644 chromium/components/gcm_driver/android/junit/src/org/chromium/components/gcm_driver/LazySubscriptionsManagerTest.java create mode 100644 chromium/components/gcm_driver/common/gcm_driver_export.h create mode 100644 chromium/components/gcm_driver/common/gcm_message.cc create mode 100644 chromium/components/gcm_driver/common/gcm_message.h create mode 100644 chromium/components/gcm_driver/crypto/DEPS create mode 100644 chromium/components/gcm_driver/crypto/encryption_header_parsers.cc create mode 100644 chromium/components/gcm_driver/crypto/encryption_header_parsers.h create mode 100644 chromium/components/gcm_driver/crypto/encryption_header_parsers_unittest.cc create mode 100644 chromium/components/gcm_driver/crypto/gcm_crypto_test_helpers.cc create mode 100644 chromium/components/gcm_driver/crypto/gcm_crypto_test_helpers.h create mode 100644 chromium/components/gcm_driver/crypto/gcm_decryption_result.cc create mode 100644 chromium/components/gcm_driver/crypto/gcm_decryption_result.h create mode 100644 chromium/components/gcm_driver/crypto/gcm_encryption_provider.cc create mode 100644 chromium/components/gcm_driver/crypto/gcm_encryption_provider.h create mode 100644 chromium/components/gcm_driver/crypto/gcm_encryption_provider_unittest.cc create mode 100644 chromium/components/gcm_driver/crypto/gcm_encryption_result.h create mode 100644 chromium/components/gcm_driver/crypto/gcm_key_store.cc create mode 100644 chromium/components/gcm_driver/crypto/gcm_key_store.h create mode 100644 chromium/components/gcm_driver/crypto/gcm_key_store_unittest.cc create mode 100644 chromium/components/gcm_driver/crypto/gcm_message_cryptographer.cc create mode 100644 chromium/components/gcm_driver/crypto/gcm_message_cryptographer.h create mode 100644 chromium/components/gcm_driver/crypto/gcm_message_cryptographer_unittest.cc create mode 100644 chromium/components/gcm_driver/crypto/message_payload_parser.cc create mode 100644 chromium/components/gcm_driver/crypto/message_payload_parser.h create mode 100644 chromium/components/gcm_driver/crypto/message_payload_parser_unittest.cc create mode 100644 chromium/components/gcm_driver/crypto/p256_key_util.cc create mode 100644 chromium/components/gcm_driver/crypto/p256_key_util.h create mode 100644 chromium/components/gcm_driver/crypto/p256_key_util_unittest.cc create mode 100644 chromium/components/gcm_driver/crypto/proto/gcm_encryption_data.proto create mode 100644 chromium/components/gcm_driver/fake_gcm_app_handler.cc create mode 100644 chromium/components/gcm_driver/fake_gcm_app_handler.h create mode 100644 chromium/components/gcm_driver/fake_gcm_client.cc create mode 100644 chromium/components/gcm_driver/fake_gcm_client.h create mode 100644 chromium/components/gcm_driver/fake_gcm_client_factory.cc create mode 100644 chromium/components/gcm_driver/fake_gcm_client_factory.h create mode 100644 chromium/components/gcm_driver/fake_gcm_driver.cc create mode 100644 chromium/components/gcm_driver/fake_gcm_driver.h create mode 100644 chromium/components/gcm_driver/fake_gcm_profile_service.cc create mode 100644 chromium/components/gcm_driver/fake_gcm_profile_service.h create mode 100644 chromium/components/gcm_driver/features.cc create mode 100644 chromium/components/gcm_driver/features.h create mode 100644 chromium/components/gcm_driver/gcm_account_mapper.cc create mode 100644 chromium/components/gcm_driver/gcm_account_mapper.h create mode 100644 chromium/components/gcm_driver/gcm_account_mapper_unittest.cc create mode 100644 chromium/components/gcm_driver/gcm_account_tracker.cc create mode 100644 chromium/components/gcm_driver/gcm_account_tracker.h create mode 100644 chromium/components/gcm_driver/gcm_account_tracker_unittest.cc create mode 100644 chromium/components/gcm_driver/gcm_activity.cc create mode 100644 chromium/components/gcm_driver/gcm_activity.h create mode 100644 chromium/components/gcm_driver/gcm_app_handler.cc create mode 100644 chromium/components/gcm_driver/gcm_app_handler.h create mode 100644 chromium/components/gcm_driver/gcm_backoff_policy.cc create mode 100644 chromium/components/gcm_driver/gcm_backoff_policy.h create mode 100644 chromium/components/gcm_driver/gcm_client.cc create mode 100644 chromium/components/gcm_driver/gcm_client.h create mode 100644 chromium/components/gcm_driver/gcm_client_factory.cc create mode 100644 chromium/components/gcm_driver/gcm_client_factory.h create mode 100644 chromium/components/gcm_driver/gcm_client_impl.cc create mode 100644 chromium/components/gcm_driver/gcm_client_impl.h create mode 100644 chromium/components/gcm_driver/gcm_client_impl_unittest.cc create mode 100644 chromium/components/gcm_driver/gcm_connection_observer.cc create mode 100644 chromium/components/gcm_driver/gcm_connection_observer.h create mode 100644 chromium/components/gcm_driver/gcm_delayed_task_controller.cc create mode 100644 chromium/components/gcm_driver/gcm_delayed_task_controller.h create mode 100644 chromium/components/gcm_driver/gcm_delayed_task_controller_unittest.cc create mode 100644 chromium/components/gcm_driver/gcm_desktop_utils.cc create mode 100644 chromium/components/gcm_driver/gcm_desktop_utils.h create mode 100644 chromium/components/gcm_driver/gcm_driver.cc create mode 100644 chromium/components/gcm_driver/gcm_driver.h create mode 100644 chromium/components/gcm_driver/gcm_driver_android.cc create mode 100644 chromium/components/gcm_driver/gcm_driver_android.h create mode 100644 chromium/components/gcm_driver/gcm_driver_constants.cc create mode 100644 chromium/components/gcm_driver/gcm_driver_constants.h create mode 100644 chromium/components/gcm_driver/gcm_driver_desktop.cc create mode 100644 chromium/components/gcm_driver/gcm_driver_desktop.h create mode 100644 chromium/components/gcm_driver/gcm_driver_desktop_unittest.cc create mode 100644 chromium/components/gcm_driver/gcm_driver_unittest.cc create mode 100644 chromium/components/gcm_driver/gcm_internals_constants.cc create mode 100644 chromium/components/gcm_driver/gcm_internals_constants.h create mode 100644 chromium/components/gcm_driver/gcm_internals_helper.cc create mode 100644 chromium/components/gcm_driver/gcm_internals_helper.h create mode 100644 chromium/components/gcm_driver/gcm_profile_service.cc create mode 100644 chromium/components/gcm_driver/gcm_profile_service.h create mode 100644 chromium/components/gcm_driver/gcm_stats_recorder_android.cc create mode 100644 chromium/components/gcm_driver/gcm_stats_recorder_android.h create mode 100644 chromium/components/gcm_driver/gcm_stats_recorder_android_unittest.cc create mode 100644 chromium/components/gcm_driver/gcm_stats_recorder_impl.cc create mode 100644 chromium/components/gcm_driver/gcm_stats_recorder_impl.h create mode 100644 chromium/components/gcm_driver/gcm_stats_recorder_impl_unittest.cc create mode 100644 chromium/components/gcm_driver/instance_id/DEPS create mode 100644 chromium/components/gcm_driver/instance_id/android/javatests/src/org/chromium/components/gcm_driver/instance_id/FakeInstanceIDWithSubtype.java create mode 100644 chromium/components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.cc create mode 100644 chromium/components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.h create mode 100644 chromium/components/gcm_driver/instance_id/instance_id.cc create mode 100644 chromium/components/gcm_driver/instance_id/instance_id.h create mode 100644 chromium/components/gcm_driver/instance_id/instance_id_android.cc create mode 100644 chromium/components/gcm_driver/instance_id/instance_id_android.h create mode 100644 chromium/components/gcm_driver/instance_id/instance_id_driver.cc create mode 100644 chromium/components/gcm_driver/instance_id/instance_id_driver.h create mode 100644 chromium/components/gcm_driver/instance_id/instance_id_driver_unittest.cc create mode 100644 chromium/components/gcm_driver/instance_id/instance_id_impl.cc create mode 100644 chromium/components/gcm_driver/instance_id/instance_id_impl.h create mode 100644 chromium/components/gcm_driver/instance_id/instance_id_profile_service.cc create mode 100644 chromium/components/gcm_driver/instance_id/instance_id_profile_service.h create mode 100644 chromium/components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.cc create mode 100644 chromium/components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.h create mode 100644 chromium/components/gcm_driver/registration_info.cc create mode 100644 chromium/components/gcm_driver/registration_info.h create mode 100644 chromium/components/gcm_driver/resources/OWNERS create mode 100644 chromium/components/gcm_driver/resources/gcm_internals.css create mode 100644 chromium/components/gcm_driver/resources/gcm_internals.html create mode 100644 chromium/components/gcm_driver/resources/gcm_internals.js create mode 100644 chromium/components/gcm_driver/system_encryptor.cc create mode 100644 chromium/components/gcm_driver/system_encryptor.h diff --git a/chromium/chrome/browser/gcm/COMMON_METADATA b/chromium/chrome/browser/gcm/COMMON_METADATA new file mode 100644 index 00000000000..777cdb6a192 --- /dev/null +++ b/chromium/chrome/browser/gcm/COMMON_METADATA @@ -0,0 +1,4 @@ +monorail: { + component: "Services>CloudMessaging" +} +team_email: "platform-capabilities@chromium.org" diff --git a/chromium/chrome/browser/gcm/DIR_METADATA b/chromium/chrome/browser/gcm/DIR_METADATA new file mode 100644 index 00000000000..66fe8683773 --- /dev/null +++ b/chromium/chrome/browser/gcm/DIR_METADATA @@ -0,0 +1 @@ +mixins: "//chrome/browser/gcm/COMMON_METADATA" diff --git a/chromium/chrome/browser/gcm/OWNERS b/chromium/chrome/browser/gcm/OWNERS new file mode 100644 index 00000000000..06f7d3cbe88 --- /dev/null +++ b/chromium/chrome/browser/gcm/OWNERS @@ -0,0 +1,4 @@ +dimich@chromium.org +fgorski@chromium.org +jianli@chromium.org +peter@chromium.org diff --git a/chromium/chrome/browser/gcm/gcm_product_util.cc b/chromium/chrome/browser/gcm/gcm_product_util.cc new file mode 100644 index 00000000000..cb2fa486c11 --- /dev/null +++ b/chromium/chrome/browser/gcm/gcm_product_util.cc @@ -0,0 +1,57 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/gcm/gcm_product_util.h" + +#include "base/strings/string_piece.h" +#include "base/strings/string_util.h" +#include "chrome/common/chrome_version.h" +#include "chrome/common/pref_names.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/prefs/pref_registry_simple.h" +#include "components/prefs/pref_service.h" +#include "components/version_info/version_info.h" + +namespace gcm { + +namespace { + +std::string ToLowerAlphaNum(base::StringPiece in) { + std::string out; + out.reserve(in.size()); + for (char ch : in) { + if (base::IsAsciiAlpha(ch) || base::IsAsciiDigit(ch)) + out.push_back(base::ToLowerASCII(ch)); + } + return out; +} + +} // namespace + +std::string GetProductCategoryForSubtypes(PrefService* prefs) { + std::string product_category_for_subtypes = + prefs->GetString(prefs::kGCMProductCategoryForSubtypes); + if (!product_category_for_subtypes.empty()) + return product_category_for_subtypes; + + std::string product = ToLowerAlphaNum(PRODUCT_SHORTNAME_STRING); + std::string ns = product == "chromium" ? "org" : "com"; + std::string platform = ToLowerAlphaNum(version_info::GetOSType()); + product_category_for_subtypes = ns + '.' + product + '.' + platform; + + prefs->SetString(prefs::kGCMProductCategoryForSubtypes, + product_category_for_subtypes); + return product_category_for_subtypes; +} + +void RegisterPrefs(PrefRegistrySimple* registry) { + registry->RegisterStringPref(prefs::kGCMProductCategoryForSubtypes, + std::string() /* default_value */); +} + +void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) { + RegisterPrefs(registry); +} + +} // namespace gcm diff --git a/chromium/chrome/browser/gcm/gcm_product_util.h b/chromium/chrome/browser/gcm/gcm_product_util.h new file mode 100644 index 00000000000..f91c869b310 --- /dev/null +++ b/chromium/chrome/browser/gcm/gcm_product_util.h @@ -0,0 +1,30 @@ +// Copyright 2016 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 CHROME_BROWSER_GCM_GCM_PRODUCT_UTIL_H_ +#define CHROME_BROWSER_GCM_GCM_PRODUCT_UTIL_H_ + +#include + +class PrefRegistrySimple; +class PrefService; + +namespace user_prefs { +class PrefRegistrySyncable; +} + +namespace gcm { + +// Returns a string like "com.chrome.macosx" that should be used as the GCM +// category when an app_id is sent as a subtype instead of as a category. This +// is generated once, then remains fixed forever (even if the product name +// changes), since it must match existing Instance ID tokens. +std::string GetProductCategoryForSubtypes(PrefService* prefs); + +void RegisterPrefs(PrefRegistrySimple* registry); +void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry); + +} // namespace gcm + +#endif // CHROME_BROWSER_GCM_GCM_PRODUCT_UTIL_H_ diff --git a/chromium/chrome/browser/gcm/gcm_profile_service_factory.cc b/chromium/chrome/browser/gcm/gcm_profile_service_factory.cc new file mode 100644 index 00000000000..b3207963fdb --- /dev/null +++ b/chromium/chrome/browser/gcm/gcm_profile_service_factory.cc @@ -0,0 +1,174 @@ +// Copyright (c) 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 "chrome/browser/gcm/gcm_profile_service_factory.h" +#include + +#include "base/bind.h" +#include "base/no_destructor.h" +#include "base/task/sequenced_task_runner.h" +#include "base/task/thread_pool.h" +#include "build/build_config.h" +#include "chrome/browser/profiles/incognito_helpers.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_key.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "components/gcm_driver/gcm_profile_service.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/offline_pages/buildflags/buildflags.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/network_service_instance.h" +#include "content/public/browser/storage_partition.h" +#include "services/network/public/mojom/network_context.mojom.h" + +#if !defined(OS_ANDROID) +#include "chrome/browser/gcm/gcm_product_util.h" +#include "chrome/common/channel_info.h" +#include "components/gcm_driver/gcm_client_factory.h" +#include "content/public/browser/browser_context.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#endif + +#if BUILDFLAG(ENABLE_OFFLINE_PAGES) +#include "chrome/browser/offline_pages/prefetch/prefetch_service_factory.h" +#include "components/gcm_driver/gcm_driver.h" +#include "components/offline_pages/core/offline_page_feature.h" +#include "components/offline_pages/core/prefetch/prefetch_gcm_app_handler.h" +#include "components/offline_pages/core/prefetch/prefetch_service.h" +#endif + +namespace gcm { + +namespace { + +#if !defined(OS_ANDROID) +// Requests a ProxyResolvingSocketFactory on the UI thread. Note that a WeakPtr +// of GCMProfileService is needed to detect when the KeyedService shuts down, +// and avoid calling into |profile| which might have also been destroyed. +void RequestProxyResolvingSocketFactoryOnUIThread( + Profile* profile, + base::WeakPtr service, + mojo::PendingReceiver + receiver) { + if (!service) + return; + network::mojom::NetworkContext* network_context = + profile->GetDefaultStoragePartition()->GetNetworkContext(); + network_context->CreateProxyResolvingSocketFactory(std::move(receiver)); +} + +// A thread-safe wrapper to request a ProxyResolvingSocketFactory. +void RequestProxyResolvingSocketFactory( + Profile* profile, + base::WeakPtr service, + mojo::PendingReceiver + receiver) { + content::GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, + base::BindOnce(&RequestProxyResolvingSocketFactoryOnUIThread, profile, + std::move(service), std::move(receiver))); +} +#endif + +BrowserContextKeyedServiceFactory::TestingFactory& GetTestingFactory() { + static base::NoDestructor + testing_factory; + return *testing_factory; +} + +} // namespace + +GCMProfileServiceFactory::ScopedTestingFactoryInstaller:: + ScopedTestingFactoryInstaller(TestingFactory testing_factory) { + DCHECK(!GetTestingFactory()); + GetTestingFactory() = std::move(testing_factory); +} + +GCMProfileServiceFactory::ScopedTestingFactoryInstaller:: + ~ScopedTestingFactoryInstaller() { + GetTestingFactory() = BrowserContextKeyedServiceFactory::TestingFactory(); +} + +// static +GCMProfileService* GCMProfileServiceFactory::GetForProfile( + content::BrowserContext* profile) { + // GCM is not supported in incognito mode. + if (profile->IsOffTheRecord()) + return NULL; + + return static_cast( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +// static +GCMProfileServiceFactory* GCMProfileServiceFactory::GetInstance() { + static base::NoDestructor instance; + return instance.get(); +} + +GCMProfileServiceFactory::GCMProfileServiceFactory() + : BrowserContextKeyedServiceFactory( + "GCMProfileService", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(IdentityManagerFactory::GetInstance()); +#if BUILDFLAG(ENABLE_OFFLINE_PAGES) + DependsOn(offline_pages::PrefetchServiceFactory::GetInstance()); +#endif // BUILDFLAG(ENABLE_OFFLINE_PAGES) +} + +GCMProfileServiceFactory::~GCMProfileServiceFactory() { +} + +KeyedService* GCMProfileServiceFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + DCHECK(!profile->IsOffTheRecord()); + + TestingFactory& testing_factory = GetTestingFactory(); + if (testing_factory) + return testing_factory.Run(context).release(); + + scoped_refptr blocking_task_runner( + base::ThreadPool::CreateSequencedTaskRunner( + {base::MayBlock(), base::TaskPriority::BEST_EFFORT, + base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})); + std::unique_ptr service; +#if defined(OS_ANDROID) + service = std::make_unique(profile->GetPath(), + blocking_task_runner); +#else + service = std::make_unique( + profile->GetPrefs(), profile->GetPath(), + base::BindRepeating(&RequestProxyResolvingSocketFactory, profile), + profile->GetDefaultStoragePartition() + ->GetURLLoaderFactoryForBrowserProcess(), + content::GetNetworkConnectionTracker(), chrome::GetChannel(), + gcm::GetProductCategoryForSubtypes(profile->GetPrefs()), + IdentityManagerFactory::GetForProfile(profile), + std::make_unique(), content::GetUIThreadTaskRunner({}), + content::GetIOThreadTaskRunner({}), blocking_task_runner); +#endif +#if BUILDFLAG(ENABLE_OFFLINE_PAGES) + offline_pages::PrefetchService* prefetch_service = + offline_pages::PrefetchServiceFactory::GetForKey( + profile->GetProfileKey()); + if (prefetch_service != nullptr) { + offline_pages::PrefetchGCMHandler* prefetch_gcm_handler = + prefetch_service->GetPrefetchGCMHandler(); + service->driver()->AddAppHandler(prefetch_gcm_handler->GetAppId(), + prefetch_gcm_handler->AsGCMAppHandler()); + } +#endif // BUILDFLAG(ENABLE_OFFLINE_PAGES) + + return service.release(); +} + +content::BrowserContext* GCMProfileServiceFactory::GetBrowserContextToUse( + content::BrowserContext* context) const { + return chrome::GetBrowserContextOwnInstanceInIncognito(context); +} + +} // namespace gcm diff --git a/chromium/chrome/browser/gcm/gcm_profile_service_factory.h b/chromium/chrome/browser/gcm/gcm_profile_service_factory.h new file mode 100644 index 00000000000..13511e84611 --- /dev/null +++ b/chromium/chrome/browser/gcm/gcm_profile_service_factory.h @@ -0,0 +1,57 @@ +// Copyright (c) 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 CHROME_BROWSER_GCM_GCM_PROFILE_SERVICE_FACTORY_H_ +#define CHROME_BROWSER_GCM_GCM_PROFILE_SERVICE_FACTORY_H_ + +#include "base/no_destructor.h" +#include "components/gcm_driver/system_encryptor.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +namespace gcm { + +class GCMProfileService; + +// Singleton that owns all GCMProfileService and associates them with +// Profiles. +class GCMProfileServiceFactory : public BrowserContextKeyedServiceFactory { + public: + static GCMProfileService* GetForProfile(content::BrowserContext* profile); + static GCMProfileServiceFactory* GetInstance(); + + // Helper registering a testing factory. Needs to be instantiated before the + // factory is accessed in your test, and deallocated after the last access. + // Usually this is achieved by putting this object as the first member in + // your test fixture. + class ScopedTestingFactoryInstaller { + public: + explicit ScopedTestingFactoryInstaller(TestingFactory testing_factory); + + ScopedTestingFactoryInstaller(const ScopedTestingFactoryInstaller&) = + delete; + ScopedTestingFactoryInstaller& operator=( + const ScopedTestingFactoryInstaller&) = delete; + + ~ScopedTestingFactoryInstaller(); + }; + + GCMProfileServiceFactory(const GCMProfileServiceFactory&) = delete; + GCMProfileServiceFactory& operator=(const GCMProfileServiceFactory&) = delete; + + private: + friend base::NoDestructor; + + GCMProfileServiceFactory(); + ~GCMProfileServiceFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const override; + content::BrowserContext* GetBrowserContextToUse( + content::BrowserContext* context) const override; +}; + +} // namespace gcm + +#endif // CHROME_BROWSER_GCM_GCM_PROFILE_SERVICE_FACTORY_H_ diff --git a/chromium/chrome/browser/gcm/gcm_profile_service_unittest.cc b/chromium/chrome/browser/gcm/gcm_profile_service_unittest.cc new file mode 100644 index 00000000000..2a13932a454 --- /dev/null +++ b/chromium/chrome/browser/gcm/gcm_profile_service_unittest.cc @@ -0,0 +1,269 @@ +// 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 +#include + +#include "base/bind.h" +#include "base/callback.h" +#include "base/callback_helpers.h" +#include "base/memory/raw_ptr.h" +#include "base/run_loop.h" +#include "base/task/sequenced_task_runner.h" +#include "base/task/thread_pool.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/gcm/gcm_product_util.h" +#include "chrome/browser/gcm/gcm_profile_service_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/common/channel_info.h" +#include "chrome/test/base/testing_profile.h" +#include "components/gcm_driver/fake_gcm_app_handler.h" +#include "components/gcm_driver/fake_gcm_client.h" +#include "components/gcm_driver/fake_gcm_client_factory.h" +#include "components/gcm_driver/gcm_client.h" +#include "components/gcm_driver/gcm_client_factory.h" +#include "components/gcm_driver/gcm_driver.h" +#include "components/gcm_driver/gcm_profile_service.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/storage_partition.h" +#include "content/public/test/browser_task_environment.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "services/network/public/cpp/resource_request.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#include "services/network/public/mojom/network_context.mojom.h" +#include "services/network/test/test_network_connection_tracker.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "chromeos/dbus/concierge/concierge_client.h" +#endif + +namespace gcm { + +namespace { + +const char kTestAppID[] = "TestApp"; +const char kUserID[] = "user"; + +void RequestProxyResolvingSocketFactoryOnUIThread( + Profile* profile, + base::WeakPtr service, + mojo::PendingReceiver + receiver) { + if (!service) + return; + return profile->GetDefaultStoragePartition() + ->GetNetworkContext() + ->CreateProxyResolvingSocketFactory(std::move(receiver)); +} + +void RequestProxyResolvingSocketFactory( + Profile* profile, + base::WeakPtr service, + mojo::PendingReceiver + receiver) { + content::GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, base::BindOnce(&RequestProxyResolvingSocketFactoryOnUIThread, + profile, service, std::move(receiver))); +} + +std::unique_ptr BuildGCMProfileService( + content::BrowserContext* context) { + Profile* profile = Profile::FromBrowserContext(context); + scoped_refptr blocking_task_runner( + base::ThreadPool::CreateSequencedTaskRunner( + {base::MayBlock(), base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})); + return std::make_unique( + profile->GetPrefs(), profile->GetPath(), + base::BindRepeating(&RequestProxyResolvingSocketFactory, profile), + profile->GetDefaultStoragePartition() + ->GetURLLoaderFactoryForBrowserProcess(), + network::TestNetworkConnectionTracker::GetInstance(), + chrome::GetChannel(), + gcm::GetProductCategoryForSubtypes(profile->GetPrefs()), + IdentityManagerFactory::GetForProfile(profile), + std::unique_ptr( + new gcm::FakeGCMClientFactory(content::GetUIThreadTaskRunner({}), + content::GetIOThreadTaskRunner({}))), + content::GetUIThreadTaskRunner({}), content::GetIOThreadTaskRunner({}), + blocking_task_runner); +} + +} // namespace + +class GCMProfileServiceTest : public testing::Test { + public: + GCMProfileServiceTest(const GCMProfileServiceTest&) = delete; + GCMProfileServiceTest& operator=(const GCMProfileServiceTest&) = delete; + + protected: + GCMProfileServiceTest(); + ~GCMProfileServiceTest() override; + + // testing::Test: + void SetUp() override; + void TearDown() override; + + FakeGCMClient* GetGCMClient() const; + + void CreateGCMProfileService(); + + void RegisterAndWaitForCompletion(const std::vector& sender_ids); + void UnregisterAndWaitForCompletion(); + void SendAndWaitForCompletion(const OutgoingMessage& message); + + void RegisterCompleted(base::OnceClosure callback, + const std::string& registration_id, + GCMClient::Result result); + void UnregisterCompleted(base::OnceClosure callback, + GCMClient::Result result); + void SendCompleted(base::OnceClosure callback, + const std::string& message_id, + GCMClient::Result result); + + GCMDriver* driver() const { return gcm_profile_service_->driver(); } + std::string registration_id() const { return registration_id_; } + GCMClient::Result registration_result() const { return registration_result_; } + GCMClient::Result unregistration_result() const { + return unregistration_result_; + } + std::string send_message_id() const { return send_message_id_; } + GCMClient::Result send_result() const { return send_result_; } + + private: + content::BrowserTaskEnvironment task_environment_; + std::unique_ptr profile_; + raw_ptr gcm_profile_service_; + std::unique_ptr gcm_app_handler_; + + std::string registration_id_; + GCMClient::Result registration_result_; + GCMClient::Result unregistration_result_; + std::string send_message_id_; + GCMClient::Result send_result_; +}; + +GCMProfileServiceTest::GCMProfileServiceTest() + : gcm_profile_service_(nullptr), + gcm_app_handler_(new FakeGCMAppHandler), + registration_result_(GCMClient::UNKNOWN_ERROR), + send_result_(GCMClient::UNKNOWN_ERROR) {} + +GCMProfileServiceTest::~GCMProfileServiceTest() { +} + +FakeGCMClient* GCMProfileServiceTest::GetGCMClient() const { + return static_cast( + gcm_profile_service_->driver()->GetGCMClientForTesting()); +} + +void GCMProfileServiceTest::SetUp() { +#if BUILDFLAG(IS_CHROMEOS_ASH) + chromeos::ConciergeClient::InitializeFake(/*fake_cicerone_client=*/nullptr); +#endif + TestingProfile::Builder builder; + profile_ = builder.Build(); +} + +void GCMProfileServiceTest::TearDown() { + gcm_profile_service_->driver()->RemoveAppHandler(kTestAppID); +#if BUILDFLAG(IS_CHROMEOS_ASH) + profile_.reset(); + chromeos::ConciergeClient::Shutdown(); +#endif +} + +void GCMProfileServiceTest::CreateGCMProfileService() { + gcm_profile_service_ = static_cast( + GCMProfileServiceFactory::GetInstance()->SetTestingFactoryAndUse( + profile_.get(), base::BindRepeating(&BuildGCMProfileService))); + gcm_profile_service_->driver()->AddAppHandler( + kTestAppID, gcm_app_handler_.get()); +} + +void GCMProfileServiceTest::RegisterAndWaitForCompletion( + const std::vector& sender_ids) { + base::RunLoop run_loop; + gcm_profile_service_->driver()->Register( + kTestAppID, sender_ids, + base::BindOnce(&GCMProfileServiceTest::RegisterCompleted, + base::Unretained(this), run_loop.QuitClosure())); + run_loop.Run(); +} + +void GCMProfileServiceTest::UnregisterAndWaitForCompletion() { + base::RunLoop run_loop; + gcm_profile_service_->driver()->Unregister( + kTestAppID, + base::BindOnce(&GCMProfileServiceTest::UnregisterCompleted, + base::Unretained(this), run_loop.QuitClosure())); + run_loop.Run(); +} + +void GCMProfileServiceTest::SendAndWaitForCompletion( + const OutgoingMessage& message) { + base::RunLoop run_loop; + gcm_profile_service_->driver()->Send( + kTestAppID, kUserID, message, + base::BindOnce(&GCMProfileServiceTest::SendCompleted, + base::Unretained(this), run_loop.QuitClosure())); + run_loop.Run(); +} + +void GCMProfileServiceTest::RegisterCompleted( + base::OnceClosure callback, + const std::string& registration_id, + GCMClient::Result result) { + registration_id_ = registration_id; + registration_result_ = result; + std::move(callback).Run(); +} + +void GCMProfileServiceTest::UnregisterCompleted(base::OnceClosure callback, + GCMClient::Result result) { + unregistration_result_ = result; + std::move(callback).Run(); +} + +void GCMProfileServiceTest::SendCompleted(base::OnceClosure callback, + const std::string& message_id, + GCMClient::Result result) { + send_message_id_ = message_id; + send_result_ = result; + std::move(callback).Run(); +} + +TEST_F(GCMProfileServiceTest, RegisterAndUnregister) { + CreateGCMProfileService(); + + std::vector sender_ids; + sender_ids.push_back("sender"); + RegisterAndWaitForCompletion(sender_ids); + + std::string expected_registration_id = + FakeGCMClient::GenerateGCMRegistrationID(sender_ids); + EXPECT_EQ(expected_registration_id, registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, registration_result()); + + UnregisterAndWaitForCompletion(); + EXPECT_EQ(GCMClient::SUCCESS, unregistration_result()); +} + +TEST_F(GCMProfileServiceTest, Send) { + CreateGCMProfileService(); + + OutgoingMessage message; + message.id = "1"; + message.data["key1"] = "value1"; + SendAndWaitForCompletion(message); + + EXPECT_EQ(message.id, send_message_id()); + EXPECT_EQ(GCMClient::SUCCESS, send_result()); +} + +} // namespace gcm diff --git a/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.cc b/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.cc new file mode 100644 index 00000000000..8123d8f04c6 --- /dev/null +++ b/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.cc @@ -0,0 +1,60 @@ +// Copyright (c) 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h" + +#include + +#include "chrome/browser/gcm/gcm_profile_service_factory.h" +#include "chrome/browser/profiles/incognito_helpers.h" +#include "chrome/browser/profiles/profile.h" +#include "components/gcm_driver/gcm_profile_service.h" +#include "components/gcm_driver/instance_id/instance_id_profile_service.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" + +namespace instance_id { + +// static +InstanceIDProfileService* InstanceIDProfileServiceFactory::GetForProfile( + content::BrowserContext* profile) { + // Instance ID is not supported in incognito mode. + if (profile->IsOffTheRecord()) + return NULL; + + return static_cast( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +// static +InstanceIDProfileServiceFactory* +InstanceIDProfileServiceFactory::GetInstance() { + return base::Singleton::get(); +} + +InstanceIDProfileServiceFactory::InstanceIDProfileServiceFactory() + : BrowserContextKeyedServiceFactory( + "InstanceIDProfileService", + BrowserContextDependencyManager::GetInstance()) { + // GCM is needed for device ID. + DependsOn(gcm::GCMProfileServiceFactory::GetInstance()); +} + +InstanceIDProfileServiceFactory::~InstanceIDProfileServiceFactory() { +} + +KeyedService* InstanceIDProfileServiceFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + return new InstanceIDProfileService( + gcm::GCMProfileServiceFactory::GetForProfile(profile)->driver(), + profile->IsOffTheRecord()); +} + +content::BrowserContext* +InstanceIDProfileServiceFactory::GetBrowserContextToUse( + content::BrowserContext* context) const { + return chrome::GetBrowserContextOwnInstanceInIncognito(context); +} + +} // namespace instance_id diff --git a/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h b/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h new file mode 100644 index 00000000000..c4505760a3d --- /dev/null +++ b/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h @@ -0,0 +1,44 @@ +// Copyright (c) 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_GCM_INSTANCE_ID_INSTANCE_ID_PROFILE_SERVICE_FACTORY_H_ +#define CHROME_BROWSER_GCM_INSTANCE_ID_INSTANCE_ID_PROFILE_SERVICE_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +namespace instance_id { + +class InstanceIDProfileService; + +// Singleton that owns all InstanceIDProfileService and associates them with +// profiles. +class InstanceIDProfileServiceFactory : + public BrowserContextKeyedServiceFactory { + public: + static InstanceIDProfileService* GetForProfile( + content::BrowserContext* profile); + static InstanceIDProfileServiceFactory* GetInstance(); + + InstanceIDProfileServiceFactory(const InstanceIDProfileServiceFactory&) = + delete; + InstanceIDProfileServiceFactory& operator=( + const InstanceIDProfileServiceFactory&) = delete; + + private: + friend struct base::DefaultSingletonTraits; + + InstanceIDProfileServiceFactory(); + ~InstanceIDProfileServiceFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const override; + content::BrowserContext* GetBrowserContextToUse( + content::BrowserContext* context) const override; +}; + +} // namespace instance_id + +#endif // CHROME_BROWSER_GCM_INSTANCE_ID_INSTANCE_ID_PROFILE_SERVICE_FACTORY_H_ diff --git a/chromium/chrome/browser/profiles/incognito_helpers.cc b/chromium/chrome/browser/profiles/incognito_helpers.cc new file mode 100644 index 00000000000..a319237928b --- /dev/null +++ b/chromium/chrome/browser/profiles/incognito_helpers.cc @@ -0,0 +1,28 @@ +// 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 "chrome/browser/profiles/incognito_helpers.h" + +#include "chrome/browser/profiles/profile.h" + +namespace chrome { + +content::BrowserContext* GetBrowserContextRedirectedInIncognito( + content::BrowserContext* context) { + return Profile::FromBrowserContext(context)->GetOriginalProfile(); +} + +const content::BrowserContext* GetBrowserContextRedirectedInIncognito( + const content::BrowserContext* context) { + const Profile* profile = Profile::FromBrowserContext( + const_cast(context)); + return profile->GetOriginalProfile(); +} + +content::BrowserContext* GetBrowserContextOwnInstanceInIncognito( + content::BrowserContext* context) { + return context; +} + +} // namespace chrome diff --git a/chromium/chrome/browser/profiles/incognito_helpers.h b/chromium/chrome/browser/profiles/incognito_helpers.h new file mode 100644 index 00000000000..e8e76ce5b95 --- /dev/null +++ b/chromium/chrome/browser/profiles/incognito_helpers.h @@ -0,0 +1,29 @@ +// 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 CHROME_BROWSER_PROFILES_INCOGNITO_HELPERS_H_ +#define CHROME_BROWSER_PROFILES_INCOGNITO_HELPERS_H_ + +namespace content { +class BrowserContext; +} + +namespace chrome { + +// Returns the original browser context even for Incognito contexts. +content::BrowserContext* GetBrowserContextRedirectedInIncognito( + content::BrowserContext* context); + +// Returns the original browser context even for Incognito contexts. +const content::BrowserContext* GetBrowserContextRedirectedInIncognito( + const content::BrowserContext* context); + +// Returns non-NULL even for Incognito contexts so that a separate +// instance of a service is created for the Incognito context. +content::BrowserContext* GetBrowserContextOwnInstanceInIncognito( + content::BrowserContext* context); + +} // namespace chrome + +#endif // CHROME_BROWSER_PROFILES_INCOGNITO_HELPERS_H_ diff --git a/chromium/chrome/browser/push_messaging/DIR_METADATA b/chromium/chrome/browser/push_messaging/DIR_METADATA new file mode 100644 index 00000000000..a684b81174a --- /dev/null +++ b/chromium/chrome/browser/push_messaging/DIR_METADATA @@ -0,0 +1 @@ +mixins: "//content/browser/push_messaging/COMMON_METADATA" diff --git a/chromium/chrome/browser/push_messaging/OWNERS b/chromium/chrome/browser/push_messaging/OWNERS new file mode 100644 index 00000000000..d09ffef01de --- /dev/null +++ b/chromium/chrome/browser/push_messaging/OWNERS @@ -0,0 +1 @@ +file://content/browser/push_messaging/OWNERS diff --git a/chromium/chrome/browser/push_messaging/budget.proto b/chromium/chrome/browser/push_messaging/budget.proto new file mode 100644 index 00000000000..229ea5370b7 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/budget.proto @@ -0,0 +1,30 @@ +// Copyright 2016 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. + +syntax = "proto2"; + +package budget_service; + +// Chrome requires this. +option optimize_for = LITE_RUNTIME; + +// Next available id: 3 +message Budget { + // The sequence of budget chunks and their expiration times. + repeated BudgetChunk budget = 1; + + // The timestamp of the last time that new engagement budget was awarded. + // This stores the internal value needed to construct a base::Time object. + optional int64 engagement_last_updated = 2; +} + +// Next available id: 3 +message BudgetChunk { + // The amount of budget remaining in this chunk. + optional double amount = 1; + + // The timestamp when the budget expires. This stores the internal value + // needed to construct a base::Time object. + optional int64 expiration = 2; +} diff --git a/chromium/chrome/browser/push_messaging/budget_database.cc b/chromium/chrome/browser/push_messaging/budget_database.cc new file mode 100644 index 00000000000..96b2d36d01d --- /dev/null +++ b/chromium/chrome/browser/push_messaging/budget_database.cc @@ -0,0 +1,400 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/push_messaging/budget_database.h" + +#include "base/bind.h" +#include "base/memory/ptr_util.h" +#include "base/metrics/histogram_macros.h" +#include "base/task/post_task.h" +#include "base/task/thread_pool.h" +#include "base/time/clock.h" +#include "base/time/default_clock.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/push_messaging/budget.pb.h" +#include "components/leveldb_proto/public/proto_database_provider.h" +#include "components/site_engagement/content/site_engagement_score.h" +#include "components/site_engagement/content/site_engagement_service.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/storage_partition.h" +#include "url/gurl.h" +#include "url/origin.h" + +using content::BrowserThread; + +namespace { + +// The default amount of time during which a budget will be valid. +constexpr int kBudgetDurationInDays = 4; + +// The amount of budget that a maximally engaged site should receive per hour. +// For context, silent push messages cost 2 each, so this allows 6 silent push +// messages a day for a fully engaged site. See budget_manager.cc for costs of +// various actions. +constexpr double kMaximumHourlyBudget = 12.0 / 24.0; + +} // namespace + +BudgetState::BudgetState() = default; +BudgetState::BudgetState(const BudgetState& other) = default; +BudgetState::~BudgetState() = default; + +BudgetState& BudgetState::operator=(const BudgetState& other) = default; + +BudgetDatabase::BudgetInfo::BudgetInfo() = default; + +BudgetDatabase::BudgetInfo::BudgetInfo(const BudgetInfo&& other) + : last_engagement_award(other.last_engagement_award) { + chunks = std::move(other.chunks); +} + +BudgetDatabase::BudgetInfo::~BudgetInfo() = default; + +BudgetDatabase::BudgetDatabase(Profile* profile) + : profile_(profile), clock_(base::WrapUnique(new base::DefaultClock)) { + auto* protodb_provider = + profile->GetDefaultStoragePartition()->GetProtoDatabaseProvider(); + // In incognito mode the provider service is not created. + if (!protodb_provider) + return; + + db_ = protodb_provider->GetDB( + leveldb_proto::ProtoDbType::BUDGET_DATABASE, + profile->GetPath().Append(FILE_PATH_LITERAL("BudgetDatabase")), + base::ThreadPool::CreateSequencedTaskRunner( + {base::MayBlock(), base::TaskPriority::BEST_EFFORT, + base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN})); + db_->Init(base::BindOnce(&BudgetDatabase::OnDatabaseInit, + weak_ptr_factory_.GetWeakPtr())); +} + +BudgetDatabase::~BudgetDatabase() = default; + +void BudgetDatabase::GetBudgetDetails(const url::Origin& origin, + GetBudgetCallback callback) { + SyncCache(origin, base::BindOnce(&BudgetDatabase::GetBudgetAfterSync, + weak_ptr_factory_.GetWeakPtr(), origin, + std::move(callback))); +} + +void BudgetDatabase::SpendBudget(const url::Origin& origin, + SpendBudgetCallback callback, + double amount) { + SyncCache(origin, base::BindOnce(&BudgetDatabase::SpendBudgetAfterSync, + weak_ptr_factory_.GetWeakPtr(), origin, + amount, std::move(callback))); +} + +void BudgetDatabase::SetClockForTesting(std::unique_ptr clock) { + clock_ = std::move(clock); +} + +void BudgetDatabase::OnDatabaseInit(leveldb_proto::Enums::InitStatus status) { + // TODO(harkness): Consider caching the budget database now? + if (status != leveldb_proto::Enums::InitStatus::kOK) + db_.reset(); +} + +bool BudgetDatabase::IsCached(const url::Origin& origin) const { + return budget_map_.find(origin) != budget_map_.end(); +} + +double BudgetDatabase::GetBudget(const url::Origin& origin) const { + double total = 0; + auto iter = budget_map_.find(origin); + if (iter == budget_map_.end()) + return total; + + const BudgetInfo& info = iter->second; + for (const BudgetChunk& chunk : info.chunks) + total += chunk.amount; + return total; +} + +void BudgetDatabase::AddToCache( + const url::Origin& origin, + CacheCallback callback, + bool success, + std::unique_ptr budget_proto) { + // If the database read failed or there's nothing to add, just return. + if (!success || !budget_proto) { + std::move(callback).Run(success); + return; + } + + // If there were two simultaneous loads, don't overwrite the cache value, + // which might have been updated after the previous load. + if (IsCached(origin)) { + std::move(callback).Run(success); + return; + } + + // Add the data to the cache, converting from the proto format to an STL + // format which is better for removing things from the list. + BudgetInfo& info = budget_map_[origin]; + for (const auto& chunk : budget_proto->budget()) { + info.chunks.emplace_back(chunk.amount(), + base::Time::FromInternalValue(chunk.expiration())); + } + + info.last_engagement_award = + base::Time::FromInternalValue(budget_proto->engagement_last_updated()); + + std::move(callback).Run(success); +} + +void BudgetDatabase::GetBudgetAfterSync(const url::Origin& origin, + GetBudgetCallback callback, + bool success) { + std::vector predictions; + + // If the database wasn't able to read the information, return the + // failure and an empty predictions array. + if (!success) { + std::move(callback).Run(std::move(predictions)); + return; + } + + // Now, build up the BudgetExpection. This is different from the format + // in which the cache stores the data. The cache stores chunks of budget and + // when that budget expires. The mojo array describes a set of times + // and the budget at those times. + double total = GetBudget(origin); + + // Always add one entry at the front of the list for the total budget now. + { + BudgetState prediction; + prediction.budget_at = total; + prediction.time = clock_->Now().ToJsTime(); + predictions.push_back(prediction); + } + + // Starting with the soonest expiring chunks, add entries for the + // expiration times going forward. + const BudgetChunks& chunks = budget_map_[origin].chunks; + for (const auto& chunk : chunks) { + BudgetState prediction; + total -= chunk.amount; + prediction.budget_at = total; + prediction.time = chunk.expiration.ToJsTime(); + predictions.push_back(prediction); + } + + std::move(callback).Run(std::move(predictions)); +} + +void BudgetDatabase::SpendBudgetAfterSync(const url::Origin& origin, + double amount, + SpendBudgetCallback callback, + bool success) { + if (!success) { + std::move(callback).Run(false /* success */); + return; + } + + // Get the current SES score, to generate UMA. + double score = GetSiteEngagementScoreForOrigin(origin); + + // Walk the list of budget chunks to see if the origin has enough budget. + double total = 0; + BudgetInfo& info = budget_map_[origin]; + for (const BudgetChunk& chunk : info.chunks) + total += chunk.amount; + + if (total < amount) { + UMA_HISTOGRAM_COUNTS_100("PushMessaging.SESForNoBudgetOrigin", score); + std::move(callback).Run(false /* success */); + return; + } else if (total < amount * 2) { + UMA_HISTOGRAM_COUNTS_100("PushMessaging.SESForLowBudgetOrigin", score); + } + + // Walk the chunks and remove enough budget to cover the needed amount. + double bill = amount; + for (auto iter = info.chunks.begin(); iter != info.chunks.end();) { + if (iter->amount > bill) { + iter->amount -= bill; + bill = 0; + break; + } + bill -= iter->amount; + iter = info.chunks.erase(iter); + } + + // There should have been enough budget to cover the entire bill. + DCHECK_EQ(0, bill); + + // Now that the cache is updated, write the data to the database. + WriteCachedValuesToDatabase( + origin, + base::BindOnce(&BudgetDatabase::SpendBudgetAfterWrite, + weak_ptr_factory_.GetWeakPtr(), std::move(callback))); +} + +// This converts the bool value which is returned from the database to a Mojo +// error type. +void BudgetDatabase::SpendBudgetAfterWrite(SpendBudgetCallback callback, + bool write_successful) { + // TODO(harkness): If the database write fails, the cache will be out of sync + // with the database. Consider ways to mitigate this. + if (!write_successful) { + std::move(callback).Run(false /* success */); + return; + } + std::move(callback).Run(true /* success */); +} + +void BudgetDatabase::WriteCachedValuesToDatabase(const url::Origin& origin, + StoreBudgetCallback callback) { + if (!db_) { + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), false)); + return; + } + + // Create the data structures that are passed to the ProtoDatabase. + std::unique_ptr< + leveldb_proto::ProtoDatabase::KeyEntryVector> + entries(new leveldb_proto::ProtoDatabase< + budget_service::Budget>::KeyEntryVector()); + std::unique_ptr> keys_to_remove( + new std::vector()); + + // Each operation can either update the existing budget or remove the origin's + // budget information. + if (IsCached(origin)) { + // Build the Budget proto object. + budget_service::Budget budget; + const BudgetInfo& info = budget_map_[origin]; + for (const auto& chunk : info.chunks) { + budget_service::BudgetChunk* budget_chunk = budget.add_budget(); + budget_chunk->set_amount(chunk.amount); + budget_chunk->set_expiration(chunk.expiration.ToInternalValue()); + } + budget.set_engagement_last_updated( + info.last_engagement_award.ToInternalValue()); + entries->push_back(std::make_pair(origin.Serialize(), budget)); + } else { + // If the origin doesn't exist in the cache, this is a remove operation. + keys_to_remove->push_back(origin.Serialize()); + } + + // Send the updates to the database. + db_->UpdateEntries(std::move(entries), std::move(keys_to_remove), + std::move(callback)); +} + +void BudgetDatabase::SyncCache(const url::Origin& origin, + CacheCallback callback) { + // If the origin isn't already cached, add it to the cache. + if (!IsCached(origin)) { + if (!db_) { + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), false)); + return; + } + CacheCallback add_callback = base::BindOnce( + &BudgetDatabase::SyncLoadedCache, weak_ptr_factory_.GetWeakPtr(), + origin, std::move(callback)); + db_->GetEntry(origin.Serialize(), + base::BindOnce(&BudgetDatabase::AddToCache, + weak_ptr_factory_.GetWeakPtr(), origin, + std::move(add_callback))); + return; + } + SyncLoadedCache(origin, std::move(callback), true /* success */); +} + +void BudgetDatabase::SyncLoadedCache(const url::Origin& origin, + CacheCallback callback, + bool success) { + if (!success) { + std::move(callback).Run(false /* success */); + return; + } + + // Now, cleanup any expired budget chunks for the origin. + bool needs_write = CleanupExpiredBudget(origin); + + // Get the SES score and add engagement budget for the site. + AddEngagementBudget(origin); + + if (needs_write) + WriteCachedValuesToDatabase(origin, std::move(callback)); + else + std::move(callback).Run(success); +} + +void BudgetDatabase::AddEngagementBudget(const url::Origin& origin) { + // Calculate how much budget should be awarded. The award depends on the + // time elapsed since the last award and the SES score. + // By default, give the origin kBudgetDurationInDays worth of budget, but + // reduce that if budget has already been given during that period. + base::TimeDelta elapsed = base::Days(kBudgetDurationInDays); + if (IsCached(origin)) { + elapsed = clock_->Now() - budget_map_[origin].last_engagement_award; + // Don't give engagement awards for periods less than an hour. + if (elapsed.InHours() < 1) + return; + // Cap elapsed time to the budget duration. + if (elapsed.InDays() > kBudgetDurationInDays) + elapsed = base::Days(kBudgetDurationInDays); + } + + // Get the current SES score, and calculate the hourly budget for that score. + double hourly_budget = kMaximumHourlyBudget * + GetSiteEngagementScoreForOrigin(origin) / + site_engagement::SiteEngagementService::GetMaxPoints(); + + // Update the last_engagement_award to the current time. If the origin wasn't + // already in the map, this adds a new entry for it. + budget_map_[origin].last_engagement_award = clock_->Now(); + + // Add a new chunk of budget for the origin at the default expiration time. + base::Time expiration = clock_->Now() + base::Days(kBudgetDurationInDays); + budget_map_[origin].chunks.emplace_back(elapsed.InHours() * hourly_budget, + expiration); + + // Any time we award engagement budget, which is done at most once an hour + // whenever any budget action is taken, record the budget. + double budget = GetBudget(origin); + UMA_HISTOGRAM_COUNTS_100("PushMessaging.BackgroundBudget", budget); +} + +// Cleans up budget in the cache. Relies on the caller eventually writing the +// cache back to the database. +bool BudgetDatabase::CleanupExpiredBudget(const url::Origin& origin) { + if (!IsCached(origin)) + return false; + + base::Time now = clock_->Now(); + BudgetChunks& chunks = budget_map_[origin].chunks; + auto cleanup_iter = chunks.begin(); + + // This relies on the list of chunks being in timestamp order. + while (cleanup_iter != chunks.end() && cleanup_iter->expiration <= now) + cleanup_iter = chunks.erase(cleanup_iter); + + // If the entire budget is empty now AND there have been no engagements + // in the last kBudgetDurationInDays days, remove this from the cache. + if (chunks.empty() && budget_map_[origin].last_engagement_award < + clock_->Now() - base::Days(kBudgetDurationInDays)) { + budget_map_.erase(origin); + return true; + } + + // Although some things may have expired, there are some chunks still valid. + // Don't write to the DB now, write either when all chunks expire or when the + // origin spends some budget. + return false; +} + +double BudgetDatabase::GetSiteEngagementScoreForOrigin( + const url::Origin& origin) const { + if (profile_->IsOffTheRecord()) + return 0; + + return site_engagement::SiteEngagementService::Get(profile_)->GetScore( + origin.GetURL()); +} diff --git a/chromium/chrome/browser/push_messaging/budget_database.h b/chromium/chrome/browser/push_messaging/budget_database.h new file mode 100644 index 00000000000..0ae20374262 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/budget_database.h @@ -0,0 +1,182 @@ +// Copyright 2016 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 CHROME_BROWSER_PUSH_MESSAGING_BUDGET_DATABASE_H_ +#define CHROME_BROWSER_PUSH_MESSAGING_BUDGET_DATABASE_H_ + +#include +#include +#include + +#include "base/callback_forward.h" +#include "base/gtest_prod_util.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "components/leveldb_proto/public/proto_database.h" + +namespace base { +class Clock; +class Time; +} // namespace base + +namespace budget_service { +class Budget; +} + +namespace url { +class Origin; +} + +class Profile; + +// Structure representing the budget at points in time in the future. +struct BudgetState { + BudgetState(); + BudgetState(const BudgetState& other); + ~BudgetState(); + + BudgetState& operator=(const BudgetState& other); + + // Amount of budget that will be available. This should be the lower bound of + // the budget between this time and the previous time. + double budget_at = 0; + + // Time at which the budget is available, in milliseconds since 00:00:00 UTC + // on 1 January 1970, at which the budget_at will be valid. + double time = 0; +}; + +// A class used to asynchronously read and write details of the budget +// assigned to an origin. The class uses an underlying LevelDB. +class BudgetDatabase { + public: + // The default amount of budget that should be spent. + static constexpr double kDefaultAmount = 2.0; + + // Callback for getting a list of all budget chunks. + using GetBudgetCallback = base::OnceCallback)>; + + // This is invoked only after the spend has been written to the database. + using SpendBudgetCallback = base::OnceCallback; + + // The database_dir specifies the location of the budget information on disk. + explicit BudgetDatabase(Profile* profile); + + BudgetDatabase(const BudgetDatabase&) = delete; + BudgetDatabase& operator=(const BudgetDatabase&) = delete; + + ~BudgetDatabase(); + + // Get the full budget expectation for the origin. This will return a + // sequence of time points and the expected budget at those times. + void GetBudgetDetails(const url::Origin& origin, GetBudgetCallback callback); + + // Spend a fixed (2.0) amount of budget for an origin. The callback indicates + // whether the budget could be spent for the given |origin|. + void SpendBudget(const url::Origin& origin, + SpendBudgetCallback callback, + double amount = kDefaultAmount); + + private: + FRIEND_TEST_ALL_PREFIXES(BudgetDatabaseTest, + DefaultSiteEngagementInIncognitoProfile); + friend class BudgetDatabaseTest; + + // Used to allow tests to change time for testing. + void SetClockForTesting(std::unique_ptr clock); + + // Holds information about individual pieces of awarded budget. There is a + // one-to-one mapping of these to the chunks in the underlying database. + struct BudgetChunk { + BudgetChunk(double amount, base::Time expiration) + : amount(amount), expiration(expiration) {} + BudgetChunk(const BudgetChunk&) = default; + BudgetChunk& operator=(const BudgetChunk&) = default; + + double amount; + base::Time expiration; + }; + + // Data structure for caching budget information. + using BudgetChunks = std::list; + + // Holds information about the overall budget for a site. This includes the + // time the budget was last incremented, as well as a list of budget chunks + // which have been awarded. + struct BudgetInfo { + BudgetInfo(); + + BudgetInfo(const BudgetInfo&) = delete; + BudgetInfo& operator=(const BudgetInfo&) = delete; + + BudgetInfo(const BudgetInfo&& other); + + ~BudgetInfo(); + + base::Time last_engagement_award; + BudgetChunks chunks; + }; + + // Callback for writing budget values to the database. + using StoreBudgetCallback = base::OnceCallback; + + using CacheCallback = base::OnceCallback; + + void OnDatabaseInit(leveldb_proto::Enums::InitStatus status); + + bool IsCached(const url::Origin& origin) const; + + double GetBudget(const url::Origin& origin) const; + + void AddToCache(const url::Origin& origin, + CacheCallback callback, + bool success, + std::unique_ptr budget); + + void GetBudgetAfterSync(const url::Origin& origin, + GetBudgetCallback callback, + bool success); + + void SpendBudgetAfterSync(const url::Origin& origin, + double amount, + SpendBudgetCallback callback, + bool success); + + void SpendBudgetAfterWrite(SpendBudgetCallback callback, bool success); + + void WriteCachedValuesToDatabase(const url::Origin& origin, + StoreBudgetCallback callback); + + void SyncCache(const url::Origin& origin, CacheCallback callback); + void SyncLoadedCache(const url::Origin& origin, + CacheCallback callback, + bool success); + + // Add budget based on engagement with an origin. The method queries for the + // engagement score of the origin, and then calculates when engagement budget + // was last awarded and awards a portion of the score based on that. + // This only writes budget to the cache. + void AddEngagementBudget(const url::Origin& origin); + + bool CleanupExpiredBudget(const url::Origin& origin); + + // Gets the current Site Engagement Score for |origin|. Will return a fixed + // score of zero when |profile_| is off the record. + double GetSiteEngagementScoreForOrigin(const url::Origin& origin) const; + + raw_ptr profile_; + + // The database for storing budget information. + std::unique_ptr> db_; + + // Cached data for the origins which have been loaded. + std::map budget_map_; + + // The clock used to vend times. + std::unique_ptr clock_; + + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +#endif // CHROME_BROWSER_PUSH_MESSAGING_BUDGET_DATABASE_H_ diff --git a/chromium/chrome/browser/push_messaging/budget_database_unittest.cc b/chromium/chrome/browser/push_messaging/budget_database_unittest.cc new file mode 100644 index 00000000000..dbf29452424 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/budget_database_unittest.cc @@ -0,0 +1,351 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/push_messaging/budget_database.h" + +#include +#include + +#include "base/bind.h" +#include "base/memory/ptr_util.h" +#include "base/run_loop.h" +#include "base/test/metrics/histogram_tester.h" +#include "base/test/simple_test_clock.h" +#include "chrome/browser/push_messaging/budget.pb.h" +#include "chrome/test/base/testing_profile.h" +#include "components/leveldb_proto/public/proto_database.h" +#include "components/leveldb_proto/public/proto_database_provider.h" +#include "components/site_engagement/content/site_engagement_score.h" +#include "components/site_engagement/content/site_engagement_service.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" +#include "url/origin.h" + +namespace { + +// These values mirror the defaults in budget_database.cc +const double kDefaultExpirationInDays = 4; +const double kMaxDailyBudget = 12; + +const double kEngagement = 25; + +const char kTestOrigin[] = "https://example.com"; + +} // namespace + +class BudgetDatabaseTest : public ::testing::Test { + public: + BudgetDatabaseTest() + : success_(false), + db_(&profile_), + origin_(url::Origin::Create(GURL(kTestOrigin))) {} + + void WriteBudgetComplete(base::OnceClosure run_loop_closure, bool success) { + success_ = success; + std::move(run_loop_closure).Run(); + } + + // Spend budget for the origin. + bool SpendBudget(double amount) { + base::RunLoop run_loop; + db_.SpendBudget( + origin(), + base::BindOnce(&BudgetDatabaseTest::WriteBudgetComplete, + base::Unretained(this), run_loop.QuitClosure()), + amount); + run_loop.Run(); + return success_; + } + + void GetBudgetDetailsComplete(base::OnceClosure run_loop_closure, + std::vector predictions) { + success_ = !predictions.empty(); + prediction_.swap(predictions); + std::move(run_loop_closure).Run(); + } + + // Get the full set of budget predictions for the origin. + void GetBudgetDetails() { + base::RunLoop run_loop; + db_.GetBudgetDetails( + origin(), + base::BindOnce(&BudgetDatabaseTest::GetBudgetDetailsComplete, + base::Unretained(this), run_loop.QuitClosure())); + run_loop.Run(); + } + + Profile* profile() { return &profile_; } + BudgetDatabase* database() { return &db_; } + const url::Origin& origin() const { return origin_; } + + // Setup a test clock so that the tests can control time. + base::SimpleTestClock* SetClockForTesting() { + base::SimpleTestClock* clock = new base::SimpleTestClock(); + db_.SetClockForTesting(base::WrapUnique(clock)); + return clock; + } + + void SetSiteEngagementScore(double score) { + site_engagement::SiteEngagementService* service = + site_engagement::SiteEngagementService::Get(&profile_); + service->ResetBaseScoreForURL(GURL(kTestOrigin), score); + } + + protected: + base::HistogramTester* GetHistogramTester() { return &histogram_tester_; } + bool success_; + std::vector prediction_; + + private: + content::BrowserTaskEnvironment task_environment_; + TestingProfile profile_; + BudgetDatabase db_; + base::HistogramTester histogram_tester_; + const url::Origin origin_; +}; + +TEST_F(BudgetDatabaseTest, GetBudgetNoBudgetOrSES) { + GetBudgetDetails(); + ASSERT_TRUE(success_); + ASSERT_EQ(2U, prediction_.size()); + EXPECT_EQ(0, prediction_[0].budget_at); +} + +TEST_F(BudgetDatabaseTest, AddEngagementBudgetTest) { + base::SimpleTestClock* clock = SetClockForTesting(); + base::Time expiration_time = + clock->Now() + base::Days(kDefaultExpirationInDays); + + // Set the default site engagement. + SetSiteEngagementScore(kEngagement); + + // The budget should include kDefaultExpirationInDays days worth of + // engagement. + double daily_budget = + kMaxDailyBudget * + (kEngagement / site_engagement::SiteEngagementScore::kMaxPoints); + GetBudgetDetails(); + ASSERT_TRUE(success_); + ASSERT_EQ(2U, prediction_.size()); + ASSERT_DOUBLE_EQ(daily_budget * kDefaultExpirationInDays, + prediction_[0].budget_at); + ASSERT_EQ(0, prediction_[1].budget_at); + ASSERT_EQ(expiration_time.ToJsTime(), prediction_[1].time); + + // Advance time 1 day and add more engagement budget. + clock->Advance(base::Days(1)); + GetBudgetDetails(); + + // The budget should now have 1 full share plus 1 daily budget. + ASSERT_TRUE(success_); + ASSERT_EQ(3U, prediction_.size()); + ASSERT_DOUBLE_EQ(daily_budget * (kDefaultExpirationInDays + 1), + prediction_[0].budget_at); + ASSERT_DOUBLE_EQ(daily_budget, prediction_[1].budget_at); + ASSERT_EQ(expiration_time.ToJsTime(), prediction_[1].time); + ASSERT_DOUBLE_EQ(0, prediction_[2].budget_at); + ASSERT_EQ((expiration_time + base::Days(1)).ToJsTime(), prediction_[2].time); + + // Advance time by 59 minutes and check that no engagement budget is added + // since budget should only be added for > 1 hour increments. + clock->Advance(base::Minutes(59)); + GetBudgetDetails(); + + // The budget should be the same as before the attempted add. + ASSERT_TRUE(success_); + ASSERT_EQ(3U, prediction_.size()); + ASSERT_DOUBLE_EQ(daily_budget * (kDefaultExpirationInDays + 1), + prediction_[0].budget_at); +} + +TEST_F(BudgetDatabaseTest, SpendBudgetTest) { + base::SimpleTestClock* clock = SetClockForTesting(); + + // Set the default site engagement. + SetSiteEngagementScore(kEngagement); + + // Intialize the budget with several chunks. + GetBudgetDetails(); + clock->Advance(base::Days(1)); + GetBudgetDetails(); + clock->Advance(base::Days(1)); + GetBudgetDetails(); + + // Spend an amount of budget less than the daily budget. + ASSERT_TRUE(SpendBudget(1)); + GetBudgetDetails(); + + // There should still be three chunks of budget of size daily_budget-1, + // daily_budget, and kDefaultExpirationInDays * daily_budget. + double daily_budget = + kMaxDailyBudget * + (kEngagement / site_engagement::SiteEngagementScore::kMaxPoints); + ASSERT_EQ(4U, prediction_.size()); + ASSERT_DOUBLE_EQ((2 + kDefaultExpirationInDays) * daily_budget - 1, + prediction_[0].budget_at); + ASSERT_DOUBLE_EQ(daily_budget * 2, prediction_[1].budget_at); + ASSERT_DOUBLE_EQ(daily_budget, prediction_[2].budget_at); + ASSERT_DOUBLE_EQ(0, prediction_[3].budget_at); + + // Now spend enough that it will use up the rest of the first chunk and all of + // the second chunk, but not all of the third chunk. + ASSERT_TRUE(SpendBudget((1 + kDefaultExpirationInDays) * daily_budget)); + GetBudgetDetails(); + ASSERT_EQ(2U, prediction_.size()); + ASSERT_DOUBLE_EQ(daily_budget - 1, prediction_[0].budget_at); + + // Validate that the code returns false if SpendBudget tries to spend more + // budget than the origin has. + EXPECT_FALSE(SpendBudget(kEngagement)); + GetBudgetDetails(); + ASSERT_EQ(2U, prediction_.size()); + ASSERT_DOUBLE_EQ(daily_budget - 1, prediction_[0].budget_at); + + // Advance time until the last remaining chunk should be expired, then query + // for the full engagement worth of budget. + clock->Advance(base::Days(kDefaultExpirationInDays + 1)); + EXPECT_TRUE(SpendBudget(daily_budget * kDefaultExpirationInDays)); +} + +// There are times when a device's clock could move backwards in time, either +// due to hardware issues or user actions. Test here to make sure that even if +// time goes backwards and then forwards again, the origin isn't granted extra +// budget. +TEST_F(BudgetDatabaseTest, GetBudgetNegativeTime) { + base::SimpleTestClock* clock = SetClockForTesting(); + + // Set the default site engagement. + SetSiteEngagementScore(kEngagement); + + // Initialize the budget with two chunks. + GetBudgetDetails(); + clock->Advance(base::Days(1)); + GetBudgetDetails(); + + // Save off the budget total. + ASSERT_EQ(3U, prediction_.size()); + double budget = prediction_[0].budget_at; + + // Move the clock backwards in time to before the budget awards. + clock->SetNow(clock->Now() - base::Days(5)); + + // Make sure the budget is the same. + GetBudgetDetails(); + ASSERT_EQ(3U, prediction_.size()); + ASSERT_EQ(budget, prediction_[0].budget_at); + + // Now move the clock back to the original time and check that no extra budget + // is awarded. + clock->SetNow(clock->Now() + base::Days(5)); + GetBudgetDetails(); + ASSERT_EQ(3U, prediction_.size()); + ASSERT_EQ(budget, prediction_[0].budget_at); +} + +TEST_F(BudgetDatabaseTest, CheckBackgroundBudgetHistogram) { + base::SimpleTestClock* clock = SetClockForTesting(); + + // Set the default site engagement. + SetSiteEngagementScore(kEngagement); + + // Initialize the budget with some interesting chunks: 30 budget (full + // engagement), 15 budget (half of the engagement), 0 budget (less than an + // hour), and then after the first two expire, another 30 budget. + GetBudgetDetails(); + clock->Advance(base::Days(kDefaultExpirationInDays / 2)); + GetBudgetDetails(); + clock->Advance(base::Minutes(59)); + GetBudgetDetails(); + clock->Advance(base::Days(kDefaultExpirationInDays + 1)); + GetBudgetDetails(); + + // The BackgroundBudget UMA is recorded when budget is added to the origin. + // This can happen a maximum of once per hour so there should be two entries. + std::vector buckets = + GetHistogramTester()->GetAllSamples("PushMessaging.BackgroundBudget"); + ASSERT_EQ(2U, buckets.size()); + // First bucket is for full award, which should have 2 entries. + double full_award = kMaxDailyBudget * kEngagement / + site_engagement::SiteEngagementScore::kMaxPoints * + kDefaultExpirationInDays; + EXPECT_EQ(floor(full_award), buckets[0].min); + EXPECT_EQ(2, buckets[0].count); + // Second bucket is for 1.5 * award, which should have 1 entry. + EXPECT_EQ(floor(full_award * 1.5), buckets[1].min); + EXPECT_EQ(1, buckets[1].count); +} + +TEST_F(BudgetDatabaseTest, CheckEngagementHistograms) { + base::SimpleTestClock* clock = SetClockForTesting(); + + // Manipulate the engagement so that the budget is twice the cost of an + // action. + double cost = 2; + double engagement = 2 * cost * + site_engagement::SiteEngagementScore::kMaxPoints / + kDefaultExpirationInDays / kMaxDailyBudget; + SetSiteEngagementScore(engagement); + + // Get the budget, which will award a chunk of budget equal to engagement. + GetBudgetDetails(); + + // Now spend the budget to trigger the UMA recording the SES score. The first + // call shouldn't write any UMA. The second should write a lowSES entry, and + // the third should write a noSES entry. + ASSERT_TRUE(SpendBudget(cost)); + ASSERT_TRUE(SpendBudget(cost)); + ASSERT_FALSE(SpendBudget(cost)); + + // Advance the clock by 12 days (to guarantee a full new engagement grant) + // then change the SES score to get a different UMA entry, then spend the + // budget again. + clock->Advance(base::Days(12)); + GetBudgetDetails(); + SetSiteEngagementScore(engagement * 2); + ASSERT_TRUE(SpendBudget(cost)); + ASSERT_TRUE(SpendBudget(cost)); + ASSERT_FALSE(SpendBudget(cost)); + + // Now check the UMA. Both UMA should have 2 buckets with 1 entry each. + std::vector no_budget_buckets = + GetHistogramTester()->GetAllSamples("PushMessaging.SESForNoBudgetOrigin"); + ASSERT_EQ(2U, no_budget_buckets.size()); + EXPECT_EQ(floor(engagement), no_budget_buckets[0].min); + EXPECT_EQ(1, no_budget_buckets[0].count); + EXPECT_EQ(floor(engagement * 2), no_budget_buckets[1].min); + EXPECT_EQ(1, no_budget_buckets[1].count); + + std::vector low_budget_buckets = + GetHistogramTester()->GetAllSamples( + "PushMessaging.SESForLowBudgetOrigin"); + ASSERT_EQ(2U, low_budget_buckets.size()); + EXPECT_EQ(floor(engagement), low_budget_buckets[0].min); + EXPECT_EQ(1, low_budget_buckets[0].count); + EXPECT_EQ(floor(engagement * 2), low_budget_buckets[1].min); + EXPECT_EQ(1, low_budget_buckets[1].count); +} + +TEST_F(BudgetDatabaseTest, DefaultSiteEngagementInIncognitoProfile) { + TestingProfile second_profile; + Profile* second_profile_incognito = + second_profile.GetPrimaryOTRProfile(/*create_if_needed=*/true); + + // Create a second BudgetDatabase instance for the off-the-record version of + // a second profile. This will not have been influenced by the |profile_|. + std::unique_ptr second_database = + std::make_unique(second_profile_incognito); + + ASSERT_FALSE(profile()->IsOffTheRecord()); + ASSERT_FALSE(second_profile.IsOffTheRecord()); + ASSERT_TRUE(second_profile_incognito->IsOffTheRecord()); + + // The Site Engagement Score considered by an Incognito profile must be equal + // to the score considered in a regular profile visting a page for the first + // time. This may grant a small amount of budget, but does mean that Incognito + // mode cannot be detected through the Budget API. + EXPECT_EQ(database()->GetSiteEngagementScoreForOrigin(origin()), + second_database->GetSiteEngagementScoreForOrigin(origin())); +} diff --git a/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.cc b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.cc new file mode 100644 index 00000000000..a09e96fba38 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.cc @@ -0,0 +1,322 @@ +// 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 "chrome/browser/push_messaging/push_messaging_app_identifier.h" + +#include + +#include "base/check_op.h" +#include "base/guid.h" +#include "base/notreached.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" +#include "base/values.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/common/pref_names.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/prefs/pref_service.h" +#include "components/prefs/scoped_user_pref_update.h" + +constexpr char kPushMessagingAppIdentifierPrefix[] = "wp:"; +constexpr char kInstanceIDGuidSuffix[] = "-V2"; + +namespace { + +// sizeof is strlen + 1 since it's null-terminated. +constexpr size_t kPrefixLength = sizeof(kPushMessagingAppIdentifierPrefix) - 1; +constexpr size_t kGuidSuffixLength = sizeof(kInstanceIDGuidSuffix) - 1; + +// Ok to use '#' as separator since only the origin of the url is used. +constexpr char kPrefValueSeparator = '#'; +constexpr size_t kGuidLength = 36; // "%08X-%04X-%04X-%04X-%012llX" + +std::string FromTimeToString(base::Time time) { + DCHECK(!time.is_null()); + return base::NumberToString(time.ToDeltaSinceWindowsEpoch().InMilliseconds()); +} + +bool FromStringToTime(const std::string& time_string, + absl::optional* time) { + DCHECK(!time_string.empty()); + int64_t milliseconds; + if (base::StringToInt64(time_string, &milliseconds) && milliseconds > 0) { + *time = absl::make_optional(base::Time::FromDeltaSinceWindowsEpoch( + base::Milliseconds(milliseconds))); + return true; + } + return false; +} + +std::string MakePrefValue( + const GURL& origin, + int64_t service_worker_registration_id, + const absl::optional& expiration_time = absl::nullopt) { + std::string result = origin.spec() + kPrefValueSeparator + + base::NumberToString(service_worker_registration_id); + if (expiration_time) + result += kPrefValueSeparator + FromTimeToString(*expiration_time); + return result; +} + +bool DisassemblePrefValue(const std::string& pref_value, + GURL* origin, + int64_t* service_worker_registration_id, + absl::optional* expiration_time) { + std::vector parts = + base::SplitString(pref_value, std::string(1, kPrefValueSeparator), + base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); + + if (parts.size() < 2 || parts.size() > 3) + return false; + + if (!base::StringToInt64(parts[1], service_worker_registration_id)) + return false; + + *origin = GURL(parts[0]); + if (!origin->is_valid()) + return false; + + if (parts.size() == 3) + return FromStringToTime(parts[2], expiration_time); + + return true; +} + +} // namespace + +// static +void PushMessagingAppIdentifier::RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) { + // TODO(johnme): If push becomes enabled in incognito, be careful that this + // pref is read from the right profile, as prefs defined in a regular profile + // are visible in the corresponding incognito profile unless overridden. + // TODO(johnme): Make sure this pref doesn't get out of sync after crashes. + registry->RegisterDictionaryPref(prefs::kPushMessagingAppIdentifierMap); +} + +// static +bool PushMessagingAppIdentifier::UseInstanceID(const std::string& app_id) { + return base::EndsWith(app_id, kInstanceIDGuidSuffix, + base::CompareCase::SENSITIVE); +} + +// static +PushMessagingAppIdentifier PushMessagingAppIdentifier::Generate( + const GURL& origin, + int64_t service_worker_registration_id, + const absl::optional& expiration_time) { + // All new push subscriptions use Instance ID tokens. + return GenerateInternal(origin, service_worker_registration_id, + true /* use_instance_id */, expiration_time); +} + +// static +PushMessagingAppIdentifier PushMessagingAppIdentifier::LegacyGenerateForTesting( + const GURL& origin, + int64_t service_worker_registration_id, + const absl::optional& expiration_time) { + return GenerateInternal(origin, service_worker_registration_id, + false /* use_instance_id */, expiration_time); +} + +// static +PushMessagingAppIdentifier PushMessagingAppIdentifier::GenerateInternal( + const GURL& origin, + int64_t service_worker_registration_id, + bool use_instance_id, + const absl::optional& expiration_time) { + // Use uppercase GUID for consistency with GUIDs Push has already sent to GCM. + // Also allows detecting case mangling; see code commented "crbug.com/461867". + std::string guid = base::ToUpperASCII(base::GenerateGUID()); + if (use_instance_id) { + guid.replace(guid.size() - kGuidSuffixLength, kGuidSuffixLength, + kInstanceIDGuidSuffix); + } + CHECK(!guid.empty()); + std::string app_id = kPushMessagingAppIdentifierPrefix + origin.spec() + + kPrefValueSeparator + guid; + + PushMessagingAppIdentifier app_identifier( + app_id, origin, service_worker_registration_id, expiration_time); + app_identifier.DCheckValid(); + return app_identifier; +} + +// static +PushMessagingAppIdentifier PushMessagingAppIdentifier::FindByAppId( + Profile* profile, const std::string& app_id) { + if (!base::StartsWith(app_id, kPushMessagingAppIdentifierPrefix, + base::CompareCase::INSENSITIVE_ASCII)) { + return PushMessagingAppIdentifier(); + } + + // Since we now know this is a Push Messaging app_id, check the case hasn't + // been mangled (crbug.com/461867). + DCHECK_EQ(kPushMessagingAppIdentifierPrefix, app_id.substr(0, kPrefixLength)); + DCHECK_GE(app_id.size(), kPrefixLength + kGuidLength); + DCHECK_EQ(app_id.substr(app_id.size() - kGuidLength), + base::ToUpperASCII(app_id.substr(app_id.size() - kGuidLength))); + + const base::Value* map = + profile->GetPrefs()->GetDictionary(prefs::kPushMessagingAppIdentifierMap); + + const std::string* map_value = map->FindStringKey(app_id); + + if (!map_value || map_value->empty()) + return PushMessagingAppIdentifier(); + + GURL origin; + int64_t service_worker_registration_id; + absl::optional expiration_time; + // Try disassemble the pref value, return an invalid app identifier if the + // pref value is corrupted + if (!DisassemblePrefValue(*map_value, &origin, + &service_worker_registration_id, + &expiration_time)) { + NOTREACHED(); + return PushMessagingAppIdentifier(); + } + + PushMessagingAppIdentifier app_identifier( + app_id, origin, service_worker_registration_id, expiration_time); + app_identifier.DCheckValid(); + return app_identifier; +} + +// static +PushMessagingAppIdentifier PushMessagingAppIdentifier::FindByServiceWorker( + Profile* profile, + const GURL& origin, + int64_t service_worker_registration_id) { + const std::string base_pref_value = + MakePrefValue(origin, service_worker_registration_id); + + const base::Value* map = + profile->GetPrefs()->GetDictionary(prefs::kPushMessagingAppIdentifierMap); + for (auto entry : map->DictItems()) { + if (entry.second.is_string() && + base::StartsWith(entry.second.GetString(), base_pref_value, + base::CompareCase::SENSITIVE)) { + return FindByAppId(profile, entry.first); + } + } + return PushMessagingAppIdentifier(); +} + +// static +std::vector PushMessagingAppIdentifier::GetAll( + Profile* profile) { + std::vector result; + + const base::Value* map = + profile->GetPrefs()->GetDictionary(prefs::kPushMessagingAppIdentifierMap); + for (auto entry : map->DictItems()) { + result.push_back(FindByAppId(profile, entry.first)); + } + + return result; +} + +// static +void PushMessagingAppIdentifier::DeleteAllFromPrefs(Profile* profile) { + DictionaryPrefUpdate update(profile->GetPrefs(), + prefs::kPushMessagingAppIdentifierMap); + base::Value* map = update.Get(); + map->DictClear(); +} + +// static +size_t PushMessagingAppIdentifier::GetCount(Profile* profile) { + return profile->GetPrefs() + ->GetDictionary(prefs::kPushMessagingAppIdentifierMap) + ->DictSize(); +} + +PushMessagingAppIdentifier::PushMessagingAppIdentifier( + const PushMessagingAppIdentifier& other) = default; + +PushMessagingAppIdentifier::PushMessagingAppIdentifier() + : origin_(GURL::EmptyGURL()), service_worker_registration_id_(-1) {} + +PushMessagingAppIdentifier::PushMessagingAppIdentifier( + const std::string& app_id, + const GURL& origin, + int64_t service_worker_registration_id, + const absl::optional& expiration_time) + : app_id_(app_id), + origin_(origin), + service_worker_registration_id_(service_worker_registration_id), + expiration_time_(expiration_time) {} + +PushMessagingAppIdentifier::~PushMessagingAppIdentifier() {} + +bool PushMessagingAppIdentifier::IsExpired() const { + return (expiration_time_) ? *expiration_time_ < base::Time::Now() : false; +} + +void PushMessagingAppIdentifier::PersistToPrefs(Profile* profile) const { + DCheckValid(); + + DictionaryPrefUpdate update(profile->GetPrefs(), + prefs::kPushMessagingAppIdentifierMap); + base::Value* map = update.Get(); + + // Delete any stale entry with the same origin and Service Worker + // registration id (hence we ensure there is a 1:1 not 1:many mapping). + PushMessagingAppIdentifier old = + FindByServiceWorker(profile, origin_, service_worker_registration_id_); + if (!old.is_null()) + map->RemoveKey(old.app_id_); + + map->SetKey(app_id_, + base::Value(MakePrefValue( + origin_, service_worker_registration_id_, expiration_time_))); +} + +void PushMessagingAppIdentifier::DeleteFromPrefs(Profile* profile) const { + DCheckValid(); + + DictionaryPrefUpdate update(profile->GetPrefs(), + prefs::kPushMessagingAppIdentifierMap); + base::Value* map = update.Get(); + map->RemoveKey(app_id_); +} + +void PushMessagingAppIdentifier::DCheckValid() const { +#if DCHECK_IS_ON() + DCHECK_GE(service_worker_registration_id_, 0); + + DCHECK(origin_.is_valid()); + DCHECK_EQ(origin_.DeprecatedGetOriginAsURL(), origin_); + + // "wp:" + DCHECK_EQ(kPushMessagingAppIdentifierPrefix, + app_id_.substr(0, kPrefixLength)); + + // Optional (origin.spec() + '#') + if (app_id_.size() != kPrefixLength + kGuidLength) { + constexpr size_t suffix_length = 1 /* kPrefValueSeparator */ + kGuidLength; + DCHECK_GT(app_id_.size(), kPrefixLength + suffix_length); + DCHECK_EQ(origin_, GURL(app_id_.substr( + kPrefixLength, + app_id_.size() - kPrefixLength - suffix_length))); + DCHECK_EQ(std::string(1, kPrefValueSeparator), + app_id_.substr(app_id_.size() - suffix_length, 1)); + } + + // GUID. In order to distinguish them, an app_id created for an InstanceID + // based subscription has the last few characters of the GUID overwritten with + // kInstanceIDGuidSuffix (which contains non-hex characters invalid in GUIDs). + std::string guid = app_id_.substr(app_id_.size() - kGuidLength); + if (UseInstanceID(app_id_)) { + DCHECK(!base::IsValidGUID(guid)); + + // Replace suffix with valid hex so we can validate the rest of the string. + guid = guid.replace(guid.size() - kGuidSuffixLength, kGuidSuffixLength, + kGuidSuffixLength, 'C'); + } + DCHECK(base::IsValidGUID(guid)); +#endif // DCHECK_IS_ON() +} diff --git a/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.h b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.h new file mode 100644 index 00000000000..631976003fc --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.h @@ -0,0 +1,152 @@ +// 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 CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_APP_IDENTIFIER_H_ +#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_APP_IDENTIFIER_H_ + +#include +#include +#include +#include + +#include "base/check.h" +#include "base/gtest_prod_util.h" +#include "base/time/time.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#include "url/gurl.h" + +class Profile; + +namespace user_prefs { +class PrefRegistrySyncable; +} + +// The prefix used for all push messaging application ids. +extern const char kPushMessagingAppIdentifierPrefix[]; + +// Type used to identify a Service Worker registration from a Push API +// perspective. These can be persisted to prefs, in a 1:1 mapping between +// app_id (which includes origin) and service_worker_registration_id. +// Legacy mapped values saved by old versions of Chrome are also supported; +// these don't contain the origin in the app_id, so instead they map from +// app_id to pair. +class PushMessagingAppIdentifier { + public: + // Register profile-specific prefs. + static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry); + + // Returns whether the modern InstanceID API should be used with this app_id + // (rather than legacy GCM registration). + static bool UseInstanceID(const std::string& app_id); + + // Generates a new app identifier, with partially random app_id. + static PushMessagingAppIdentifier Generate( + const GURL& origin, + int64_t service_worker_registration_id, + const absl::optional& expiration_time = absl::nullopt); + + // Looks up an app identifier by app_id. If not found, is_null() will be true. + static PushMessagingAppIdentifier FindByAppId(Profile* profile, + const std::string& app_id); + + // Looks up an app identifier by origin & service worker registration id. + // If not found, is_null() will be true. + static PushMessagingAppIdentifier FindByServiceWorker( + Profile* profile, + const GURL& origin, + int64_t service_worker_registration_id); + + // Returns all the PushMessagingAppIdentifiers currently registered for the + // given |profile|. + static std::vector GetAll(Profile* profile); + + // Deletes all PushMessagingAppIdentifiers currently registered for the given + // |profile|. + static void DeleteAllFromPrefs(Profile* profile); + + // Returns the number of PushMessagingAppIdentifiers currently registered for + // the given |profile|. + static size_t GetCount(Profile* profile); + + ~PushMessagingAppIdentifier(); + + // Persist this app identifier to prefs. + void PersistToPrefs(Profile* profile) const; + + // Delete this app identifier from prefs. + void DeleteFromPrefs(Profile* profile) const; + + // Returns true if this identifier does not represent an app (i.e. this was + // returned by a failed Find call). + bool is_null() const { return service_worker_registration_id_ < 0; } + + // String that should be passed to push services like GCM to identify a + // particular Service Worker (so we can route incoming messages). Example: + // wp:https://foo.example.com:8443/#9CC55CCE-B8F9-4092-A364-3B0F73A3AB5F + // Legacy app_ids have no origin, e.g. wp:9CC55CCE-B8F9-4092-A364-3B0F73A3AB5F + const std::string& app_id() const { + DCHECK(!is_null()); + return app_id_; + } + + const GURL& origin() const { + DCHECK(!is_null()); + return origin_; + } + + int64_t service_worker_registration_id() const { + DCHECK(!is_null()); + return service_worker_registration_id_; + } + + void set_expiration_time(const absl::optional& expiration_time) { + expiration_time_ = expiration_time; + } + + bool IsExpired() const; + + absl::optional expiration_time() const { + DCHECK(!is_null()); + return expiration_time_; + } + + // Copy constructor + PushMessagingAppIdentifier(const PushMessagingAppIdentifier& other); + + private: + friend class PushMessagingAppIdentifierTest; + friend class PushMessagingBrowserTestBase; + FRIEND_TEST_ALL_PREFIXES(PushMessagingAppIdentifierTest, FindLegacy); + + // Generates a new app identifier for legacy GCM (not modern InstanceID). + static PushMessagingAppIdentifier LegacyGenerateForTesting( + const GURL& origin, + int64_t service_worker_registration_id, + const absl::optional& expiration_time = absl::nullopt); + + static PushMessagingAppIdentifier GenerateInternal( + const GURL& origin, + int64_t service_worker_registration_id, + bool use_instance_id, + const absl::optional& expiration_time = absl::nullopt); + + // Constructs an invalid app identifier. + PushMessagingAppIdentifier(); + // Constructs a valid app identifier. + PushMessagingAppIdentifier( + const std::string& app_id, + const GURL& origin, + int64_t service_worker_registration_id, + const absl::optional& expiration_time = absl::nullopt); + + // Validates that all the fields contain valid values. + void DCheckValid() const; + + std::string app_id_; + GURL origin_; + int64_t service_worker_registration_id_; + absl::optional expiration_time_; +}; + +#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_APP_IDENTIFIER_H_ diff --git a/chromium/chrome/browser/push_messaging/push_messaging_app_identifier_unittest.cc b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier_unittest.cc new file mode 100644 index 00000000000..5b7c649c2e4 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier_unittest.cc @@ -0,0 +1,310 @@ +// 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 "chrome/browser/push_messaging/push_messaging_app_identifier.h" + +#include + +#include "base/time/time.h" +#include "chrome/test/base/testing_profile.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +void ExpectAppIdentifiersEqual(const PushMessagingAppIdentifier& a, + const PushMessagingAppIdentifier& b) { + EXPECT_EQ(a.app_id(), b.app_id()); + EXPECT_EQ(a.origin(), b.origin()); + EXPECT_EQ(a.service_worker_registration_id(), + b.service_worker_registration_id()); + EXPECT_EQ(a.expiration_time(), b.expiration_time()); +} + +base::Time kExpirationTime = + base::Time::FromDeltaSinceWindowsEpoch(base::Seconds(1)); + +} // namespace + +class PushMessagingAppIdentifierTest : public testing::Test { + protected: + PushMessagingAppIdentifier GenerateId( + const GURL& origin, + int64_t service_worker_registration_id) { + // To bypass DCHECK in PushMessagingAppIdentifier::Generate, we just use it + // to generate app_id, and then use private constructor. + std::string app_id = PushMessagingAppIdentifier::Generate( + GURL("https://www.example.com/"), 1) + .app_id(); + return PushMessagingAppIdentifier(app_id, origin, + service_worker_registration_id); + } + + void SetUp() override { + original_ = PushMessagingAppIdentifier::Generate( + GURL("https://www.example.com/"), 1); + same_origin_and_sw_ = PushMessagingAppIdentifier::Generate( + GURL("https://www.example.com"), 1); + different_origin_ = PushMessagingAppIdentifier::Generate( + GURL("https://foobar.example.com/"), 1); + different_sw_ = PushMessagingAppIdentifier::Generate( + GURL("https://www.example.com/"), 42); + with_et_ = PushMessagingAppIdentifier::Generate( + GURL("https://www.example.com/"), 1, kExpirationTime); + different_et_ = PushMessagingAppIdentifier::Generate( + GURL("https://www.example.com/"), 1, + kExpirationTime + base::Seconds(100)); + } + + Profile* profile() { return &profile_; } + + PushMessagingAppIdentifier original_; + PushMessagingAppIdentifier same_origin_and_sw_; + PushMessagingAppIdentifier different_origin_; + PushMessagingAppIdentifier different_sw_; + PushMessagingAppIdentifier different_et_; + PushMessagingAppIdentifier with_et_; + + private: + content::BrowserTaskEnvironment task_environment_; + TestingProfile profile_; +}; + +TEST_F(PushMessagingAppIdentifierTest, ConstructorValidity) { + // The following two are valid: + EXPECT_FALSE(GenerateId(GURL("https://www.example.com/"), 1).is_null()); + EXPECT_FALSE(GenerateId(GURL("https://www.example.com"), 1).is_null()); + // The following four are invalid and will DCHECK in Generate: + EXPECT_FALSE(GenerateId(GURL(""), 1).is_null()); + EXPECT_FALSE(GenerateId(GURL("foo"), 1).is_null()); + EXPECT_FALSE(GenerateId(GURL("https://www.example.com/foo"), 1).is_null()); + EXPECT_FALSE(GenerateId(GURL("https://www.example.com/#foo"), 1).is_null()); + // The following one is invalid and will DCHECK in Generate and be null: + EXPECT_TRUE(GenerateId(GURL("https://www.example.com/"), -1).is_null()); +} + +TEST_F(PushMessagingAppIdentifierTest, UniqueGuids) { + EXPECT_NE( + PushMessagingAppIdentifier::Generate(GURL("https://www.example.com/"), 1) + .app_id(), + PushMessagingAppIdentifier::Generate(GURL("https://www.example.com/"), 1) + .app_id()); +} + +TEST_F(PushMessagingAppIdentifierTest, FindInvalidAppId) { + // These calls to FindByAppId should not DCHECK. + EXPECT_TRUE(PushMessagingAppIdentifier::FindByAppId(profile(), "").is_null()); + EXPECT_TRUE(PushMessagingAppIdentifier::FindByAppId( + profile(), "amhfneadkjmnlefnpidcijoldiibcdnd") + .is_null()); +} + +TEST_F(PushMessagingAppIdentifierTest, PersistAndFind) { + ASSERT_TRUE( + PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id()) + .is_null()); + + const auto identifier = PushMessagingAppIdentifier::FindByServiceWorker( + profile(), original_.origin(), + original_.service_worker_registration_id()); + + ASSERT_TRUE(identifier.is_null()); + + // Test basic PersistToPrefs round trips. + original_.PersistToPrefs(profile()); + { + PushMessagingAppIdentifier found_by_app_id = + PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id()); + EXPECT_FALSE(found_by_app_id.is_null()); + ExpectAppIdentifiersEqual(original_, found_by_app_id); + } + { + PushMessagingAppIdentifier found_by_origin_and_swr_id = + PushMessagingAppIdentifier::FindByServiceWorker( + profile(), original_.origin(), + original_.service_worker_registration_id()); + EXPECT_FALSE(found_by_origin_and_swr_id.is_null()); + ExpectAppIdentifiersEqual(original_, found_by_origin_and_swr_id); + } +} + +TEST_F(PushMessagingAppIdentifierTest, FindLegacy) { + const std::string legacy_app_id("wp:9CC55CCE-B8F9-4092-A364-3B0F73A3AB5F"); + ASSERT_TRUE(PushMessagingAppIdentifier::FindByAppId(profile(), legacy_app_id) + .is_null()); + + const auto identifier = PushMessagingAppIdentifier::FindByServiceWorker( + profile(), original_.origin(), + original_.service_worker_registration_id()); + + ASSERT_TRUE(identifier.is_null()); + + // Create a legacy preferences entry (the test happens to use PersistToPrefs + // since that currently works, but it's ok to change the behavior of + // PersistToPrefs; if so, this test can just do a raw DictionaryPrefUpdate). + original_.app_id_ = legacy_app_id; + original_.PersistToPrefs(profile()); + + // Test that legacy entries can be read back from prefs. + { + PushMessagingAppIdentifier found_by_app_id = + PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id()); + EXPECT_FALSE(found_by_app_id.is_null()); + ExpectAppIdentifiersEqual(original_, found_by_app_id); + } + { + PushMessagingAppIdentifier found_by_origin_and_swr_id = + PushMessagingAppIdentifier::FindByServiceWorker( + profile(), original_.origin(), + original_.service_worker_registration_id()); + EXPECT_FALSE(found_by_origin_and_swr_id.is_null()); + ExpectAppIdentifiersEqual(original_, found_by_origin_and_swr_id); + } +} + +TEST_F(PushMessagingAppIdentifierTest, PersistOverwritesSameOriginAndSW) { + original_.PersistToPrefs(profile()); + + // Test that PersistToPrefs overwrites when same origin and Service Worker. + ASSERT_NE(original_.app_id(), same_origin_and_sw_.app_id()); + ASSERT_EQ(original_.origin(), same_origin_and_sw_.origin()); + ASSERT_EQ(original_.service_worker_registration_id(), + same_origin_and_sw_.service_worker_registration_id()); + same_origin_and_sw_.PersistToPrefs(profile()); + { + PushMessagingAppIdentifier found_by_original_app_id = + PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id()); + EXPECT_TRUE(found_by_original_app_id.is_null()); + } + { + PushMessagingAppIdentifier found_by_soas_app_id = + PushMessagingAppIdentifier::FindByAppId(profile(), + same_origin_and_sw_.app_id()); + EXPECT_FALSE(found_by_soas_app_id.is_null()); + ExpectAppIdentifiersEqual(same_origin_and_sw_, found_by_soas_app_id); + } + { + PushMessagingAppIdentifier found_by_original_origin_and_swr_id = + PushMessagingAppIdentifier::FindByServiceWorker( + profile(), original_.origin(), + original_.service_worker_registration_id()); + EXPECT_FALSE(found_by_original_origin_and_swr_id.is_null()); + ExpectAppIdentifiersEqual(same_origin_and_sw_, + found_by_original_origin_and_swr_id); + } +} + +TEST_F(PushMessagingAppIdentifierTest, PersistDoesNotOverwriteDifferent) { + original_.PersistToPrefs(profile()); + + // Test that PersistToPrefs doesn't overwrite when different origin or SW. + ASSERT_NE(original_.app_id(), different_origin_.app_id()); + ASSERT_NE(original_.app_id(), different_sw_.app_id()); + different_origin_.PersistToPrefs(profile()); + different_sw_.PersistToPrefs(profile()); + { + PushMessagingAppIdentifier found_by_original_app_id = + PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id()); + EXPECT_FALSE(found_by_original_app_id.is_null()); + ExpectAppIdentifiersEqual(original_, found_by_original_app_id); + } + { + PushMessagingAppIdentifier found_by_original_origin_and_swr_id = + PushMessagingAppIdentifier::FindByServiceWorker( + profile(), original_.origin(), + original_.service_worker_registration_id()); + EXPECT_FALSE(found_by_original_origin_and_swr_id.is_null()); + ExpectAppIdentifiersEqual(original_, found_by_original_origin_and_swr_id); + } +} + +TEST_F(PushMessagingAppIdentifierTest, DeleteFromPrefs) { + original_.PersistToPrefs(profile()); + different_origin_.PersistToPrefs(profile()); + different_sw_.PersistToPrefs(profile()); + + // Test DeleteFromPrefs. Deleted app identifier should be deleted. + original_.DeleteFromPrefs(profile()); + { + PushMessagingAppIdentifier found_by_original_app_id = + PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id()); + EXPECT_TRUE(found_by_original_app_id.is_null()); + } + { + PushMessagingAppIdentifier found_by_original_origin_and_swr_id = + PushMessagingAppIdentifier::FindByServiceWorker( + profile(), original_.origin(), + original_.service_worker_registration_id()); + EXPECT_TRUE(found_by_original_origin_and_swr_id.is_null()); + } +} + +TEST_F(PushMessagingAppIdentifierTest, GetAll) { + original_.PersistToPrefs(profile()); + different_origin_.PersistToPrefs(profile()); + different_sw_.PersistToPrefs(profile()); + + original_.DeleteFromPrefs(profile()); + + // Test GetAll. Non-deleted app identifiers should all be listed. + std::vector all_app_identifiers = + PushMessagingAppIdentifier::GetAll(profile()); + EXPECT_EQ(2u, all_app_identifiers.size()); + // Order is unspecified. + bool contained_different_origin = false; + bool contained_different_sw = false; + for (const PushMessagingAppIdentifier& app_identifier : all_app_identifiers) { + if (app_identifier.app_id() == different_origin_.app_id()) { + ExpectAppIdentifiersEqual(different_origin_, app_identifier); + contained_different_origin = true; + } else { + ExpectAppIdentifiersEqual(different_sw_, app_identifier); + contained_different_sw = true; + } + } + EXPECT_TRUE(contained_different_origin); + EXPECT_TRUE(contained_different_sw); +} + +TEST_F(PushMessagingAppIdentifierTest, PersistWithExpirationTime) { + ASSERT_TRUE(with_et_.expiration_time()); + ASSERT_TRUE(different_et_.expiration_time()); + ASSERT_EQ(with_et_.origin(), different_et_.origin()); + ASSERT_EQ(with_et_.service_worker_registration_id(), + different_et_.service_worker_registration_id()); + ASSERT_FALSE(kExpirationTime.is_null()); + + different_et_.PersistToPrefs(profile()); + + // Test PersistToPrefs and FindByAppId, whether expiration time is saved + // properly + std::vector all_app_identifiers = + PushMessagingAppIdentifier::GetAll(profile()); + EXPECT_EQ(1u, all_app_identifiers.size()); + { + PushMessagingAppIdentifier found_by_app_id = + PushMessagingAppIdentifier::FindByAppId(profile(), + different_et_.app_id()); + // Check whether expiration time was saved + ExpectAppIdentifiersEqual(found_by_app_id, different_et_); + } + with_et_.PersistToPrefs(profile()); + { + all_app_identifiers = PushMessagingAppIdentifier::GetAll(profile()); + EXPECT_EQ(1u, all_app_identifiers.size()); + } + { + PushMessagingAppIdentifier found_by_with_et_app_id = + PushMessagingAppIdentifier::FindByAppId(profile(), with_et_.app_id()); + EXPECT_FALSE(found_by_with_et_app_id.is_null()); + EXPECT_EQ(found_by_with_et_app_id.expiration_time(), kExpirationTime); + ExpectAppIdentifiersEqual(found_by_with_et_app_id, with_et_); + } + { + PushMessagingAppIdentifier found_by_different_et_app_id = + PushMessagingAppIdentifier::FindByAppId(profile(), + different_et_.app_id()); + EXPECT_TRUE(found_by_different_et_app_id.is_null()); + } +} diff --git a/chromium/chrome/browser/push_messaging/push_messaging_browsertest.cc b/chromium/chrome/browser/push_messaging/push_messaging_browsertest.cc new file mode 100644 index 00000000000..b94212aad9f --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_browsertest.cc @@ -0,0 +1,3227 @@ +// 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 +#include + +#include +#include +#include + +#include "base/barrier_closure.h" +#include "base/base64url.h" +#include "base/bind.h" +#include "base/command_line.h" +#include "base/memory/raw_ptr.h" +#include "base/run_loop.h" +#include "base/strings/utf_string_conversions.h" +#include "base/test/bind.h" +#include "base/test/metrics/histogram_tester.h" +#include "base/test/scoped_feature_list.h" +#include "build/build_config.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/browsing_data/chrome_browsing_data_remover_constants.h" +#include "chrome/browser/chrome_notification_types.h" +#include "chrome/browser/content_settings/host_content_settings_map_factory.h" +#include "chrome/browser/gcm/gcm_profile_service_factory.h" +#include "chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h" +#include "chrome/browser/notifications/notification_display_service_tester.h" +#include "chrome/browser/notifications/notification_handler.h" +#include "chrome/browser/permissions/crowd_deny_fake_safe_browsing_database_manager.h" +#include "chrome/browser/permissions/crowd_deny_preload_data.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/push_messaging/push_messaging_app_identifier.h" +#include "chrome/browser/push_messaging/push_messaging_constants.h" +#include "chrome/browser/push_messaging/push_messaging_features.h" +#include "chrome/browser/push_messaging/push_messaging_service_factory.h" +#include "chrome/browser/push_messaging/push_messaging_service_impl.h" +#include "chrome/browser/safe_browsing/test_safe_browsing_service.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "chrome/common/buildflags.h" +#include "chrome/common/chrome_features.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/ui_test_utils.h" +#include "components/browsing_data/content/browsing_data_helper.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/content_settings/core/common/content_settings.h" +#include "components/content_settings/core/common/content_settings_types.h" +#include "components/gcm_driver/common/gcm_message.h" +#include "components/gcm_driver/fake_gcm_profile_service.h" +#include "components/gcm_driver/gcm_client.h" +#include "components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.h" +#include "components/gcm_driver/instance_id/instance_id_driver.h" +#include "components/gcm_driver/instance_id/instance_id_profile_service.h" +#include "components/keep_alive_registry/keep_alive_registry.h" +#include "components/keep_alive_registry/keep_alive_types.h" +#include "components/network_session_configurator/common/network_switches.h" +#include "components/permissions/permission_request_manager.h" +#include "components/site_engagement/content/site_engagement_score.h" +#include "components/site_engagement/content/site_engagement_service.h" +#include "content/public/browser/browsing_data_remover.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/web_contents.h" +#include "content/public/common/content_features.h" +#include "content/public/common/content_switches.h" +#include "content/public/test/browser_test.h" +#include "content/public/test/browser_test_utils.h" +#include "content/public/test/browsing_data_remover_test_util.h" +#include "content/public/test/prerender_test_util.h" +#include "content/public/test/test_utils.h" +#include "net/dns/mock_host_resolver.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "third_party/blink/public/mojom/push_messaging/push_messaging.mojom.h" +#include "third_party/blink/public/mojom/push_messaging/push_messaging_status.mojom.h" +#include "ui/base/window_open_disposition.h" +#include "ui/message_center/public/cpp/notification.h" + +#if BUILDFLAG(ENABLE_BACKGROUND_MODE) +#include "chrome/browser/background/background_mode_manager.h" +#endif + +namespace { + +const char kManifestSenderId[] = "1234567890"; +const int32_t kApplicationServerKeyLength = 65; + +enum class PushSubscriptionKeyFormat { kOmitKey, kBinary, kBase64UrlEncoded }; + +// NIST P-256 public key made available to tests. Must be an uncompressed +// point in accordance with SEC1 2.3.3. +const uint8_t kApplicationServerKey[kApplicationServerKeyLength] = { + 0x04, 0x55, 0x52, 0x6A, 0xA5, 0x6E, 0x8E, 0xAA, 0x47, 0x97, 0x36, + 0x10, 0xC1, 0x66, 0x3C, 0x1E, 0x65, 0xBF, 0xA1, 0x7B, 0xEE, 0x48, + 0xC9, 0xC6, 0xBB, 0xBF, 0x02, 0x18, 0x53, 0x72, 0x1D, 0x0C, 0x7B, + 0xA9, 0xE3, 0x11, 0xB7, 0x03, 0x52, 0x21, 0xD3, 0x71, 0x90, 0x13, + 0xA8, 0xC1, 0xCF, 0xED, 0x20, 0xF7, 0x1F, 0xD1, 0x7F, 0xF2, 0x76, + 0xB6, 0x01, 0x20, 0xD8, 0x35, 0xA5, 0xD9, 0x3C, 0x43, 0xFD}; + +// URL-safe base64 encoded version of the |kApplicationServerKey|. +const char kEncodedApplicationServerKey[] = + "BFVSaqVujqpHlzYQwWY8HmW_oXvuSMnGu78CGFNyHQx7qeMRtwNSIdNxkBOowc_tIPcf0X_ydr" + "YBINg1pdk8Q_0"; + +// From chrome/browser/push_messaging/push_messaging_manager.cc +const char* kIncognitoWarningPattern = + "Chrome currently does not support the Push API in incognito mode " + "(https://crbug.com/401439). There is deliberately no way to " + "feature-detect this, since incognito mode needs to be undetectable by " + "websites."; + +std::string GetTestApplicationServerKey(bool base64_url_encoded = false) { + std::string application_server_key; + + if (base64_url_encoded) { + base::Base64UrlEncode(reinterpret_cast(kApplicationServerKey), + base::Base64UrlEncodePolicy::OMIT_PADDING, + &application_server_key); + } else { + application_server_key = + std::string(kApplicationServerKey, + kApplicationServerKey + base::size(kApplicationServerKey)); + } + + return application_server_key; +} + +void LegacyRegisterCallback(base::OnceClosure done_callback, + std::string* out_registration_id, + gcm::GCMClient::Result* out_result, + const std::string& registration_id, + gcm::GCMClient::Result result) { + if (out_registration_id) + *out_registration_id = registration_id; + if (out_result) + *out_result = result; + std::move(done_callback).Run(); +} + +void DidRegister(base::OnceClosure done_callback, + const std::string& registration_id, + const GURL& endpoint, + const absl::optional& expiration_time, + const std::vector& p256dh, + const std::vector& auth, + blink::mojom::PushRegistrationStatus status) { + EXPECT_EQ(blink::mojom::PushRegistrationStatus::SUCCESS_FROM_PUSH_SERVICE, + status); + std::move(done_callback).Run(); +} + +void InstanceIDResultCallback(base::OnceClosure done_callback, + instance_id::InstanceID::Result* out_result, + instance_id::InstanceID::Result result) { + DCHECK(out_result); + *out_result = result; + std::move(done_callback).Run(); +} + +} // namespace + +class PushMessagingBrowserTestBase : public InProcessBrowserTest { + public: + PushMessagingBrowserTestBase() + : scoped_testing_factory_installer_( + base::BindRepeating(&gcm::FakeGCMProfileService::Build)), + gcm_service_(nullptr), + gcm_driver_(nullptr) {} + + ~PushMessagingBrowserTestBase() override = default; + + PushMessagingBrowserTestBase(const PushMessagingBrowserTestBase&) = delete; + PushMessagingBrowserTestBase& operator=(const PushMessagingBrowserTestBase&) = + delete; + + // InProcessBrowserTest: + void SetUp() override { + https_server_ = std::make_unique( + net::EmbeddedTestServer::TYPE_HTTPS); + https_server_->ServeFilesFromSourceDirectory(GetChromeTestDataDir()); + content::SetupCrossSiteRedirector(https_server_.get()); + + site_engagement::SiteEngagementScore::SetParamValuesForTesting(); + InProcessBrowserTest::SetUp(); + } + void SetUpCommandLine(base::CommandLine* command_line) override { + // Enable experimental features for subscription restrictions. + command_line->AppendSwitch( + switches::kEnableExperimentalWebPlatformFeatures); + + // HTTPS server only serves a valid cert for localhost, so this is needed to + // load webby domains like "embedded.com" without an interstitial. + command_line->AppendSwitch(switches::kIgnoreCertificateErrors); + } + + // InProcessBrowserTest: + void SetUpOnMainThread() override { + host_resolver()->AddRule("*", "127.0.0.1"); + ASSERT_TRUE(https_server_->Start()); + + KeyedService* keyed_service = + gcm::GCMProfileServiceFactory::GetForProfile(GetBrowser()->profile()); + if (keyed_service) { + gcm_service_ = static_cast(keyed_service); + gcm_driver_ = static_cast( + gcm_service_->driver()); + } + + notification_tester_ = std::make_unique( + GetBrowser()->profile()); + + push_service_ = + PushMessagingServiceFactory::GetForProfile(GetBrowser()->profile()); + + LoadTestPage(); + } + + void TearDownOnMainThread() override { + notification_tester_.reset(); + InProcessBrowserTest::TearDownOnMainThread(); + } + + // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro. + void RestartPushService() { + Profile* profile = GetBrowser()->profile(); + PushMessagingServiceFactory::GetInstance()->SetTestingFactory( + profile, BrowserContextKeyedServiceFactory::TestingFactory()); + ASSERT_EQ(nullptr, PushMessagingServiceFactory::GetForProfile(profile)); + PushMessagingServiceFactory::GetInstance()->RestoreFactoryForTests(profile); + PushMessagingServiceImpl::InitializeForProfile(profile); + push_service_ = PushMessagingServiceFactory::GetForProfile(profile); + } + + // Helper function to test if a Keep Alive is registered while avoiding the + // platform checks. Returns a boolean so that assertion failures are reported + // at the right line. + // Returns true when KeepAlives are not supported by the platform, or when + // the registration state is equal to the expectation. + bool IsRegisteredKeepAliveEqualTo(bool expectation) { +#if BUILDFLAG(ENABLE_BACKGROUND_MODE) + return expectation == + KeepAliveRegistry::GetInstance()->IsOriginRegistered( + KeepAliveOrigin::IN_FLIGHT_PUSH_MESSAGE); +#else + return true; +#endif + } + + void LoadTestPage(const std::string& path) { + ASSERT_TRUE(ui_test_utils::NavigateToURL(GetBrowser(), + https_server_->GetURL(path))); + } + + void LoadTestPage() { LoadTestPage(GetTestURL()); } + + void LoadTestPageWithoutManifest() { LoadTestPage(GetNoManifestTestURL()); } + + bool RunScript(const std::string& script, std::string* result) { + return RunScript(script, result, nullptr); + } + + bool RunScript(const std::string& script, std::string* result, + content::WebContents* web_contents) { + if (!web_contents) + web_contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); + return content::ExecuteScriptAndExtractString(web_contents->GetMainFrame(), + script, result); + } + + gcm::GCMAppHandler* GetAppHandler() { + return gcm_driver_->GetAppHandler(kPushMessagingAppIdentifierPrefix); + } + + permissions::PermissionRequestManager* GetPermissionRequestManager() { + return permissions::PermissionRequestManager::FromWebContents( + GetBrowser()->tab_strip_model()->GetActiveWebContents()); + } + + // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro. + void RequestAndAcceptPermission(); + // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro. + void RequestAndDenyPermission(); + + // Sets out_token to the subscription token (not including server URL). + // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro. + void SubscribeSuccessfully( + PushSubscriptionKeyFormat key_format = PushSubscriptionKeyFormat::kBinary, + std::string* out_token = nullptr); + + // Sets up the state corresponding to a dangling push subscription whose + // service worker registration no longer exists. Some users may be left with + // such orphaned subscriptions due to service worker unregistrations not + // clearing push subscriptions in the past. This allows us to emulate that. + // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro. + void SetupOrphanedPushSubscription(std::string* out_app_id); + + // Legacy subscribe path using GCMDriver rather than Instance IDs. Only + // for testing that we maintain support for existing stored registrations. + // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro. + void LegacySubscribeSuccessfully(std::string* out_subscription_id = nullptr); + + // Strips server URL from a registration endpoint to get subscription token. + // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro. + void EndpointToToken(const std::string& endpoint, + bool standard_protocol = true, + std::string* out_token = nullptr); + + blink::mojom::PushSubscriptionPtr GetSubscriptionForAppIdentifier( + const PushMessagingAppIdentifier& app_identifier) { + blink::mojom::PushSubscriptionPtr result; + base::RunLoop run_loop; + push_service_->GetPushSubscriptionFromAppIdentifier( + app_identifier, + base::BindLambdaForTesting( + [&](blink::mojom::PushSubscriptionPtr subscription) { + result = std::move(subscription); + run_loop.Quit(); + })); + run_loop.Run(); + return result; + } + + // Deletes an Instance ID from the GCM Store but keeps the push subscription + // stored in the PushMessagingAppIdentifier map and Service Worker DB. + // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro. + void DeleteInstanceIDAsIfGCMStoreReset(const std::string& app_id); + + PushMessagingAppIdentifier GetAppIdentifierForServiceWorkerRegistration( + int64_t service_worker_registration_id); + + void SendMessageAndWaitUntilHandled( + const PushMessagingAppIdentifier& app_identifier, + const gcm::IncomingMessage& message); + + net::EmbeddedTestServer* https_server() const { return https_server_.get(); } + + // Returns a vector of the currently displayed Notification objects. + std::vector GetDisplayedNotifications() { + return notification_tester_->GetDisplayedNotificationsForType( + NotificationHandler::Type::WEB_PERSISTENT); + } + + // Returns the number of notifications that are currently being shown. + size_t GetNotificationCount() { return GetDisplayedNotifications().size(); } + + // Removes all shown notifications. + void RemoveAllNotifications() { + notification_tester_->RemoveAllNotifications( + NotificationHandler::Type::WEB_PERSISTENT, true /* by_user */); + } + + // To be called when delivery of a push message has finished. The |run_loop| + // will be told to quit after |messages_required| messages were received. + void OnDeliveryFinished(std::vector* number_of_notifications_shown, + base::OnceClosure done_closure) { + DCHECK(number_of_notifications_shown); + number_of_notifications_shown->push_back(GetNotificationCount()); + + std::move(done_closure).Run(); + } + + PushMessagingServiceImpl* push_service() const { return push_service_; } + + void SetSiteEngagementScore(const GURL& url, double score) { + site_engagement::SiteEngagementService* service = + site_engagement::SiteEngagementService::Get(GetBrowser()->profile()); + service->ResetBaseScoreForURL(url, score); + EXPECT_EQ(score, service->GetScore(url)); + } + + // Matches |tag| against the notification's ID to see if the notification's + // js-provided tag could have been |tag|. This is not perfect as it might + // return true for a |tag| that is a substring of the original tag. + static bool TagEquals(const message_center::Notification& notification, + const std::string& tag) { + return std::string::npos != notification.id().find(tag); + } + + protected: + virtual std::string GetTestURL() { return "/push_messaging/test.html"; } + + virtual std::string GetNoManifestTestURL() { + return "/push_messaging/test_no_manifest.html"; + } + + virtual Browser* GetBrowser() const { return browser(); } + + gcm::GCMProfileServiceFactory::ScopedTestingFactoryInstaller + scoped_testing_factory_installer_; + + raw_ptr gcm_service_; + raw_ptr gcm_driver_; + base::HistogramTester histogram_tester_; + + std::unique_ptr notification_tester_; + + private: + std::unique_ptr https_server_; + raw_ptr push_service_; +}; + +void PushMessagingBrowserTestBase::RequestAndAcceptPermission() { + std::string script_result; + GetPermissionRequestManager()->set_auto_response_for_test( + permissions::PermissionRequestManager::ACCEPT_ALL); + ASSERT_TRUE(RunScript("requestNotificationPermission();", &script_result)); + ASSERT_EQ("permission status - granted", script_result); +} + +void PushMessagingBrowserTestBase::RequestAndDenyPermission() { + std::string script_result; + GetPermissionRequestManager()->set_auto_response_for_test( + permissions::PermissionRequestManager::DENY_ALL); + ASSERT_TRUE(RunScript("requestNotificationPermission();", &script_result)); + ASSERT_EQ("permission status - denied", script_result); +} + +void PushMessagingBrowserTestBase::SubscribeSuccessfully( + PushSubscriptionKeyFormat key_format, + std::string* out_token) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + switch (key_format) { + case PushSubscriptionKeyFormat::kBinary: + ASSERT_TRUE(RunScript("removeManifest()", &script_result)); + ASSERT_EQ("manifest removed", script_result); + + ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result)); + ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result, true, out_token)); + break; + case PushSubscriptionKeyFormat::kBase64UrlEncoded: + ASSERT_TRUE(RunScript("removeManifest()", &script_result)); + ASSERT_EQ("manifest removed", script_result); + + ASSERT_TRUE(RunScript("documentSubscribePushWithBase64URLEncodedString()", + &script_result)); + ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result, true, out_token)); + break; + case PushSubscriptionKeyFormat::kOmitKey: + // Test backwards compatibility with old ID based subscriptions. + ASSERT_TRUE( + RunScript("documentSubscribePushWithoutKey()", &script_result)); + ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result, false, out_token)); + break; + default: + NOTREACHED(); + } +} + +void PushMessagingBrowserTestBase::SetupOrphanedPushSubscription( + std::string* out_app_id) { + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + GURL requesting_origin = + https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + // Use 1234LL as it's unlikely to collide with an active service worker + // registration id (they increment from 0). + const int64_t service_worker_registration_id = 1234LL; + + auto options = blink::mojom::PushSubscriptionOptions::New(); + options->user_visible_only = true; + + std::string test_application_server_key = GetTestApplicationServerKey(); + options->application_server_key = std::vector( + test_application_server_key.begin(), test_application_server_key.end()); + + base::RunLoop run_loop; + push_service()->SubscribeFromWorker( + requesting_origin, service_worker_registration_id, std::move(options), + base::BindOnce(&DidRegister, run_loop.QuitClosure())); + run_loop.Run(); + + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker( + GetBrowser()->profile(), requesting_origin, + service_worker_registration_id); + ASSERT_FALSE(app_identifier.is_null()); + *out_app_id = app_identifier.app_id(); +} + +void PushMessagingBrowserTestBase::LegacySubscribeSuccessfully( + std::string* out_subscription_id) { + // Create a non-InstanceID GCM registration. Have to directly access + // GCMDriver, since this codepath has been deleted from Push. + + std::string script_result; + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + GURL requesting_origin = + https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + int64_t service_worker_registration_id = 0LL; + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::LegacyGenerateForTesting( + requesting_origin, service_worker_registration_id); + push_service_->IncreasePushSubscriptionCount(1, true /* is_pending */); + + std::string subscription_id; + { + base::RunLoop run_loop; + gcm::GCMClient::Result register_result = gcm::GCMClient::UNKNOWN_ERROR; + gcm_driver_->Register( + app_identifier.app_id(), {kManifestSenderId}, + base::BindOnce(&LegacyRegisterCallback, run_loop.QuitClosure(), + &subscription_id, ®ister_result)); + run_loop.Run(); + ASSERT_EQ(gcm::GCMClient::SUCCESS, register_result); + } + + app_identifier.PersistToPrefs(GetBrowser()->profile()); + push_service_->IncreasePushSubscriptionCount(1, false /* is_pending */); + push_service_->DecreasePushSubscriptionCount(1, true /* was_pending */); + + { + base::RunLoop run_loop; + push_service_->StorePushSubscriptionForTesting( + GetBrowser()->profile(), requesting_origin, + service_worker_registration_id, subscription_id, kManifestSenderId, + run_loop.QuitClosure()); + run_loop.Run(); + } + + if (out_subscription_id) + *out_subscription_id = subscription_id; +} + +void PushMessagingBrowserTestBase::EndpointToToken(const std::string& endpoint, + bool standard_protocol, + std::string* out_token) { + size_t last_slash = endpoint.rfind('/'); + + ASSERT_EQ(kPushMessagingGcmEndpoint, endpoint.substr(0, last_slash + 1)); + + ASSERT_LT(last_slash + 1, endpoint.length()); // Token must not be empty. + + if (out_token) + *out_token = endpoint.substr(last_slash + 1); +} + +PushMessagingAppIdentifier +PushMessagingBrowserTestBase::GetAppIdentifierForServiceWorkerRegistration( + int64_t service_worker_registration_id) { + GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker( + GetBrowser()->profile(), origin, service_worker_registration_id); + EXPECT_FALSE(app_identifier.is_null()); + return app_identifier; +} + +void PushMessagingBrowserTestBase::DeleteInstanceIDAsIfGCMStoreReset( + const std::string& app_id) { + // Delete the Instance ID directly, keeping the push subscription stored in + // the PushMessagingAppIdentifier map and the Service Worker database. This + // simulates the GCM Store getting reset but failing to clear push + // subscriptions, either because the store got reset before + // 93ec793ac69a542b2213297737178a55d069fd0d (Chrome 56), or because a race + // condition (e.g. shutdown) prevents PushMessagingServiceImpl::OnStoreReset + // from clearing all subscriptions. + instance_id::InstanceIDProfileService* instance_id_profile_service = + instance_id::InstanceIDProfileServiceFactory::GetForProfile( + GetBrowser()->profile()); + DCHECK(instance_id_profile_service); + instance_id::InstanceIDDriver* instance_id_driver = + instance_id_profile_service->driver(); + DCHECK(instance_id_driver); + instance_id::InstanceID::Result delete_result = + instance_id::InstanceID::UNKNOWN_ERROR; + base::RunLoop run_loop; + instance_id_driver->GetInstanceID(app_id)->DeleteID(base::BindOnce( + &InstanceIDResultCallback, run_loop.QuitClosure(), &delete_result)); + run_loop.Run(); + ASSERT_EQ(instance_id::InstanceID::SUCCESS, delete_result); +} + +void PushMessagingBrowserTestBase::SendMessageAndWaitUntilHandled( + const PushMessagingAppIdentifier& app_identifier, + const gcm::IncomingMessage& message) { + base::RunLoop run_loop; + push_service()->SetMessageCallbackForTesting(run_loop.QuitClosure()); + push_service()->OnMessage(app_identifier.app_id(), message); + run_loop.Run(); +} + +class PushMessagingBrowserTest : public PushMessagingBrowserTestBase { + public: + PushMessagingBrowserTest() { + feature_list_.InitAndDisableFeature( + features::kPushMessagingDisallowSenderIDs); + } + + private: + base::test::ScopedFeatureList feature_list_; +}; + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + SubscribeWithoutKeySuccessNotificationsGranted) { + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kOmitKey)); + EXPECT_EQ(kManifestSenderId, gcm_driver_->last_gettoken_authorized_entity()); + EXPECT_EQ(GetAppIdentifierForServiceWorkerRegistration(0LL).app_id(), + gcm_driver_->last_gettoken_app_id()); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + SubscribeSuccessNotificationsGranted) { + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + EXPECT_EQ(kEncodedApplicationServerKey, + gcm_driver_->last_gettoken_authorized_entity()); + EXPECT_EQ(GetAppIdentifierForServiceWorkerRegistration(0LL).app_id(), + gcm_driver_->last_gettoken_app_id()); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + SubscribeSuccessNotificationsGrantedWithBase64URLKey) { + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBase64UrlEncoded)); + EXPECT_EQ(kEncodedApplicationServerKey, + gcm_driver_->last_gettoken_authorized_entity()); + EXPECT_EQ(GetAppIdentifierForServiceWorkerRegistration(0LL).app_id(), + gcm_driver_->last_gettoken_app_id()); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + SubscribeSuccessNotificationsPrompt) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + GetPermissionRequestManager()->set_auto_response_for_test( + permissions::PermissionRequestManager::ACCEPT_ALL); + ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result)); + // Both of these methods EXPECT that they succeed. + ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result)); + GetAppIdentifierForServiceWorkerRegistration(0LL); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + SubscribeFailureNotificationsBlocked) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndDenyPermission()); + + ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result)); + EXPECT_EQ("NotAllowedError - Registration failed - permission denied", + script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, SubscribeFailureNoManifest) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + ASSERT_TRUE(RunScript("removeManifest()", &script_result)); + ASSERT_EQ("manifest removed", script_result); + + ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, and " + "manifest empty or missing", + script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, SubscribeFailureNoSenderId) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + ASSERT_TRUE(RunScript("swapManifestNoSenderId()", &script_result)); + ASSERT_EQ("sender id removed from manifest", script_result); + + ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, and " + "gcm_sender_id not found in manifest", + script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + RegisterFailureEmptyPushSubscriptionOptions) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + ASSERT_TRUE( + RunScript("documentSubscribePushWithEmptyOptions()", &script_result)); + EXPECT_EQ("NotAllowedError - Registration failed - permission denied", + script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, SubscribeWithInvalidation) { + std::string token1, token2, token3; + + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token1)); + ASSERT_FALSE(token1.empty()); + + // Repeated calls to |subscribe()| should yield the same token. + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token2)); + ASSERT_EQ(token1, token2); + + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker( + GetBrowser()->profile(), + https_server()->GetURL("/").DeprecatedGetOriginAsURL(), + 0LL /* service_worker_registration_id */); + + ASSERT_FALSE(app_identifier.is_null()); + EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_gettoken_app_id()); + + // Delete the InstanceID. This captures two scenarios: either the database was + // corrupted, or the subscription was invalidated by the server. + ASSERT_NO_FATAL_FAILURE( + DeleteInstanceIDAsIfGCMStoreReset(app_identifier.app_id())); + + EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_deletetoken_app_id()); + + // Repeated calls to |subscribe()| will now (silently) result in a new token. + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token3)); + ASSERT_FALSE(token3.empty()); + EXPECT_NE(token1, token3); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, SubscribeWorker) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + LoadTestPage(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Try to subscribe from a worker without a key. This should fail. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, and " + "gcm_sender_id not found in manifest", + script_result); + + // Now run the subscribe with a key. This should succeed. + ASSERT_TRUE(RunScript("workerSubscribePush()", &script_result)); + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, true /* standard_protocol */)); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + SubscribeWorkerWithBase64URLEncodedString) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + LoadTestPage(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Try to subscribe from a worker without a key. This should fail. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, and " + "gcm_sender_id not found in manifest", + script_result); + + // Now run the subscribe with a key. This should succeed. + ASSERT_TRUE(RunScript("workerSubscribePushWithBase64URLEncodedString()", + &script_result)); + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, true /* standard_protocol */)); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + ResubscribeWithoutKeyAfterSubscribingWithKeyInManifest) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + LoadTestPage(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Run the subscription from the document without a key, this will trigger + // the code to read sender id from the manifest and will write it to the + // datastore. + ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result)); + std::string token1; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token1)); + + ASSERT_TRUE(RunScript("removeManifest()", &script_result)); + ASSERT_EQ("manifest removed", script_result); + + // Try to resubscribe from the document without a key or manifest. + // This should fail. + ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, " + "and manifest empty or missing", + script_result); + + // Now run the subscribe from the service worker without a key. + // In this case, the sender id should be read from the datastore. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + std::string token2; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token2)); + EXPECT_EQ(token1, token2); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + + // After unsubscribing, subscribe again from the worker with no key. + // The sender id should again be read from the datastore, so the + // subcribe should succeed, and we should get a new subscription token. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + std::string token3; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token3)); + EXPECT_NE(token1, token3); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); +} + +IN_PROC_BROWSER_TEST_F( + PushMessagingBrowserTest, + ResubscribeWithoutKeyAfterSubscribingFromDocumentWithP256Key) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + LoadTestPageWithoutManifest(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Run the subscription from the document with a key. + ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result)); + ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result)); + + // Try to resubscribe from the document without a key - should fail. + ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, " + "and manifest empty or missing", + script_result); + + // Now try to resubscribe from the service worker without a key. + // This should also fail as the original key was not numeric. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, " + "and gcm_sender_id not found in manifest", + script_result); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + + // After unsubscribing, try to resubscribe again without a key. + // This should again fail. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, " + "and gcm_sender_id not found in manifest", + script_result); +} + +IN_PROC_BROWSER_TEST_F( + PushMessagingBrowserTest, + ResubscribeWithoutKeyAfterSubscribingFromWorkerWithP256Key) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + LoadTestPageWithoutManifest(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Run the subscribe from the service worker with a key. + // This should succeed. + ASSERT_TRUE(RunScript("workerSubscribePush()", &script_result)); + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, true /* standard_protocol */)); + + // Try to resubscribe from the document without a key - should fail. + ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, " + "and manifest empty or missing", + script_result); + + // Now try to resubscribe from the service worker without a key. + // This should also fail as the original key was not numeric. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, and " + "gcm_sender_id not found in manifest", + script_result); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + + // After unsubscribing, try to resubscribe again without a key. + // This should again fail. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, " + "and gcm_sender_id not found in manifest", + script_result); +} + +IN_PROC_BROWSER_TEST_F( + PushMessagingBrowserTest, + ResubscribeWithoutKeyAfterSubscribingFromDocumentWithNumber) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + LoadTestPageWithoutManifest(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Run the subscribe from the document with a numeric key. + // This should succeed. + ASSERT_TRUE( + RunScript("documentSubscribePushWithNumericKey()", &script_result)); + std::string token1; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token1)); + + // Try to resubscribe from the document without a key - should fail. + ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, " + "and manifest empty or missing", + script_result); + + // Now run the subscribe from the service worker without a key. + // In this case, the sender id should be read from the datastore. + // Note, we would rather this failed as we only really want to support + // no-key subscribes after subscribing with a numeric gcm sender id in the + // manifest, not a numeric applicationServerKey, but for code simplicity + // this case is allowed. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + std::string token2; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token2)); + EXPECT_EQ(token1, token2); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + + // After unsubscribing, subscribe again from the worker with no key. + // The sender id should again be read from the datastore, so the + // subcribe should succeed, and we should get a new subscription token. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + std::string token3; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token3)); + EXPECT_NE(token1, token3); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); +} + +IN_PROC_BROWSER_TEST_F( + PushMessagingBrowserTest, + ResubscribeWithoutKeyAfterSubscribingFromWorkerWithNumber) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + LoadTestPageWithoutManifest(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Run the subscribe from the service worker with a numeric key. + // This should succeed. + ASSERT_TRUE(RunScript("workerSubscribePushWithNumericKey()", &script_result)); + std::string token1; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token1)); + + // Try to resubscribe from the document without a key - should fail. + ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, " + "and manifest empty or missing", + script_result); + + // Now run the subscribe from the service worker without a key. + // In this case, the sender id should be read from the datastore. + // Note, we would rather this failed as we only really want to support + // no-key subscribes after subscribing with a numeric gcm sender id in the + // manifest, not a numeric applicationServerKey, but for code simplicity + // this case is allowed. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + std::string token2; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token2)); + EXPECT_EQ(token1, token2); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + + // After unsubscribing, subscribe again from the worker with no key. + // The sender id should again be read from the datastore, so the + // subcribe should succeed, and we should get a new subscription token. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + std::string token3; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token3)); + EXPECT_NE(token1, token3); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, ResubscribeWithMismatchedKey) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + LoadTestPage(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Run the subscribe from the service worker with a key. + // This should succeed. + ASSERT_TRUE( + RunScript("workerSubscribePushWithNumericKey('11111')", &script_result)); + std::string token1; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token1)); + + // Try to resubscribe with a different key - should fail. + ASSERT_TRUE( + RunScript("workerSubscribePushWithNumericKey('22222')", &script_result)); + EXPECT_EQ( + "InvalidStateError - Registration failed - A subscription with a " + "different applicationServerKey (or gcm_sender_id) already exists; to " + "change the applicationServerKey, unsubscribe then resubscribe.", + script_result); + + // Try to resubscribe with the original key - should succeed. + ASSERT_TRUE( + RunScript("workerSubscribePushWithNumericKey('11111')", &script_result)); + std::string token2; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token2)); + EXPECT_EQ(token1, token2); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + + // Resubscribe with a different key after unsubscribing. + // Should succeed, and we should get a new subscription token. + ASSERT_TRUE( + RunScript("workerSubscribePushWithNumericKey('22222')", &script_result)); + std::string token3; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token3)); + EXPECT_NE(token1, token3); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, SubscribePersisted) { + std::string script_result; + + // First, test that Service Worker registration IDs are assigned in order of + // registering the Service Workers, and the (fake) push subscription ids are + // assigned in order of push subscription (even when these orders are + // different). + + std::string token1; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token1)); + PushMessagingAppIdentifier sw0_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + EXPECT_EQ(sw0_identifier.app_id(), gcm_driver_->last_gettoken_app_id()); + + LoadTestPage("/push_messaging/subscope1/test.html"); + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + LoadTestPage("/push_messaging/subscope2/test.html"); + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + // Note that we need to reload the page after registering, otherwise + // navigator.serviceWorker.ready is going to be resolved with the parent + // Service Worker which still controls the page. + LoadTestPage("/push_messaging/subscope2/test.html"); + std::string token2; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token2)); + EXPECT_NE(token1, token2); + PushMessagingAppIdentifier sw2_identifier = + GetAppIdentifierForServiceWorkerRegistration(2LL); + EXPECT_EQ(sw2_identifier.app_id(), gcm_driver_->last_gettoken_app_id()); + + LoadTestPage("/push_messaging/subscope1/test.html"); + std::string token3; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token3)); + EXPECT_NE(token1, token3); + EXPECT_NE(token2, token3); + PushMessagingAppIdentifier sw1_identifier = + GetAppIdentifierForServiceWorkerRegistration(1LL); + EXPECT_EQ(sw1_identifier.app_id(), gcm_driver_->last_gettoken_app_id()); + + // Now test that the Service Worker registration IDs and push subscription IDs + // generated above were persisted to SW storage, by checking that they are + // unchanged despite requesting them in a different order. + + LoadTestPage("/push_messaging/subscope1/test.html"); + std::string token4; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token4)); + EXPECT_EQ(token3, token4); + EXPECT_EQ(sw1_identifier.app_id(), gcm_driver_->last_gettoken_app_id()); + + LoadTestPage("/push_messaging/subscope2/test.html"); + std::string token5; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token5)); + EXPECT_EQ(token2, token5); + EXPECT_EQ(sw2_identifier.app_id(), gcm_driver_->last_gettoken_app_id()); + + LoadTestPage(); + std::string token6; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token6)); + EXPECT_EQ(token1, token6); + EXPECT_EQ(sw0_identifier.app_id(), gcm_driver_->last_gettoken_app_id()); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, AppHandlerOnlyIfSubscribed) { + // This test restarts the push service to simulate restarting the browser. + + EXPECT_NE(push_service(), GetAppHandler()); + ASSERT_NO_FATAL_FAILURE(RestartPushService()); + EXPECT_NE(push_service(), GetAppHandler()); + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + EXPECT_EQ(push_service(), GetAppHandler()); + ASSERT_NO_FATAL_FAILURE(RestartPushService()); + EXPECT_EQ(push_service(), GetAppHandler()); + + std::string script_result; + + // Unsubscribe. + base::RunLoop run_loop; + push_service()->SetUnsubscribeCallbackForTesting(run_loop.QuitClosure()); + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + // The app handler is only guaranteed to be unregistered once the unsubscribe + // callback for testing has been run (PushSubscription.unsubscribe() usually + // resolves before that, in order to avoid blocking on network retries etc). + run_loop.Run(); + + EXPECT_NE(push_service(), GetAppHandler()); + ASSERT_NO_FATAL_FAILURE(RestartPushService()); + EXPECT_NE(push_service(), GetAppHandler()); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PushEventSuccess) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("false - is not controlled", script_result); + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false)); + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.raw_data = "testdata"; + message.decrypted = true; + push_service()->OnMessage(app_identifier.app_id(), message); + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(true)); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result)); + EXPECT_EQ("testdata", script_result); + + // Check that we record this case in UMA. + histogram_tester_.ExpectUniqueSample( + "PushMessaging.DeliveryStatus.FindServiceWorker", + 0 /* SERVICE_WORKER_OK */, 1); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.DeliveryStatus.ServiceWorkerEvent", + 0 /* SERVICE_WORKER_OK */, 1); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.DeliveryStatus", + static_cast(blink::mojom::PushEventStatus::SUCCESS), 1); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PushEventOnShutdown) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("false - is not controlled", script_result); + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false)); + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.raw_data = "testdata"; + message.decrypted = true; + push_service()->Observe(chrome::NOTIFICATION_APP_TERMINATING, + content::NotificationService::AllSources(), + content::NotificationService::NoDetails()); + push_service()->OnMessage(app_identifier.app_id(), message); + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false)); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PushEventWithoutPayload) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.decrypted = false; + + push_service()->OnMessage(app_identifier.app_id(), message); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result)); + EXPECT_EQ("[NULL]", script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, LegacyPushEvent) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + gcm::IncomingMessage message; + message.sender_id = kManifestSenderId; + message.decrypted = false; + + push_service()->OnMessage(app_identifier.app_id(), message); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result)); + EXPECT_EQ("[NULL]", script_result); +} + +// Some users may have gotten into a state in the past where they still have +// a subscription even though the service worker was unregistered. +// Emulate this and test a push message triggers unsubscription. +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PushEventNoServiceWorker) { + std::string app_id; + ASSERT_NO_FATAL_FAILURE(SetupOrphanedPushSubscription(&app_id)); + + // Try to send a push message. + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.raw_data = "testdata"; + message.decrypted = true; + + base::RunLoop run_loop; + push_service()->SetMessageCallbackForTesting(run_loop.QuitClosure()); + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false)); + push_service()->OnMessage(app_id, message); + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(true)); + run_loop.Run(); + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false)); + + // No push data should have been received. + std::string script_result; + ASSERT_TRUE(RunScript("resultQueue.popImmediately()", &script_result)); + EXPECT_EQ("null", script_result); + + // Check that we record this case in UMA. + histogram_tester_.ExpectUniqueSample( + "PushMessaging.DeliveryStatus.FindServiceWorker", + 5 /* SERVICE_WORKER_ERROR_NOT_FOUND */, 1); + histogram_tester_.ExpectTotalCount( + "PushMessaging.DeliveryStatus.ServiceWorkerEvent", 0); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.DeliveryStatus", + static_cast(blink::mojom::PushEventStatus::NO_SERVICE_WORKER), 1); + + // Missing Service Workers should trigger an automatic unsubscription attempt. + EXPECT_EQ(app_id, gcm_driver_->last_deletetoken_app_id()); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast( + blink::mojom::PushUnregistrationReason::DELIVERY_NO_SERVICE_WORKER), + 1); + + // |app_identifier| should no longer be stored in prefs. + PushMessagingAppIdentifier stored_app_identifier = + PushMessagingAppIdentifier::FindByAppId(GetBrowser()->profile(), app_id); + EXPECT_TRUE(stored_app_identifier.is_null()); +} + +// Tests receiving messages for a subscription that no longer exists. +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, NoSubscription) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API), + 1); + + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.raw_data = "testdata"; + message.decrypted = true; + SendMessageAndWaitUntilHandled(app_identifier, message); + + // No push data should have been received. + ASSERT_TRUE(RunScript("resultQueue.popImmediately()", &script_result)); + EXPECT_EQ("null", script_result); + + // Check that we record this case in UMA. + histogram_tester_.ExpectTotalCount( + "PushMessaging.DeliveryStatus.FindServiceWorker", 0); + histogram_tester_.ExpectTotalCount( + "PushMessaging.DeliveryStatus.ServiceWorkerEvent", 0); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.DeliveryStatus", + static_cast(blink::mojom::PushEventStatus::UNKNOWN_APP_ID), 1); + + // Missing subscriptions should trigger an automatic unsubscription attempt. + EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_deletetoken_app_id()); + histogram_tester_.ExpectBucketCount( + "PushMessaging.UnregistrationReason", + static_cast( + blink::mojom::PushUnregistrationReason::DELIVERY_UNKNOWN_APP_ID), + 1); +} + +// Tests receiving messages for an origin that does not have permission, but +// somehow still has a subscription (as happened in https://crbug.com/633310). +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PushEventWithoutPermission) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Revoke notifications permission, but first disable the + // PushMessagingServiceImpl's OnContentSettingChanged handler so that it + // doesn't automatically unsubscribe, since we want to test the case where + // there is still a subscription. + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->RemoveObserver(push_service()); + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->ClearSettingsForOneType(ContentSettingsType::NOTIFICATIONS); + base::RunLoop().RunUntilIdle(); + + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.raw_data = "testdata"; + message.decrypted = true; + SendMessageAndWaitUntilHandled(app_identifier, message); + + // No push data should have been received. + ASSERT_TRUE(RunScript("resultQueue.popImmediately()", &script_result)); + EXPECT_EQ("null", script_result); + + // Check that we record this case in UMA. + histogram_tester_.ExpectTotalCount( + "PushMessaging.DeliveryStatus.FindServiceWorker", 0); + histogram_tester_.ExpectTotalCount( + "PushMessaging.DeliveryStatus.ServiceWorkerEvent", 0); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.DeliveryStatus", + static_cast(blink::mojom::PushEventStatus::PERMISSION_DENIED), 1); + + // Missing permission should trigger an automatic unsubscription attempt. + EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_deletetoken_app_id()); + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("false - not subscribed", script_result); + GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + PushMessagingAppIdentifier app_identifier_afterwards = + PushMessagingAppIdentifier::FindByServiceWorker(GetBrowser()->profile(), + origin, 0LL); + EXPECT_TRUE(app_identifier_afterwards.is_null()); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast( + blink::mojom::PushUnregistrationReason::DELIVERY_PERMISSION_DENIED), + 1); +} + +// https://crbug.com/458160 test is flaky on all platforms; but mostly linux. +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + DISABLED_PushEventEnforcesUserVisibleNotification) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("false - is not controlled", script_result); + + LoadTestPage(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + RemoveAllNotifications(); + ASSERT_EQ(0u, GetNotificationCount()); + + // We'll need to specify the web_contents in which to eval script, since we're + // going to run script in a background tab. + content::WebContents* web_contents = + GetBrowser()->tab_strip_model()->GetActiveWebContents(); + + // Set the site engagement score for the site. Setting it to 10 means it + // should have a budget of 4, enough for two non-shown notification, which + // cost 2 each. + SetSiteEngagementScore(web_contents->GetLastCommittedURL(), 10.0); + + // If the site is visible in an active tab, we should not force a notification + // to be shown. Try it twice, since we allow one mistake per 10 push events. + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.decrypted = true; + for (int n = 0; n < 2; n++) { + message.raw_data = "testdata"; + SendMessageAndWaitUntilHandled(app_identifier, message); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result)); + EXPECT_EQ("testdata", script_result); + EXPECT_EQ(0u, GetNotificationCount()); + } + + // Open a blank foreground tab so site is no longer visible. + ui_test_utils::NavigateToURLWithDisposition( + GetBrowser(), GURL("about:blank"), + WindowOpenDisposition::NEW_FOREGROUND_TAB, + ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB); + + // If the Service Worker push event handler shows a notification, we + // should not show a forced one. + message.raw_data = "shownotification"; + SendMessageAndWaitUntilHandled(app_identifier, message); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents)); + EXPECT_EQ("shownotification", script_result); + EXPECT_EQ(1u, GetNotificationCount()); + EXPECT_TRUE(TagEquals(GetDisplayedNotifications()[0], "push_test_tag")); + RemoveAllNotifications(); + + // If the Service Worker push event handler does not show a notification, we + // should show a forced one, but only once the origin is out of budget. + message.raw_data = "testdata"; + for (int n = 0; n < 2; n++) { + // First two missed notifications shouldn't force a default one. + SendMessageAndWaitUntilHandled(app_identifier, message); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents)); + EXPECT_EQ("testdata", script_result); + EXPECT_EQ(0u, GetNotificationCount()); + } + + // Third missed notification should trigger a default notification, since the + // origin will be out of budget. + message.raw_data = "testdata"; + SendMessageAndWaitUntilHandled(app_identifier, message); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents)); + EXPECT_EQ("testdata", script_result); + + { + std::vector notifications = + GetDisplayedNotifications(); + ASSERT_EQ(notifications.size(), 1u); + + EXPECT_TRUE( + TagEquals(notifications[0], kPushMessagingForcedNotificationTag)); + EXPECT_TRUE(notifications[0].silent()); + } + + // The notification will be automatically dismissed when the developer shows + // a new notification themselves at a later point in time. + message.raw_data = "shownotification"; + SendMessageAndWaitUntilHandled(app_identifier, message); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents)); + EXPECT_EQ("shownotification", script_result); + + { + std::vector notifications = + GetDisplayedNotifications(); + ASSERT_EQ(notifications.size(), 1u); + + EXPECT_FALSE( + TagEquals(notifications[0], kPushMessagingForcedNotificationTag)); + } +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + PushEventAllowSilentPushCommandLineFlag) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_gettoken_app_id()); + EXPECT_EQ(kEncodedApplicationServerKey, + gcm_driver_->last_gettoken_authorized_entity()); + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("false - is not controlled", script_result); + + LoadTestPage(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + RemoveAllNotifications(); + ASSERT_EQ(0u, GetNotificationCount()); + + // We'll need to specify the web_contents in which to eval script, since we're + // going to run script in a background tab. + content::WebContents* web_contents = + GetBrowser()->tab_strip_model()->GetActiveWebContents(); + + SetSiteEngagementScore(web_contents->GetLastCommittedURL(), 5.0); + + ui_test_utils::NavigateToURLWithDisposition( + GetBrowser(), GURL("about:blank"), + WindowOpenDisposition::NEW_FOREGROUND_TAB, + ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB); + + // Send a missed notification to use up the budget. + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.raw_data = "testdata"; + message.decrypted = true; + + SendMessageAndWaitUntilHandled(app_identifier, message); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents)); + EXPECT_EQ("testdata", script_result); + EXPECT_EQ(0u, GetNotificationCount()); + + // If the Service Worker push event handler does not show a notification, we + // should show a forced one providing there is no foreground tab and the + // origin ran out of budget. + SendMessageAndWaitUntilHandled(app_identifier, message); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents)); + EXPECT_EQ("testdata", script_result); + + // Because the --allow-silent-push command line flag has not been passed, + // this should have shown a default notification. + { + std::vector notifications = + GetDisplayedNotifications(); + ASSERT_EQ(notifications.size(), 1u); + + EXPECT_TRUE( + TagEquals(notifications[0], kPushMessagingForcedNotificationTag)); + EXPECT_TRUE(notifications[0].silent()); + } + + RemoveAllNotifications(); + + // Send the message again, but this time with the -allow-silent-push command + // line flag set. The default notification should *not* be shown. + base::CommandLine::ForCurrentProcess()->AppendSwitch( + switches::kAllowSilentPush); + + SendMessageAndWaitUntilHandled(app_identifier, message); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents)); + EXPECT_EQ("testdata", script_result); + + ASSERT_EQ(0u, GetNotificationCount()); +} + +class PushMessagingBrowserTestWithAbusiveOriginPermissionRevocation + : public PushMessagingBrowserTestBase { + public: + PushMessagingBrowserTestWithAbusiveOriginPermissionRevocation() = default; + + using SiteReputation = CrowdDenyPreloadData::SiteReputation; + + void CreatedBrowserMainParts( + content::BrowserMainParts* browser_main_parts) override { + PushMessagingBrowserTestBase::CreatedBrowserMainParts(browser_main_parts); + + testing_preload_data_.emplace(); + fake_database_manager_ = + base::MakeRefCounted(); + test_safe_browsing_factory_ = + std::make_unique(); + test_safe_browsing_factory_->SetTestDatabaseManager( + fake_database_manager_.get()); + safe_browsing::SafeBrowsingServiceInterface::RegisterFactory( + test_safe_browsing_factory_.get()); + } + + void AddToPreloadDataBlocklist( + const GURL& origin, + chrome_browser_crowd_deny:: + SiteReputation_NotificationUserExperienceQuality reputation_type) { + SiteReputation reputation; + reputation.set_notification_ux_quality(reputation_type); + testing_preload_data_->SetOriginReputation(url::Origin::Create(origin), + std::move(reputation)); + } + + void AddToSafeBrowsingBlocklist(const GURL& url) { + safe_browsing::ThreatMetadata test_metadata; + test_metadata.api_permissions.emplace("NOTIFICATIONS"); + fake_database_manager_->SetSimulatedMetadataForUrl(url, test_metadata); + } + + private: + base::test::ScopedFeatureList feature_list_; + absl::optional + testing_preload_data_; + scoped_refptr + fake_database_manager_; + std::unique_ptr + test_safe_browsing_factory_; +}; + +IN_PROC_BROWSER_TEST_F( + PushMessagingBrowserTestWithAbusiveOriginPermissionRevocation, + PushEventPermissionRevoked) { + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + LoadTestPage(); // Reload to become controlled. + std::string script_result; + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Add an origin to blocking lists after service worker is registered. + AddToPreloadDataBlocklist( + https_server()->GetURL("/").DeprecatedGetOriginAsURL(), + SiteReputation::ABUSIVE_CONTENT); + AddToSafeBrowsingBlocklist( + https_server()->GetURL("/").DeprecatedGetOriginAsURL()); + + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.raw_data = "testdata"; + message.decrypted = true; + SendMessageAndWaitUntilHandled(app_identifier, message); + + // No push data should have been received. + ASSERT_TRUE(RunScript("resultQueue.popImmediately()", &script_result)); + EXPECT_EQ("null", script_result); + + // Check that we record this case in UMA. + histogram_tester_.ExpectTotalCount( + "PushMessaging.DeliveryStatus.FindServiceWorker", 0); + histogram_tester_.ExpectTotalCount( + "PushMessaging.DeliveryStatus.ServiceWorkerEvent", 0); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.DeliveryStatus", + static_cast( + blink::mojom::PushEventStatus::PERMISSION_REVOKED_ABUSIVE), + 1); + + // Missing permission should trigger an automatic unsubscription attempt. + EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_deletetoken_app_id()); + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("false - not subscribed", script_result); + GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + PushMessagingAppIdentifier app_identifier_afterwards = + PushMessagingAppIdentifier::FindByServiceWorker(GetBrowser()->profile(), + origin, 0LL); + EXPECT_TRUE(app_identifier_afterwards.is_null()); + + // 1st event - blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED. + // 2nd event - + // blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED_ABUSIVE. + histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 2); + + histogram_tester_.ExpectBucketCount( + "PushMessaging.UnregistrationReason", + blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED_ABUSIVE, 1); + histogram_tester_.ExpectBucketCount( + "PushMessaging.UnregistrationReason", + blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED, 1); +} + +// That test verifies that an origin is not revoked because it is not on +// SafeBrowsing blocking list. +IN_PROC_BROWSER_TEST_F( + PushMessagingBrowserTestWithAbusiveOriginPermissionRevocation, + OriginIsNotOnSafeBrowsingBlockingList) { + std::string script_result; + + // The origin should be marked as |ABUSIVE_CONTENT| on |CrowdDenyPreloadData| + // otherwise the permission revocation logic will not be triggered. + AddToPreloadDataBlocklist( + https_server()->GetURL("/").DeprecatedGetOriginAsURL(), + SiteReputation::ABUSIVE_CONTENT); + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("false - is not controlled", script_result); + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false)); + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.raw_data = "testdata"; + message.decrypted = true; + push_service()->OnMessage(app_identifier.app_id(), message); + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(true)); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result)); + EXPECT_EQ("testdata", script_result); + + // Check that we record this case in UMA. + histogram_tester_.ExpectUniqueSample( + "PushMessaging.DeliveryStatus.FindServiceWorker", + 0 /* SERVICE_WORKER_OK */, 1); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.DeliveryStatus.ServiceWorkerEvent", + 0 /* SERVICE_WORKER_OK */, 1); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.DeliveryStatus", + static_cast(blink::mojom::PushEventStatus::SUCCESS), 1); +} + +class PushMessagingBrowserTestWithNotificationTriggersEnabled + : public PushMessagingBrowserTestBase { + public: + PushMessagingBrowserTestWithNotificationTriggersEnabled() { + feature_list_.InitAndEnableFeature(features::kNotificationTriggers); + } + + private: + base::test::ScopedFeatureList feature_list_; +}; + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTestWithNotificationTriggersEnabled, + PushEventIgnoresScheduledNotificationsForEnforcement) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + LoadTestPage(); // Reload to become controlled. + + RemoveAllNotifications(); + + // We'll need to specify the web_contents in which to eval script, since we're + // going to run script in a background tab. + content::WebContents* web_contents = + GetBrowser()->tab_strip_model()->GetActiveWebContents(); + + // Initialize site engagement score to have no budget for silent pushes. + SetSiteEngagementScore(web_contents->GetLastCommittedURL(), 0); + + ui_test_utils::NavigateToURLWithDisposition( + GetBrowser(), GURL("about:blank"), + WindowOpenDisposition::NEW_FOREGROUND_TAB, + ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB); + + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.raw_data = "shownotification-with-showtrigger"; + message.decrypted = true; + + // If the Service Worker push event handler only schedules a notification, we + // should show a forced one providing there is no foreground tab and the + // origin ran out of budget. + SendMessageAndWaitUntilHandled(app_identifier, message); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents)); + EXPECT_EQ("shownotification-with-showtrigger", script_result); + + // Because scheduled notifications do not count as displayed notifications, + // this should have shown a default notification. + std::vector notifications = + GetDisplayedNotifications(); + ASSERT_EQ(notifications.size(), 1u); + + EXPECT_TRUE(TagEquals(notifications[0], kPushMessagingForcedNotificationTag)); + EXPECT_TRUE(notifications[0].silent()); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + PushEventEnforcesUserVisibleNotificationAfterQueue) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("false - is not controlled", script_result); + + LoadTestPage(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Fire off two push messages in sequence, only the second one of which will + // display a notification. The additional round-trip and I/O required by the + // second message, which shows a notification, should give us a reasonable + // confidence that the ordering will be maintained. + + std::vector number_of_notifications_shown; + + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.decrypted = true; + + { + base::RunLoop run_loop; + push_service()->SetMessageCallbackForTesting(base::BindRepeating( + &PushMessagingBrowserTestBase::OnDeliveryFinished, + base::Unretained(this), &number_of_notifications_shown, + base::BarrierClosure(2 /* num_closures */, run_loop.QuitClosure()))); + + message.raw_data = "testdata"; + push_service()->OnMessage(app_identifier.app_id(), message); + + message.raw_data = "shownotification"; + push_service()->OnMessage(app_identifier.app_id(), message); + + run_loop.Run(); + } + + ASSERT_EQ(2u, number_of_notifications_shown.size()); + EXPECT_EQ(0u, number_of_notifications_shown[0]); + EXPECT_EQ(1u, number_of_notifications_shown[1]); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + PushEventNotificationWithoutEventWaitUntil) { + std::string script_result; + content::WebContents* web_contents = + GetBrowser()->tab_strip_model()->GetActiveWebContents(); + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("false - is not controlled", script_result); + + LoadTestPage(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + base::RunLoop run_loop; + base::RepeatingClosure quit_barrier = + base::BarrierClosure(2 /* num_closures */, run_loop.QuitClosure()); + push_service()->SetMessageCallbackForTesting(quit_barrier); + notification_tester_->SetNotificationAddedClosure(quit_barrier); + + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.raw_data = "shownotification-without-waituntil"; + message.decrypted = true; + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false)); + push_service()->OnMessage(app_identifier.app_id(), message); + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(true)); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents)); + EXPECT_EQ("immediate:shownotification-without-waituntil", script_result); + + run_loop.Run(); + + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false)); + ASSERT_EQ(1u, GetNotificationCount()); + EXPECT_TRUE(TagEquals(GetDisplayedNotifications()[0], "push_test_tag")); + + // Verify that the renderer process hasn't crashed. + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PermissionStateSaysPrompt) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + ASSERT_EQ("permission status - prompt", script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PermissionStateSaysGranted) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result)); + ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result)); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PermissionStateSaysDenied) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndDenyPermission()); + + ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result)); + EXPECT_EQ("NotAllowedError - Registration failed - permission denied", + script_result); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - denied", script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, CrossOriginFrame) { + const GURL kEmbedderURL = https_server()->GetURL( + "embedder.com", "/push_messaging/framed_test.html"); + const GURL kRequesterURL = https_server()->GetURL("requester.com", "/"); + + ASSERT_TRUE(ui_test_utils::NavigateToURL(GetBrowser(), kEmbedderURL)); + + auto* web_contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); + LOG(ERROR) << web_contents->GetLastCommittedURL(); + auto* subframe = content::ChildFrameAt(web_contents->GetMainFrame(), 0u); + ASSERT_TRUE(subframe); + + // A cross-origin subframe that had not been granted the NOTIFICATIONS + // permission previously should see it as "denied", not be able to request it, + // and not be able to use the Push and Web Notification API. It is verified + // that no prompts are shown by auto-accepting and still expecting the + // permission to be denied. + + GetPermissionRequestManager()->set_auto_response_for_test( + permissions::PermissionRequestManager::ACCEPT_ALL); + + std::string script_result; + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + subframe, "requestNotificationPermission();", &script_result)); + EXPECT_EQ("permission status - denied", script_result); + + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + subframe, "registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + subframe, "pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - denied", script_result); + + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + subframe, "notificationPermissionState()", &script_result)); + EXPECT_EQ("permission status - denied", script_result); + + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + subframe, "notificationPermissionAPIState()", &script_result)); + EXPECT_EQ("permission status - denied", script_result); + + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + subframe, "documentSubscribePush()", &script_result)); + EXPECT_EQ("NotAllowedError - Registration failed - permission denied", + script_result); + + // A cross-origin subframe that had been granted the NOTIFICATIONS permission + // previously (in a first-party context) should see it as "granted", and be + // able to use the Push and Web Notifications APIs. + + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->SetContentSettingDefaultScope(kRequesterURL, kRequesterURL, + ContentSettingsType::NOTIFICATIONS, + CONTENT_SETTING_ALLOW); + + GetPermissionRequestManager()->set_auto_response_for_test( + permissions::PermissionRequestManager::DENY_ALL); + + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + subframe, "requestNotificationPermission();", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + subframe, "pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + subframe, "notificationPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + subframe, "notificationPermissionAPIState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + subframe, "documentSubscribePush()", &script_result)); + ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result)); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, UnsubscribeSuccess) { + std::string script_result; + + std::string token1; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kOmitKey, &token1)); + ASSERT_TRUE(RunScript("storePushSubscription()", &script_result)); + EXPECT_EQ("ok - stored", script_result); + + // Resolves true if there was a subscription. + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API), + 1); + + // Resolves false if there was no longer a subscription. + ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result)); + EXPECT_EQ("unsubscribe result: false", script_result); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API), + 2); + + // TODO(johnme): Test that doesn't reject if there was a network error (should + // deactivate subscription locally anyway). + // TODO(johnme): Test that doesn't reject if there were other push service + // errors (should deactivate subscription locally anyway). + + // Unsubscribing (with an existing reference to a PushSubscription), after + // replacing the Service Worker, actually still works, as the Service Worker + // registration is unchanged. + std::string token2; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kOmitKey, &token2)); + EXPECT_NE(token1, token2); + ASSERT_TRUE(RunScript("storePushSubscription()", &script_result)); + EXPECT_EQ("ok - stored", script_result); + ASSERT_TRUE(RunScript("replaceServiceWorker()", &script_result)); + EXPECT_EQ("ok - service worker replaced", script_result); + ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API), + 3); + + // Unsubscribing (with an existing reference to a PushSubscription), after + // unregistering the Service Worker, should fail. + std::string token3; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kOmitKey, &token3)); + EXPECT_NE(token1, token3); + EXPECT_NE(token2, token3); + ASSERT_TRUE(RunScript("storePushSubscription()", &script_result)); + EXPECT_EQ("ok - stored", script_result); + + // Unregister service worker and wait for callback. + base::RunLoop run_loop; + push_service()->SetServiceWorkerUnregisteredCallbackForTesting( + run_loop.QuitClosure()); + ASSERT_TRUE(RunScript("unregisterServiceWorker()", &script_result)); + EXPECT_EQ("service worker unregistration status: true", script_result); + run_loop.Run(); + + // Unregistering should have triggered an automatic unsubscribe. + histogram_tester_.ExpectBucketCount( + "PushMessaging.UnregistrationReason", + static_cast( + blink::mojom::PushUnregistrationReason::SERVICE_WORKER_UNREGISTERED), + 1); + histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 4); + + // Now manual unsubscribe should return false. + ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result)); + EXPECT_EQ("unsubscribe result: false", script_result); +} + +// Push subscriptions used to be non-InstanceID GCM registrations. Still need +// to be able to unsubscribe these, even though new ones are no longer created. +// Flaky on some Win and Linux buildbots. See crbug.com/835382. +#if defined(OS_WIN) || defined(OS_LINUX) || defined(OS_CHROMEOS) +#define MAYBE_LegacyUnsubscribeSuccess DISABLED_LegacyUnsubscribeSuccess +#else +#define MAYBE_LegacyUnsubscribeSuccess LegacyUnsubscribeSuccess +#endif +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + MAYBE_LegacyUnsubscribeSuccess) { + std::string script_result; + + std::string subscription_id1; + ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully(&subscription_id1)); + ASSERT_TRUE(RunScript("storePushSubscription()", &script_result)); + EXPECT_EQ("ok - stored", script_result); + + // Resolves true if there was a subscription. + gcm_service_->AddExpectedUnregisterResponse(gcm::GCMClient::SUCCESS); + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API), + 1); + + // Resolves false if there was no longer a subscription. + ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result)); + EXPECT_EQ("unsubscribe result: false", script_result); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API), + 2); + + // Doesn't reject if there was a network error (deactivates subscription + // locally anyway). + std::string subscription_id2; + ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully(&subscription_id2)); + EXPECT_NE(subscription_id1, subscription_id2); + gcm_service_->AddExpectedUnregisterResponse(gcm::GCMClient::NETWORK_ERROR); + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API), + 3); + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("false - not subscribed", script_result); + + // Doesn't reject if there were other push service errors (deactivates + // subscription locally anyway). + std::string subscription_id3; + ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully(&subscription_id3)); + EXPECT_NE(subscription_id1, subscription_id3); + EXPECT_NE(subscription_id2, subscription_id3); + gcm_service_->AddExpectedUnregisterResponse( + gcm::GCMClient::INVALID_PARAMETER); + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API), + 4); + + // Unsubscribing (with an existing reference to a PushSubscription), after + // replacing the Service Worker, actually still works, as the Service Worker + // registration is unchanged. + std::string subscription_id4; + ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully(&subscription_id4)); + EXPECT_NE(subscription_id1, subscription_id4); + EXPECT_NE(subscription_id2, subscription_id4); + EXPECT_NE(subscription_id3, subscription_id4); + ASSERT_TRUE(RunScript("storePushSubscription()", &script_result)); + EXPECT_EQ("ok - stored", script_result); + ASSERT_TRUE(RunScript("replaceServiceWorker()", &script_result)); + EXPECT_EQ("ok - service worker replaced", script_result); + ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API), + 5); + + // Unsubscribing (with an existing reference to a PushSubscription), after + // unregistering the Service Worker, should fail. + std::string subscription_id5; + ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully(&subscription_id5)); + EXPECT_NE(subscription_id1, subscription_id5); + EXPECT_NE(subscription_id2, subscription_id5); + EXPECT_NE(subscription_id3, subscription_id5); + EXPECT_NE(subscription_id4, subscription_id5); + ASSERT_TRUE(RunScript("storePushSubscription()", &script_result)); + EXPECT_EQ("ok - stored", script_result); + + // Unregister service worker and wait for callback. + base::RunLoop run_loop; + push_service()->SetServiceWorkerUnregisteredCallbackForTesting( + run_loop.QuitClosure()); + ASSERT_TRUE(RunScript("unregisterServiceWorker()", &script_result)); + EXPECT_EQ("service worker unregistration status: true", script_result); + run_loop.Run(); + + // Unregistering should have triggered an automatic unsubscribe. + histogram_tester_.ExpectBucketCount( + "PushMessaging.UnregistrationReason", + static_cast( + blink::mojom::PushUnregistrationReason::SERVICE_WORKER_UNREGISTERED), + 1); + histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 6); + + // Now manual unsubscribe should return false. + ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result)); + EXPECT_EQ("unsubscribe result: false", script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, UnsubscribeOffline) { + std::string script_result; + + EXPECT_NE(push_service(), GetAppHandler()); + + std::string token; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token)); + + gcm_service_->set_offline(true); + + // Should quickly resolve true after deleting local state (rather than waiting + // until unsubscribing over the network exceeds the maximum backoff duration). + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API), + 1); + + // Since the service is offline, the network request to GCM is still being + // retried, so the app handler shouldn't have been unregistered yet. + EXPECT_EQ(push_service(), GetAppHandler()); + // But restarting the push service will unregister the app handler, since the + // subscription is no longer stored in the PushMessagingAppIdentifier map. + ASSERT_NO_FATAL_FAILURE(RestartPushService()); + EXPECT_NE(push_service(), GetAppHandler()); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + UnregisteringServiceWorkerUnsubscribes) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Unregister the worker, and wait for callback to complete. + base::RunLoop run_loop; + push_service()->SetServiceWorkerUnregisteredCallbackForTesting( + run_loop.QuitClosure()); + ASSERT_TRUE(RunScript("unregisterServiceWorker()", &script_result)); + ASSERT_EQ("service worker unregistration status: true", script_result); + run_loop.Run(); + + // This should have unregistered the push subscription. + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast( + blink::mojom::PushUnregistrationReason::SERVICE_WORKER_UNREGISTERED), + 1); + + // We should not be able to look up the app id. + GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker( + GetBrowser()->profile(), origin, + 0LL /* service_worker_registration_id */); + EXPECT_TRUE(app_identifier.is_null()); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + ServiceWorkerDatabaseDeletionUnsubscribes) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Pretend as if the Service Worker database went away, and wait for callback + // to complete. + base::RunLoop run_loop; + push_service()->SetServiceWorkerDatabaseWipedCallbackForTesting( + run_loop.QuitClosure()); + push_service()->DidDeleteServiceWorkerDatabase(); + run_loop.Run(); + + // This should have unregistered the push subscription. + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast(blink::mojom::PushUnregistrationReason:: + SERVICE_WORKER_DATABASE_WIPED), + 1); + + // There should not be any subscriptions left. + EXPECT_EQ(PushMessagingAppIdentifier::GetCount(GetBrowser()->profile()), 0u); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + InvalidGetSubscriptionUnsubscribes) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + PushMessagingAppIdentifier app_identifier1 = + PushMessagingAppIdentifier::FindByServiceWorker( + GetBrowser()->profile(), origin, + 0LL /* service_worker_registration_id */); + ASSERT_FALSE(app_identifier1.is_null()); + + ASSERT_NO_FATAL_FAILURE( + DeleteInstanceIDAsIfGCMStoreReset(app_identifier1.app_id())); + + // Push messaging should not yet be aware of the InstanceID being deleted. + histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 0); + // We should still be able to look up the app id. + PushMessagingAppIdentifier app_identifier2 = + PushMessagingAppIdentifier::FindByServiceWorker( + GetBrowser()->profile(), origin, + 0LL /* service_worker_registration_id */); + EXPECT_FALSE(app_identifier2.is_null()); + EXPECT_EQ(app_identifier1.app_id(), app_identifier2.app_id()); + + // Now call PushManager.getSubscription(). It should return null. + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("false - not subscribed", script_result); + + // This should have unsubscribed the push subscription. + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast(blink::mojom::PushUnregistrationReason:: + GET_SUBSCRIPTION_STORAGE_CORRUPT), + 1); + // We should no longer be able to look up the app id. + PushMessagingAppIdentifier app_identifier3 = + PushMessagingAppIdentifier::FindByServiceWorker( + GetBrowser()->profile(), origin, + 0LL /* service_worker_registration_id */); + EXPECT_TRUE(app_identifier3.is_null()); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + GlobalResetPushPermissionUnsubscribes) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + scoped_refptr message_loop_runner = + new content::MessageLoopRunner; + push_service()->SetContentSettingChangedCallbackForTesting( + base::BarrierClosure(1, message_loop_runner->QuitClosure())); + + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->ClearSettingsForOneType(ContentSettingsType::NOTIFICATIONS); + + message_loop_runner->Run(); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - prompt", script_result); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("false - not subscribed", script_result); + + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast( + blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED), + 1); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + LocalResetPushPermissionUnsubscribes) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + scoped_refptr message_loop_runner = + new content::MessageLoopRunner; + push_service()->SetContentSettingChangedCallbackForTesting( + base::BarrierClosure(1, message_loop_runner->QuitClosure())); + + GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->SetContentSettingDefaultScope(origin, origin, + ContentSettingsType::NOTIFICATIONS, + CONTENT_SETTING_DEFAULT); + + message_loop_runner->Run(); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - prompt", script_result); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("false - not subscribed", script_result); + + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast( + blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED), + 1); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + DenyPushPermissionUnsubscribes) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + scoped_refptr message_loop_runner = + new content::MessageLoopRunner; + push_service()->SetContentSettingChangedCallbackForTesting( + base::BarrierClosure(1, message_loop_runner->QuitClosure())); + + GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->SetContentSettingDefaultScope(origin, origin, + ContentSettingsType::NOTIFICATIONS, + CONTENT_SETTING_BLOCK); + + message_loop_runner->Run(); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - denied", script_result); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("false - not subscribed", script_result); + + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast( + blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED), + 1); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + GlobalResetNotificationsPermissionUnsubscribes) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + scoped_refptr message_loop_runner = + new content::MessageLoopRunner; + push_service()->SetContentSettingChangedCallbackForTesting( + base::BarrierClosure(1, message_loop_runner->QuitClosure())); + + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->ClearSettingsForOneType(ContentSettingsType::NOTIFICATIONS); + + message_loop_runner->Run(); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - prompt", script_result); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("false - not subscribed", script_result); + + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast( + blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED), + 1); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + LocalResetNotificationsPermissionUnsubscribes) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + scoped_refptr message_loop_runner = + new content::MessageLoopRunner; + push_service()->SetContentSettingChangedCallbackForTesting( + base::BarrierClosure(1, message_loop_runner->QuitClosure())); + + GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->SetContentSettingDefaultScope(origin, GURL(), + ContentSettingsType::NOTIFICATIONS, + CONTENT_SETTING_DEFAULT); + + message_loop_runner->Run(); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - prompt", script_result); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("false - not subscribed", script_result); + + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast( + blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED), + 1); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + DenyNotificationsPermissionUnsubscribes) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + scoped_refptr message_loop_runner = + new content::MessageLoopRunner; + push_service()->SetContentSettingChangedCallbackForTesting( + base::BarrierClosure(1, message_loop_runner->QuitClosure())); + + GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->SetContentSettingDefaultScope(origin, GURL(), + ContentSettingsType::NOTIFICATIONS, + CONTENT_SETTING_BLOCK); + + message_loop_runner->Run(); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - denied", script_result); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("false - not subscribed", script_result); + + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast( + blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED), + 1); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + GrantAlreadyGrantedPermissionDoesNotUnsubscribe) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + scoped_refptr message_loop_runner = + new content::MessageLoopRunner; + push_service()->SetContentSettingChangedCallbackForTesting( + base::BarrierClosure(1, message_loop_runner->QuitClosure())); + + GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->SetContentSettingDefaultScope(origin, GURL(), + ContentSettingsType::NOTIFICATIONS, + CONTENT_SETTING_ALLOW); + + message_loop_runner->Run(); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 0); +} + +// This test is testing some non-trivial content settings rules and make sure +// that they are respected with regards to automatic unsubscription. In other +// words, it checks that the push service does not end up unsubscribing origins +// that have push permission with some non-common rules. +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + AutomaticUnsubscriptionFollowsContentSettingRules) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + scoped_refptr message_loop_runner = + new content::MessageLoopRunner; + push_service()->SetContentSettingChangedCallbackForTesting( + base::BarrierClosure(2, message_loop_runner->QuitClosure())); + + GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->SetDefaultContentSetting(ContentSettingsType::NOTIFICATIONS, + CONTENT_SETTING_ALLOW); + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->SetContentSettingDefaultScope(origin, GURL(), + ContentSettingsType::NOTIFICATIONS, + CONTENT_SETTING_DEFAULT); + + message_loop_runner->Run(); + + // The two first rules should give |origin| the permission to use Push even + // if the rules it used to have have been reset. + // The Push service should not unsubscribe |origin| because at no point it was + // left without permission to use Push. + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 0); +} + +// Checks automatically unsubscribing due to a revoked permission after +// previously clearing site data, under legacy conditions (ie. when +// unregistering a worker did not unsubscribe from push.) +IN_PROC_BROWSER_TEST_F( + PushMessagingBrowserTest, + ResetPushPermissionAfterClearingSiteDataUnderLegacyConditions) { + std::string app_id; + ASSERT_NO_FATAL_FAILURE(SetupOrphanedPushSubscription(&app_id)); + + // Simulate a user clearing site data (including Service Workers, crucially). + content::BrowsingDataRemover* remover = + GetBrowser()->profile()->GetBrowsingDataRemover(); + content::BrowsingDataRemoverCompletionObserver observer(remover); + remover->RemoveAndReply( + base::Time(), base::Time::Max(), + chrome_browsing_data_remover::DATA_TYPE_SITE_DATA, + content::BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB, &observer); + observer.BlockUntilCompletion(); + + base::RunLoop run_loop; + push_service()->SetContentSettingChangedCallbackForTesting( + run_loop.QuitClosure()); + // This shouldn't (asynchronously) cause a DCHECK. + // TODO(johnme): Get this test running on Android with legacy GCM + // registrations, which have a different codepath due to sender_id being + // required for unsubscribing there. + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->ClearSettingsForOneType(ContentSettingsType::NOTIFICATIONS); + + run_loop.Run(); + + // |app_identifier| should no longer be stored in prefs. + PushMessagingAppIdentifier stored_app_identifier = + PushMessagingAppIdentifier::FindByAppId(GetBrowser()->profile(), app_id); + EXPECT_TRUE(stored_app_identifier.is_null()); + + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast( + blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED), + 1); + + base::RunLoop().RunUntilIdle(); + + // Revoked permission should trigger an automatic unsubscription attempt. + EXPECT_EQ(app_id, gcm_driver_->last_deletetoken_app_id()); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, EncryptionKeyUniqueness) { + std::string token1; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kOmitKey, &token1)); + + std::string first_public_key; + ASSERT_TRUE(RunScript("GetP256dh()", &first_public_key)); + EXPECT_GE(first_public_key.size(), 32u); + + std::string script_result; + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + + std::string token2; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token2)); + EXPECT_NE(token1, token2); + + std::string second_public_key; + ASSERT_TRUE(RunScript("GetP256dh()", &second_public_key)); + EXPECT_GE(second_public_key.size(), 32u); + + EXPECT_NE(first_public_key, second_public_key); +} + +class PushMessagingIncognitoBrowserTest : public PushMessagingBrowserTestBase { + public: + PushMessagingIncognitoBrowserTest() + : prerender_helper_(base::BindRepeating( + &PushMessagingIncognitoBrowserTest::web_contents, + base::Unretained(this))) {} + ~PushMessagingIncognitoBrowserTest() override = default; + + // PushMessagingBrowserTest: + void SetUpOnMainThread() override { + incognito_browser_ = CreateIncognitoBrowser(); + // We SetUp here rather than in SetUp since the https_server isn't yet + // created at that time. + prerender_helper_.SetUp(https_server()); + PushMessagingBrowserTestBase::SetUpOnMainThread(); + } + Browser* GetBrowser() const override { return incognito_browser_; } + + content::WebContents* web_contents() { + return GetBrowser()->tab_strip_model()->GetActiveWebContents(); + } + + protected: + content::test::PrerenderTestHelper prerender_helper_; + raw_ptr incognito_browser_ = nullptr; +}; + +// Regression test for https://crbug.com/476474 +IN_PROC_BROWSER_TEST_F(PushMessagingIncognitoBrowserTest, + IncognitoGetSubscriptionDoesNotHang) { + ASSERT_TRUE(GetBrowser()->profile()->IsOffTheRecord()); + + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + // In Incognito mode the promise returned by getSubscription should not hang, + // it should just fulfill with null. + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + ASSERT_EQ("false - not subscribed", script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingIncognitoBrowserTest, WarningToCorrectRFH) { + ASSERT_TRUE(GetBrowser()->profile()->IsOffTheRecord()); + + content::WebContentsConsoleObserver console_observer(web_contents()); + console_observer.SetPattern(kIncognitoWarningPattern); + + // Filter out the main frame host of the currently active page. + content::RenderFrameHost* rfh = web_contents()->GetMainFrame(); + console_observer.SetFilter(base::BindLambdaForTesting( + [&](const content::WebContentsConsoleObserver::Message& message) { + return message.source_frame == rfh; + })); + + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result)); + ASSERT_EQ("AbortError - Registration failed - permission denied", + script_result); + + console_observer.Wait(); + EXPECT_EQ(1u, console_observer.messages().size()); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingIncognitoBrowserTest, + WarningToCorrectRFH_Prerender) { + ASSERT_TRUE(GetBrowser()->profile()->IsOffTheRecord()); + + const GURL url(https_server()->GetURL(GetTestURL())); + + // Start a prerender with the push messaging test URL. + int host_id = prerender_helper_.AddPrerender(url); + content::test::PrerenderHostObserver prerender_observer(*web_contents(), + host_id); + ASSERT_NE(prerender_helper_.GetHostForUrl(url), + content::RenderFrameHost::kNoFrameTreeNodeId); + + content::WebContentsConsoleObserver console_observer(web_contents()); + console_observer.SetPattern(kIncognitoWarningPattern); + + // Filter out the main frame host of the prerendered page. + content::RenderFrameHost* prerender_rfh = + prerender_helper_.GetPrerenderedMainFrameHost(host_id); + console_observer.SetFilter(base::BindLambdaForTesting( + [&](const content::WebContentsConsoleObserver::Message& message) { + return message.source_frame == prerender_rfh; + })); + + std::string script_result; + + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + prerender_rfh, "registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + // Use ExecuteScriptAsync because binding of blink::mojom::PushMessaging + // is deferred for the prerendered page. Script execution will finish after + // the activation. + ExecuteScriptAsync(prerender_rfh, "documentSubscribePush()"); + + // Activate the prerendered page and wait for a response of script execution. + content::DOMMessageQueue message_queue; + prerender_helper_.NavigatePrimaryPage(url); + // Make sure that the prerender was activated. + ASSERT_TRUE(prerender_observer.was_activated()); + do { + ASSERT_TRUE(message_queue.WaitForMessage(&script_result)); + } while (script_result != + "\"AbortError - Registration failed - permission denied\""); + + console_observer.Wait(); + EXPECT_EQ(1u, console_observer.messages().size()); +} + +class PushMessagingDisallowSenderIdsBrowserTest + : public PushMessagingBrowserTestBase { + public: + PushMessagingDisallowSenderIdsBrowserTest() { + scoped_feature_list_.InitAndEnableFeature( + features::kPushMessagingDisallowSenderIDs); + } + + ~PushMessagingDisallowSenderIdsBrowserTest() override = default; + + private: + base::test::ScopedFeatureList scoped_feature_list_; +}; + +IN_PROC_BROWSER_TEST_F(PushMessagingDisallowSenderIdsBrowserTest, + SubscriptionWithSenderIdFails) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + LoadTestPage(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Attempt to create a subscription with a GCM Sender ID ("numeric key"), + // which should fail because the kPushMessagingDisallowSenderIDs feature has + // been enabled for this test. + ASSERT_TRUE( + RunScript("documentSubscribePushWithNumericKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - GCM Sender IDs are no longer " + "supported, please upgrade to VAPID authentication instead", + script_result); +} + +class PushSubscriptionWithExpirationTimeTest + : public PushMessagingBrowserTestBase { + public: + PushSubscriptionWithExpirationTimeTest() { + scoped_feature_list_.InitAndEnableFeature( + features::kPushSubscriptionWithExpirationTime); + } + + ~PushSubscriptionWithExpirationTimeTest() override = default; + + // Checks whether |expiration_time| lies in the future and is in the + // valid format (seconds elapsed since Unix time) + bool IsExpirationTimeValid(const std::string& expiration_time); + + private: + base::test::ScopedFeatureList scoped_feature_list_; +}; + +bool PushSubscriptionWithExpirationTimeTest::IsExpirationTimeValid( + const std::string& expiration_time) { + int64_t output; + if (!base::StringToInt64(expiration_time, &output)) + return false; + return base::Time::Now().ToJsTimeIgnoringNull() < output; +} + +IN_PROC_BROWSER_TEST_F(PushSubscriptionWithExpirationTimeTest, + SubscribeGetSubscriptionWithExpirationTime) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + LoadTestPage(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Subscribe with expiration time enabled, should get a subscription with + // expiration time in the future back + std::string subscription_expiration_time; + ASSERT_TRUE(RunScript("documentSubscribePushGetExpirationTime()", + &subscription_expiration_time)); + EXPECT_TRUE(IsExpirationTimeValid(subscription_expiration_time)); + + std::string get_subscription_expiration_time; + // Get subscription should also yield a subscription with expiration time + ASSERT_TRUE(RunScript("GetSubscriptionExpirationTime()", + &get_subscription_expiration_time)); + EXPECT_TRUE(IsExpirationTimeValid(get_subscription_expiration_time)); + // Both methods should return the same expiration time + ASSERT_EQ(subscription_expiration_time, get_subscription_expiration_time); +} + +IN_PROC_BROWSER_TEST_F(PushSubscriptionWithExpirationTimeTest, + GetSubscriptionWithExpirationTime) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + // Get subscription should also yield a subscription with expiration time + ASSERT_TRUE(RunScript("GetSubscriptionExpirationTime()", &script_result)); + EXPECT_TRUE(IsExpirationTimeValid(script_result)); +} + +class PushSubscriptionWithoutExpirationTimeTest + : public PushMessagingBrowserTestBase { + public: + PushSubscriptionWithoutExpirationTimeTest() { + // Override current feature list to ensure having + // |kPushSubscriptionWithExpirationTime| disabled + scoped_feature_list_.InitAndDisableFeature( + features::kPushSubscriptionWithExpirationTime); + } + + ~PushSubscriptionWithoutExpirationTimeTest() override = default; + + private: + base::test::ScopedFeatureList scoped_feature_list_; +}; + +IN_PROC_BROWSER_TEST_F(PushSubscriptionWithoutExpirationTimeTest, + SubscribeDocumentExpirationTimeNull) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + LoadTestPage(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // When |features::kPushSubscriptionWithExpirationTime| is disabled, + // expiration time should be null + ASSERT_TRUE( + RunScript("documentSubscribePushGetExpirationTime()", &script_result)); + EXPECT_EQ("null", script_result); +} + +class PushSubscriptionChangeEventTest : public PushMessagingBrowserTestBase { + public: + PushSubscriptionChangeEventTest() { + scoped_feature_list_.InitWithFeatures( + {features::kPushSubscriptionChangeEvent, + features::kPushSubscriptionWithExpirationTime}, + {}); + } + + ~PushSubscriptionChangeEventTest() override = default; + + private: + base::test::ScopedFeatureList scoped_feature_list_; +}; + +IN_PROC_BROWSER_TEST_F(PushSubscriptionChangeEventTest, + PushSubscriptionChangeEventSuccess) { + std::string script_result; + + // Create the |old_subscription| by subscribing and unsubscribing again + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + blink::mojom::PushSubscriptionPtr old_subscription = + GetSubscriptionForAppIdentifier(app_identifier); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + + // There should be no subscription since we unsubscribed + EXPECT_EQ(PushMessagingAppIdentifier::GetCount(GetBrowser()->profile()), 0u); + + // Create a |new_subscription| by resubscribing + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + app_identifier = GetAppIdentifierForServiceWorkerRegistration(0LL); + + blink::mojom::PushSubscriptionPtr new_subscription = + GetSubscriptionForAppIdentifier(app_identifier); + + // Save the endpoints to compare with the JS result + GURL old_endpoint = old_subscription->endpoint; + GURL new_endpoint = new_subscription->endpoint; + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("false - is not controlled", script_result); + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + base::RunLoop run_loop; + push_service()->FirePushSubscriptionChange( + app_identifier, run_loop.QuitClosure(), std::move(new_subscription), + std::move(old_subscription)); + run_loop.Run(); + + // Compare old subscription + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result)); + EXPECT_EQ(old_endpoint.spec(), script_result); + // Compare new subscription + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result)); + EXPECT_EQ(new_endpoint.spec(), script_result); + + // Check that we record this case in UMA. + histogram_tester_.ExpectUniqueSample( + "PushMessaging.PushSubscriptionChangeStatus", + blink::mojom::PushEventStatus::SUCCESS, 1); +} + +IN_PROC_BROWSER_TEST_F(PushSubscriptionChangeEventTest, + FiredAfterPermissionRevoked) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("false - is not controlled", script_result); + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + auto old_subscription = GetSubscriptionForAppIdentifier(app_identifier); + + base::RunLoop run_loop; + push_service()->SetContentSettingChangedCallbackForTesting( + run_loop.QuitClosure()); + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->SetContentSettingDefaultScope(app_identifier.origin(), GURL(), + ContentSettingsType::NOTIFICATIONS, + CONTENT_SETTING_BLOCK); + run_loop.Run(); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - denied", script_result); + + // Check if the pushsubscriptionchangeevent arrived in the document and + // whether the |old_subscription| has the expected endpoint and + // |new_subscription| is null + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result)); + EXPECT_EQ(old_subscription->endpoint.spec(), script_result); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result)); + EXPECT_EQ("null", script_result); + + // Check that we record this case in UMA. + histogram_tester_.ExpectUniqueSample( + "PushMessaging.PushSubscriptionChangeStatus", + blink::mojom::PushEventStatus::SUCCESS, 1); +} + +IN_PROC_BROWSER_TEST_F(PushSubscriptionChangeEventTest, OnInvalidation) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("false - is not controlled", script_result); + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + ASSERT_FALSE(app_identifier.is_null()); + + base::RunLoop run_loop; + push_service()->SetInvalidationCallbackForTesting(run_loop.QuitClosure()); + push_service()->OnSubscriptionInvalidation(app_identifier.app_id()); + run_loop.Run(); + + // Old subscription should be gone + PushMessagingAppIdentifier deleted_identifier = + PushMessagingAppIdentifier::FindByAppId(GetBrowser()->profile(), + app_identifier.app_id()); + EXPECT_TRUE(deleted_identifier.is_null()); + + // New subscription with a different app id should exist + PushMessagingAppIdentifier new_identifier = + PushMessagingAppIdentifier::FindByServiceWorker( + GetBrowser()->profile(), app_identifier.origin(), + app_identifier.service_worker_registration_id()); + EXPECT_FALSE(new_identifier.is_null()); + + base::RunLoop().RunUntilIdle(); + + // Expect `pushsubscriptionchange` event that is not null + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result)); + EXPECT_NE("null", script_result); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result)); + EXPECT_NE("null", script_result); + + // Check that we record this case in UMA. + histogram_tester_.ExpectUniqueSample( + "PushMessaging.PushSubscriptionChangeStatus", + blink::mojom::PushEventStatus::SUCCESS, 1); +} diff --git a/chromium/chrome/browser/push_messaging/push_messaging_constants.cc b/chromium/chrome/browser/push_messaging/push_messaging_constants.cc new file mode 100644 index 00000000000..bd0d33d6134 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_constants.cc @@ -0,0 +1,11 @@ +// 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 "chrome/browser/push_messaging/push_messaging_constants.h" + +const char kPushMessagingGcmEndpoint[] = + "https://fcm.googleapis.com/fcm/send/"; + +const char kPushMessagingForcedNotificationTag[] = + "user_visible_auto_notification"; diff --git a/chromium/chrome/browser/push_messaging/push_messaging_constants.h b/chromium/chrome/browser/push_messaging/push_messaging_constants.h new file mode 100644 index 00000000000..cf0480a5f14 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_constants.h @@ -0,0 +1,25 @@ +// 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 CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_CONSTANTS_H_ +#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_CONSTANTS_H_ + +#include "base/time/time.h" + +extern const char kPushMessagingGcmEndpoint[]; + +// The tag of the notification that will be automatically shown if a webapp +// receives a push message then fails to show a notification. +extern const char kPushMessagingForcedNotificationTag[]; + +// Chrome decided cadence on subscription refreshes. According to the standards: +// https://w3c.github.io/push-api/#dfn-subscription-expiration-time it is +// optional and set by the browser. +constexpr base::TimeDelta kPushSubscriptionExpirationPeriodTimeDelta = + base::Days(90); + +// TimeDelta for subscription refreshes to keep old subscriptions alive +constexpr base::TimeDelta kPushSubscriptionRefreshTimeDelta = base::Minutes(2); + +#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_CONSTANTS_H_ diff --git a/chromium/chrome/browser/push_messaging/push_messaging_features.cc b/chromium/chrome/browser/push_messaging/push_messaging_features.cc new file mode 100644 index 00000000000..11ad3a97bb5 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_features.cc @@ -0,0 +1,15 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/push_messaging/push_messaging_features.h" + +namespace features { + +const base::Feature kPushMessagingDisallowSenderIDs{ + "PushMessagingDisallowSenderIDs", base::FEATURE_DISABLED_BY_DEFAULT}; + +const base::Feature kPushSubscriptionWithExpirationTime{ + "PushSubscriptionWithExpirationTime", base::FEATURE_DISABLED_BY_DEFAULT}; + +} // namespace features diff --git a/chromium/chrome/browser/push_messaging/push_messaging_features.h b/chromium/chrome/browser/push_messaging/push_messaging_features.h new file mode 100644 index 00000000000..1bdc9237860 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_features.h @@ -0,0 +1,21 @@ +// Copyright 2020 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 CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_FEATURES_H_ +#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_FEATURES_H_ + +#include "base/feature_list.h" + +namespace features { + +// Feature flag to disallow creation of push messages with GCM Sender IDs. +extern const base::Feature kPushMessagingDisallowSenderIDs; + +// Feature flag to enable push subscription with expiration times specified in +// /chrome/browser/push_messaging/push_messaging_constants.h +extern const base::Feature kPushSubscriptionWithExpirationTime; + +} // namespace features + +#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_FEATURES_H_ diff --git a/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.cc b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.cc new file mode 100644 index 00000000000..e2a8f2adc42 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.cc @@ -0,0 +1,353 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/push_messaging/push_messaging_notification_manager.h" + +#include + +#include +#include + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/feature_list.h" +#include "base/metrics/histogram_functions.h" +#include "base/metrics/histogram_macros.h" +#include "base/strings/utf_string_conversions.h" +#include "base/task/post_task.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/notifications/platform_notification_service_factory.h" +#include "chrome/browser/notifications/platform_notification_service_impl.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/push_messaging/push_messaging_constants.h" +#include "chrome/grit/generated_resources.h" +#include "components/site_engagement/content/site_engagement_service.h" +#include "components/url_formatter/elide_url.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/platform_notification_context.h" +#include "content/public/browser/push_messaging_service.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/storage_partition.h" +#include "content/public/browser/web_contents.h" +#include "content/public/common/content_features.h" +#include "content/public/common/page_visibility_state.h" +#include "content/public/common/url_constants.h" +#include "net/base/registry_controlled_domains/registry_controlled_domain.h" +#include "third_party/blink/public/common/notifications/notification_resources.h" +#include "third_party/blink/public/mojom/notifications/notification.mojom-shared.h" +#include "third_party/blink/public/mojom/push_messaging/push_messaging_status.mojom.h" +#include "ui/base/l10n/l10n_util.h" +#include "url/gurl.h" + +#if defined(OS_ANDROID) +#include "chrome/browser/ui/android/tab_model/tab_model.h" +#include "chrome/browser/ui/android/tab_model/tab_model_list.h" +#else +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_list.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#endif + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "chrome/browser/ash/android_sms/android_sms_service_factory.h" +#include "chrome/browser/ash/android_sms/android_sms_urls.h" +#include "chrome/browser/ash/multidevice_setup/multidevice_setup_client_factory.h" +#endif + +using content::BrowserThread; +using content::NotificationDatabaseData; +using content::PlatformNotificationContext; +using content::PushMessagingService; +using content::ServiceWorkerContext; +using content::WebContents; + +namespace { +void RecordUserVisibleStatus(blink::mojom::PushUserVisibleStatus status) { + UMA_HISTOGRAM_ENUMERATION("PushMessaging.UserVisibleStatus", status); +} + +content::StoragePartition* GetStoragePartition(Profile* profile, + const GURL& origin) { + return profile->GetStoragePartitionForUrl(origin); +} + +NotificationDatabaseData CreateDatabaseData( + const GURL& origin, + int64_t service_worker_registration_id) { + blink::PlatformNotificationData notification_data; + notification_data.title = url_formatter::FormatUrlForSecurityDisplay( + origin, url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS); + notification_data.direction = + blink::mojom::NotificationDirection::LEFT_TO_RIGHT; + notification_data.body = + l10n_util::GetStringUTF16(IDS_PUSH_MESSAGING_GENERIC_NOTIFICATION_BODY); + notification_data.tag = kPushMessagingForcedNotificationTag; + notification_data.icon = GURL(); + notification_data.timestamp = base::Time::Now(); + notification_data.silent = true; + + NotificationDatabaseData database_data; + database_data.origin = origin; + database_data.service_worker_registration_id = service_worker_registration_id; + database_data.notification_data = notification_data; + + // Make sure we don't expose this notification to the site. + database_data.is_shown_by_browser = true; + + return database_data; +} + +} // namespace + +PushMessagingNotificationManager::PushMessagingNotificationManager( + Profile* profile) + : profile_(profile), budget_database_(profile) {} + +PushMessagingNotificationManager::~PushMessagingNotificationManager() = default; + +void PushMessagingNotificationManager::EnforceUserVisibleOnlyRequirements( + const GURL& origin, + int64_t service_worker_registration_id, + EnforceRequirementsCallback message_handled_callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + +#if BUILDFLAG(IS_CHROMEOS_ASH) + if (ShouldSkipUserVisibleOnlyRequirements(origin)) { + std::move(message_handled_callback) + .Run(/* did_show_generic_notification= */ false); + return; + } +#endif + + // TODO(johnme): Relax this heuristic slightly. + scoped_refptr notification_context = + GetStoragePartition(profile_, origin)->GetPlatformNotificationContext(); + + notification_context->CountVisibleNotificationsForServiceWorkerRegistration( + origin, service_worker_registration_id, + base::BindOnce( + &PushMessagingNotificationManager::DidCountVisibleNotifications, + weak_factory_.GetWeakPtr(), origin, service_worker_registration_id, + std::move(message_handled_callback))); +} + +void PushMessagingNotificationManager::DidCountVisibleNotifications( + const GURL& origin, + int64_t service_worker_registration_id, + EnforceRequirementsCallback message_handled_callback, + bool success, + int notification_count) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + // TODO(johnme): Hiding an existing notification should also count as a useful + // user-visible action done in response to a push message - but make sure that + // sending two messages in rapid succession which show then hide a + // notification doesn't count. + // TODO(crbug.com/891339): Scheduling a notification should count as a + // user-visible action, if it is not immediately cancelled or the |origin| + // schedules too many notifications too far in the future. + bool notification_shown = notification_count > 0; + bool notification_needed = true; + + base::UmaHistogramCounts100("PushMessaging.VisibleNotificationCount", + notification_count); + + // Sites with a currently visible tab don't need to show notifications. +#if defined(OS_ANDROID) + for (const TabModel* model : TabModelList::models()) { + Profile* profile = model->GetProfile(); + WebContents* active_web_contents = model->GetActiveWebContents(); +#else + for (auto* browser : *BrowserList::GetInstance()) { + Profile* profile = browser->profile(); + WebContents* active_web_contents = + browser->tab_strip_model()->GetActiveWebContents(); +#endif + if (IsTabVisible(profile, active_web_contents, origin)) { + notification_needed = false; + break; + } + } + + // If more than one notification is showing for this Service Worker, close + // the default notification if it happens to be part of this group. + if (notification_count >= 2) { + scoped_refptr notification_context = + GetStoragePartition(profile_, origin)->GetPlatformNotificationContext(); + notification_context->DeleteAllNotificationDataWithTag( + kPushMessagingForcedNotificationTag, /*is_shown_by_browser=*/true, + origin, base::DoNothing()); + } + + if (notification_needed && !notification_shown) { + // If the worker needed to show a notification and didn't, see if a silent + // push was allowed. + budget_database_.SpendBudget( + url::Origin::Create(origin), + base::BindOnce(&PushMessagingNotificationManager::ProcessSilentPush, + weak_factory_.GetWeakPtr(), origin, + service_worker_registration_id, + std::move(message_handled_callback))); + return; + } + + if (notification_needed && notification_shown) { + RecordUserVisibleStatus( + blink::mojom::PushUserVisibleStatus::REQUIRED_AND_SHOWN); + } else if (!notification_needed && !notification_shown) { + RecordUserVisibleStatus( + blink::mojom::PushUserVisibleStatus::NOT_REQUIRED_AND_NOT_SHOWN); + } else { + RecordUserVisibleStatus( + blink::mojom::PushUserVisibleStatus::NOT_REQUIRED_BUT_SHOWN); + } + + std::move(message_handled_callback) + .Run(/* did_show_generic_notification= */ false); +} + +bool PushMessagingNotificationManager::IsTabVisible( + Profile* profile, + WebContents* active_web_contents, + const GURL& origin) { + if (!active_web_contents || !active_web_contents->GetMainFrame()) + return false; + + // Don't leak information from other profiles. + if (profile != profile_) + return false; + + // Ignore minimized windows. + switch (active_web_contents->GetMainFrame()->GetVisibilityState()) { + case content::PageVisibilityState::kHidden: + case content::PageVisibilityState::kHiddenButPainting: + return false; + case content::PageVisibilityState::kVisible: + break; + } + + // Use the visible URL since that's the one the user is aware of (and it + // doesn't matter whether the page loaded successfully). + GURL visible_url = active_web_contents->GetVisibleURL(); + + // view-source: pages are considered to be controlled Service Worker clients + // and thus should be considered when checking the visible URL. However, the + // prefix has to be removed before the origins can be compared. + if (visible_url.SchemeIs(content::kViewSourceScheme)) + visible_url = GURL(visible_url.GetContent()); + + return visible_url.DeprecatedGetOriginAsURL() == origin; +} + +void PushMessagingNotificationManager::ProcessSilentPush( + const GURL& origin, + int64_t service_worker_registration_id, + EnforceRequirementsCallback message_handled_callback, + bool silent_push_allowed) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + // If the origin was allowed to issue a silent push, just return. + if (silent_push_allowed) { + RecordUserVisibleStatus( + blink::mojom::PushUserVisibleStatus::REQUIRED_BUT_NOT_SHOWN_USED_GRACE); + std::move(message_handled_callback) + .Run(/* did_show_generic_notification= */ false); + return; + } + + RecordUserVisibleStatus(blink::mojom::PushUserVisibleStatus:: + REQUIRED_BUT_NOT_SHOWN_GRACE_EXCEEDED); + + // The site failed to show a notification when one was needed, and they don't + // have enough budget to cover the cost of suppressing, so we will show a + // generic notification. + NotificationDatabaseData database_data = + CreateDatabaseData(origin, service_worker_registration_id); + scoped_refptr notification_context = + GetStoragePartition(profile_, origin)->GetPlatformNotificationContext(); + int64_t next_persistent_notification_id = + PlatformNotificationServiceFactory::GetForProfile(profile_) + ->ReadNextPersistentNotificationId(); + + notification_context->WriteNotificationData( + next_persistent_notification_id, service_worker_registration_id, origin, + database_data, + base::BindOnce( + &PushMessagingNotificationManager::DidWriteNotificationData, + weak_factory_.GetWeakPtr(), std::move(message_handled_callback))); +} + +void PushMessagingNotificationManager::DidWriteNotificationData( + EnforceRequirementsCallback message_handled_callback, + bool success, + const std::string& notification_id) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + if (!success) + DLOG(ERROR) << "Writing forced notification to database should not fail"; + + std::move(message_handled_callback) + .Run(/* did_show_generic_notification= */ true); +} + +#if BUILDFLAG(IS_CHROMEOS_ASH) +bool PushMessagingNotificationManager::ShouldSkipUserVisibleOnlyRequirements( + const GURL& origin) { + // This is a short-term exception to user visible only enforcement added + // to support for "Messages for Web" integration on ChromeOS. + + chromeos::multidevice_setup::MultiDeviceSetupClient* multidevice_setup_client; + if (test_multidevice_setup_client_) { + multidevice_setup_client = test_multidevice_setup_client_; + } else { + multidevice_setup_client = + ash::multidevice_setup::MultiDeviceSetupClientFactory::GetForProfile( + profile_); + } + + if (!multidevice_setup_client) + return false; + + // Check if messages feature is enabled + if (multidevice_setup_client->GetFeatureState( + chromeos::multidevice_setup::mojom::Feature::kMessages) != + chromeos::multidevice_setup::mojom::FeatureState::kEnabledByUser) { + return false; + } + + ash::android_sms::AndroidSmsAppManager* android_sms_app_manager; + if (test_android_sms_app_manager_) { + android_sms_app_manager = test_android_sms_app_manager_; + } else { + auto* android_sms_service = + ash::android_sms::AndroidSmsServiceFactory::GetForBrowserContext( + profile_); + if (!android_sms_service) + return false; + android_sms_app_manager = android_sms_service->android_sms_app_manager(); + } + + // Check if origin matches current messages url + absl::optional app_url = android_sms_app_manager->GetCurrentAppUrl(); + if (!app_url) + app_url = ash::android_sms::GetAndroidMessagesURL(); + + if (!origin.EqualsIgnoringRef(app_url->DeprecatedGetOriginAsURL())) + return false; + + return true; +} + +void PushMessagingNotificationManager::SetTestMultiDeviceSetupClient( + chromeos::multidevice_setup::MultiDeviceSetupClient* + multidevice_setup_client) { + test_multidevice_setup_client_ = multidevice_setup_client; +} + +void PushMessagingNotificationManager::SetTestAndroidSmsAppManager( + ash::android_sms::AndroidSmsAppManager* android_sms_app_manager) { + test_android_sms_app_manager_ = android_sms_app_manager; +} +#endif diff --git a/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.h b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.h new file mode 100644 index 00000000000..b2621f55c53 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.h @@ -0,0 +1,122 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_NOTIFICATION_MANAGER_H_ +#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_NOTIFICATION_MANAGER_H_ + +#include +#include + +#include "base/callback_forward.h" +#include "base/gtest_prod_util.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/push_messaging/budget_database.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "chrome/browser/ash/android_sms/android_sms_app_manager.h" +#include "chromeos/services/multidevice_setup/public/cpp/multidevice_setup_client.h" +#endif + +class GURL; +class Profile; + +namespace content { +class WebContents; +} // namespace content + +// Developers may be required to display a Web Notification in response to an +// incoming push message in order to clarify to the user that something has +// happened in the background. When they forget to do so, a default notification +// has to be displayed on their behalf. +// +// This class implements the heuristics for determining whether the default +// notification is necessary, as well as the functionality of displaying the +// default notification when it is. +// +// See the following document and bug for more context: +// https://docs.google.com/document/d/13VxFdLJbMwxHrvnpDm8RXnU41W2ZlcP0mdWWe9zXQT8/edit +// https://crbug.com/437277 +class PushMessagingNotificationManager { + public: + using EnforceRequirementsCallback = + base::OnceCallback; + + explicit PushMessagingNotificationManager(Profile* profile); + + PushMessagingNotificationManager(const PushMessagingNotificationManager&) = + delete; + PushMessagingNotificationManager& operator=( + const PushMessagingNotificationManager&) = delete; + + ~PushMessagingNotificationManager(); + + // Enforces the requirements implied for push subscriptions which must display + // a Web Notification in response to an incoming message. + void EnforceUserVisibleOnlyRequirements( + const GURL& origin, + int64_t service_worker_registration_id, + EnforceRequirementsCallback message_handled_callback); + + private: + FRIEND_TEST_ALL_PREFIXES(PushMessagingNotificationManagerTest, IsTabVisible); + FRIEND_TEST_ALL_PREFIXES(PushMessagingNotificationManagerTest, + IsTabVisibleViewSource); + FRIEND_TEST_ALL_PREFIXES( + PushMessagingNotificationManagerTest, + SkipEnforceUserVisibleOnlyRequirementsForAndroidMessages); + + void DidCountVisibleNotifications( + const GURL& origin, + int64_t service_worker_registration_id, + EnforceRequirementsCallback message_handled_callback, + bool success, + int notification_count); + + // Checks whether |profile| is the one owning this instance, + // |active_web_contents| exists and its main frame is visible, and the URL + // currently visible to the user is for |origin|. + bool IsTabVisible(Profile* profile, + content::WebContents* active_web_contents, + const GURL& origin); + + void ProcessSilentPush(const GURL& origin, + int64_t service_worker_registration_id, + EnforceRequirementsCallback message_handled_callback, + bool silent_push_allowed); + + void DidWriteNotificationData( + EnforceRequirementsCallback message_handled_callback, + bool success, + const std::string& notification_id); + +#if BUILDFLAG(IS_CHROMEOS_ASH) + bool ShouldSkipUserVisibleOnlyRequirements(const GURL& origin); + + void SetTestMultiDeviceSetupClient( + chromeos::multidevice_setup::MultiDeviceSetupClient* + multidevice_setup_client); + + void SetTestAndroidSmsAppManager( + ash::android_sms::AndroidSmsAppManager* android_sms_app_manager); +#endif + + // Weak. This manager is owned by a keyed service on this profile. + raw_ptr profile_; + + BudgetDatabase budget_database_; + +#if BUILDFLAG(IS_CHROMEOS_ASH) + chromeos::multidevice_setup::MultiDeviceSetupClient* + test_multidevice_setup_client_ = nullptr; + + ash::android_sms::AndroidSmsAppManager* test_android_sms_app_manager_ = + nullptr; +#endif + + base::WeakPtrFactory weak_factory_{this}; +}; + +#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_NOTIFICATION_MANAGER_H_ diff --git a/chromium/chrome/browser/push_messaging/push_messaging_notification_manager_unittest.cc b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager_unittest.cc new file mode 100644 index 00000000000..7b6b46680f5 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager_unittest.cc @@ -0,0 +1,87 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/push_messaging/push_messaging_notification_manager.h" + +#include "base/bind.h" +#include "build/chromeos_buildflags.h" +#include "chrome/test/base/chrome_render_view_host_test_harness.h" +#include "chrome/test/base/testing_profile.h" +#include "content/public/browser/web_contents.h" +#include "content/public/test/test_renderer_host.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include + +#include "chrome/browser/ash/android_sms/fake_android_sms_app_manager.h" +#include "chromeos/services/multidevice_setup/public/cpp/fake_multidevice_setup_client.h" +#endif + +class PushMessagingNotificationManagerTest + : public ChromeRenderViewHostTestHarness {}; + +TEST_F(PushMessagingNotificationManagerTest, IsTabVisible) { + PushMessagingNotificationManager manager(profile()); + GURL origin("https://google.com/"); + GURL origin_with_path = origin.Resolve("/path/"); + NavigateAndCommit(origin_with_path); + + EXPECT_FALSE(manager.IsTabVisible(profile(), nullptr, origin)); + EXPECT_FALSE(manager.IsTabVisible(profile(), web_contents(), + GURL("https://chrome.com/"))); + EXPECT_TRUE(manager.IsTabVisible(profile(), web_contents(), origin)); + + content::RenderViewHostTester::For(rvh())->SimulateWasHidden(); + EXPECT_FALSE(manager.IsTabVisible(profile(), web_contents(), origin)); + + content::RenderViewHostTester::For(rvh())->SimulateWasShown(); + EXPECT_TRUE(manager.IsTabVisible(profile(), web_contents(), origin)); +} + +TEST_F(PushMessagingNotificationManagerTest, IsTabVisibleViewSource) { + PushMessagingNotificationManager manager(profile()); + + GURL origin("https://google.com/"); + GURL view_source_page("view-source:https://google.com/path/"); + + NavigateAndCommit(view_source_page); + + ASSERT_EQ(view_source_page, web_contents()->GetVisibleURL()); + EXPECT_TRUE(manager.IsTabVisible(profile(), web_contents(), origin)); + + content::RenderViewHostTester::For(rvh())->SimulateWasHidden(); + EXPECT_FALSE(manager.IsTabVisible(profile(), web_contents(), origin)); +} + +#if BUILDFLAG(IS_CHROMEOS_ASH) +TEST_F(PushMessagingNotificationManagerTest, + SkipEnforceUserVisibleOnlyRequirementsForAndroidMessages) { + GURL app_url("https://example.com/test/"); + auto fake_android_sms_app_manager = + std::make_unique(); + fake_android_sms_app_manager->SetInstalledAppUrl(app_url); + + auto fake_multidevice_setup_client = std::make_unique< + chromeos::multidevice_setup::FakeMultiDeviceSetupClient>(); + fake_multidevice_setup_client->SetFeatureState( + chromeos::multidevice_setup::mojom::Feature::kMessages, + chromeos::multidevice_setup::mojom::FeatureState::kEnabledByUser); + + PushMessagingNotificationManager manager(profile()); + manager.SetTestMultiDeviceSetupClient(fake_multidevice_setup_client.get()); + manager.SetTestAndroidSmsAppManager(fake_android_sms_app_manager.get()); + + bool was_called = false; + manager.EnforceUserVisibleOnlyRequirements( + app_url.DeprecatedGetOriginAsURL(), 0l, + base::BindOnce( + [](bool* was_called, bool did_show_generic_notification) { + *was_called = true; + }, + &was_called)); + EXPECT_TRUE(was_called); +} +#endif diff --git a/chromium/chrome/browser/push_messaging/push_messaging_refresher.cc b/chromium/chrome/browser/push_messaging/push_messaging_refresher.cc new file mode 100644 index 00000000000..2f8bdee4b43 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_refresher.cc @@ -0,0 +1,121 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/push_messaging/push_messaging_refresher.h" + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/feature_list.h" +#include "chrome/browser/push_messaging/push_messaging_app_identifier.h" +#include "chrome/browser/push_messaging/push_messaging_constants.h" +#include "chrome/browser/push_messaging/push_messaging_service_impl.h" +#include "chrome/browser/push_messaging/push_messaging_utils.h" +#include "chrome/common/pref_names.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/push_messaging_service.h" +#include "content/public/common/content_features.h" +#include "third_party/blink/public/mojom/push_messaging/push_messaging_status.mojom.h" +#include "url/gurl.h" + +PushMessagingRefresher::PushMessagingRefresher() = default; + +PushMessagingRefresher::~PushMessagingRefresher() = default; + +size_t PushMessagingRefresher::GetCount() const { + return old_subscriptions_.size(); +} + +void PushMessagingRefresher::Refresh( + PushMessagingAppIdentifier old_app_identifier, + const std::string& new_app_id, + const std::string& sender_id) { + RefreshObject refresh_object = {old_app_identifier, sender_id, + false /* is_valid */}; + // Insert is as current started refresh + old_subscriptions_.emplace(new_app_id, refresh_object); + refresh_map_.emplace(old_app_identifier.app_id(), new_app_id); + // TODO(viviy): Save old_subscription in a seperate map in preferences, so + // that in case of a browser shutdown the subscription is remembered. + // Unsubscribe on next startup. +} + +void PushMessagingRefresher::OnSubscriptionUpdated( + const std::string& new_app_id) { + RefreshInfo::iterator result = old_subscriptions_.find(new_app_id); + + if (result == old_subscriptions_.end()) + return; + + // Schedule a unsubscription event for the old subscription + base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, + base::BindOnce(&PushMessagingRefresher::NotifyOnOldSubscriptionExpired, + weak_factory_.GetWeakPtr(), + result->second.old_identifier.app_id(), + result->second.sender_id), + kPushSubscriptionRefreshTimeDelta); +} + +void PushMessagingRefresher::NotifyOnOldSubscriptionExpired( + const std::string& old_app_id, + const std::string& sender_id) { + for (Observer& obs : observers_) + obs.OnOldSubscriptionExpired(old_app_id, sender_id); +} + +void PushMessagingRefresher::OnUnsubscribed(const std::string& old_app_id) { + auto found_new_app_id = refresh_map_.find(old_app_id); + // Already unsubscribed + if (found_new_app_id == refresh_map_.end()) + return; + + std::string new_app_id = found_new_app_id->second; + refresh_map_.erase(found_new_app_id); + + RefreshInfo::iterator result = old_subscriptions_.find(new_app_id); + DCHECK(result != old_subscriptions_.end()); + + PushMessagingAppIdentifier old_identifier = result->second.old_identifier; + old_subscriptions_.erase(result); + + for (Observer& obs : observers_) + obs.OnRefreshFinished(old_identifier); +} + +void PushMessagingRefresher::GotMessageFrom(const std::string& app_id) { + RefreshInfo::iterator result = old_subscriptions_.find(app_id); + // If a message arrives that is part of the refresh, expire the old + // subscription immediately + if (result != old_subscriptions_.end() && !result->second.is_valid) { + NotifyOnOldSubscriptionExpired(result->second.old_identifier.app_id(), + result->second.sender_id); + result->second.is_valid = true; + } +} + +absl::optional +PushMessagingRefresher::FindActiveAppIdentifier(const std::string& app_id) { + absl::optional app_identifier; + RefreshMap::iterator refresh_map_it = refresh_map_.find(app_id); + if (refresh_map_it != refresh_map_.end()) { + RefreshInfo::iterator result = + old_subscriptions_.find(refresh_map_it->second); + if (result != old_subscriptions_.end() && !result->second.is_valid) { + app_identifier = result->second.old_identifier; + } + } + return app_identifier; +} + +base::WeakPtr PushMessagingRefresher::GetWeakPtr() { + return weak_factory_.GetWeakPtr(); +} + +void PushMessagingRefresher::AddObserver(Observer* observer) { + observers_.AddObserver(observer); +} + +void PushMessagingRefresher::RemoveObserver(Observer* observer) { + observers_.RemoveObserver(observer); +} diff --git a/chromium/chrome/browser/push_messaging/push_messaging_refresher.h b/chromium/chrome/browser/push_messaging/push_messaging_refresher.h new file mode 100644 index 00000000000..c049a14388e --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_refresher.h @@ -0,0 +1,99 @@ +// Copyright 2020 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 CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_REFRESHER_H_ +#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_REFRESHER_H_ + +#include +#include + +#include "base/memory/weak_ptr.h" +#include "base/observer_list.h" +#include "base/observer_list_types.h" +#include "chrome/browser/push_messaging/push_messaging_app_identifier.h" +#include "content/public/browser/push_messaging_service.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#include "third_party/blink/public/mojom/push_messaging/push_messaging.mojom-forward.h" + +// This class enables push subscription refreshes as defined in the docs: +// https://w3c.github.io/push-api/#subscription-refreshes +// The idea is to keep the refresh information of both new and old subscription +// in memory during the refresh process to be still able to receive messages +// through the old subscription after it was replaced by the new subscription. +class PushMessagingRefresher { + public: + PushMessagingRefresher(); + + PushMessagingRefresher(const PushMessagingRefresher&) = delete; + PushMessagingRefresher& operator=(const PushMessagingRefresher&) = delete; + + ~PushMessagingRefresher(); + + // Return number of objects that are currently being refreshed + size_t GetCount() const; + + // Register a new refresh pair with relevant information. + void Refresh(PushMessagingAppIdentifier old_app_identifier, + const std::string& new_app_id, + const std::string& sender_id); + + // The subscription with the new app id was updated, new messages arriving + // through the new subscription should be accepted now. + void OnSubscriptionUpdated(const std::string& new_app_id); + + // Unsubscribe event happened for the old subscription. It is deleted in + // the RefreshMap and notify all observers that the refresh process for + // |app_id| has finished + void OnUnsubscribed(const std::string& app_id); + + // If a new message arrives through an |app_id| that is associated with a + // refresh, the old subscription needs to be deactivated. + void GotMessageFrom(const std::string& app_id); + + // If a subscription was refreshed, we accept the old subscription for + // a moment after refresh + absl::optional FindActiveAppIdentifier( + const std::string& app_id); + + base::WeakPtr GetWeakPtr(); + + // Observer for Refresh status updates + class Observer : public base::CheckedObserver { + public: + virtual void OnOldSubscriptionExpired(const std::string& app_id, + const std::string& sender_id) = 0; + virtual void OnRefreshFinished( + const PushMessagingAppIdentifier& app_identifier) = 0; + }; + + void AddObserver(Observer* observer); + void RemoveObserver(Observer* observer); + + private: + // A RefreshObject carries subscription information that is needed to receive + // messages and to unsubscribe from the old subscription + struct RefreshObject { + PushMessagingAppIdentifier old_identifier; + std::string sender_id; + bool is_valid; + }; + + void NotifyOnOldSubscriptionExpired(const std::string& app_id, + const std::string& sender_id); + + base::ObserverList observers_; + + // Maps from new app id to the refresh information of the old subscription + // that is needed to receive messages and unsubscribe + using RefreshInfo = std::map; + RefreshInfo old_subscriptions_; + + // Maps from old app id to new app id + using RefreshMap = std::map; + RefreshMap refresh_map_; + + base::WeakPtrFactory weak_factory_{this}; +}; + +#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_REFRESHER_H_ diff --git a/chromium/chrome/browser/push_messaging/push_messaging_refresher_unittest.cc b/chromium/chrome/browser/push_messaging/push_messaging_refresher_unittest.cc new file mode 100644 index 00000000000..63570f41e83 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_refresher_unittest.cc @@ -0,0 +1,84 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/push_messaging/push_messaging_refresher.h" + +#include + +#include "base/time/time.h" +#include "chrome/browser/push_messaging/push_messaging_app_identifier.h" +#include "chrome/browser/push_messaging/push_messaging_refresher.h" +#include "chrome/test/base/testing_profile.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#include "url/gurl.h" +namespace { + +void ExpectAppIdentifiersEqual(const PushMessagingAppIdentifier& a, + const PushMessagingAppIdentifier& b) { + EXPECT_EQ(a.app_id(), b.app_id()); + EXPECT_EQ(a.origin(), b.origin()); + EXPECT_EQ(a.service_worker_registration_id(), + b.service_worker_registration_id()); + EXPECT_EQ(a.expiration_time(), b.expiration_time()); +} + +constexpr char kTestOrigin[] = "https://example.com"; +constexpr char kTestSenderId[] = "1234567890"; +const int64_t kTestServiceWorkerId = 42; + +class PushMessagingRefresherTest : public testing::Test { + protected: + void SetUp() override { + old_app_identifier_ = PushMessagingAppIdentifier::Generate( + GURL(kTestOrigin), kTestServiceWorkerId); + new_app_identifier_ = PushMessagingAppIdentifier::Generate( + GURL(kTestOrigin), kTestServiceWorkerId); + } + + Profile* profile() { return &profile_; } + + PushMessagingRefresher* refresher() { return &refresher_; } + + absl::optional old_app_identifier_; + absl::optional new_app_identifier_; + + private: + content::BrowserTaskEnvironment task_environment_; + TestingProfile profile_; + PushMessagingRefresher refresher_; +}; + +TEST_F(PushMessagingRefresherTest, GotMessageThroughNewSubscription) { + refresher()->Refresh(old_app_identifier_.value(), + new_app_identifier_.value().app_id(), kTestSenderId); + refresher()->GotMessageFrom(new_app_identifier_.value().app_id()); + auto app_identifier = refresher()->FindActiveAppIdentifier( + old_app_identifier_.value().app_id()); + EXPECT_FALSE(app_identifier.has_value()); +} + +TEST_F(PushMessagingRefresherTest, LookupOldSubscription) { + refresher()->Refresh(old_app_identifier_.value(), + new_app_identifier_.value().app_id(), kTestSenderId); + { + absl::optional found_old_app_identifier = + refresher()->FindActiveAppIdentifier( + old_app_identifier_.value().app_id()); + EXPECT_TRUE(found_old_app_identifier.has_value()); + ExpectAppIdentifiersEqual(old_app_identifier_.value(), + found_old_app_identifier.value()); + } + refresher()->OnUnsubscribed(old_app_identifier_.value().app_id()); + { + absl::optional found_after_unsubscribe = + refresher()->FindActiveAppIdentifier( + old_app_identifier_.value().app_id()); + EXPECT_FALSE(found_after_unsubscribe.has_value()); + } + EXPECT_EQ(0u, refresher()->GetCount()); +} + +} // namespace diff --git a/chromium/chrome/browser/push_messaging/push_messaging_service_factory.cc b/chromium/chrome/browser/push_messaging/push_messaging_service_factory.cc new file mode 100644 index 00000000000..2987c24efdf --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_service_factory.cc @@ -0,0 +1,82 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/push_messaging/push_messaging_service_factory.h" + +#include + +#include "base/bind.h" +#include "base/memory/ptr_util.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/content_settings/host_content_settings_map_factory.h" +#include "chrome/browser/engagement/site_engagement_service_factory.h" +#include "chrome/browser/gcm/gcm_profile_service_factory.h" +#include "chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h" +#include "chrome/browser/permissions/permission_manager_factory.h" +#include "chrome/browser/profiles/incognito_helpers.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/push_messaging/push_messaging_service_impl.h" +#include "components/gcm_driver/instance_id/instance_id_profile_service.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "chrome/browser/ash/android_sms/android_sms_service_factory.h" +#include "chrome/browser/ash/multidevice_setup/multidevice_setup_client_factory.h" +#endif + +// static +PushMessagingServiceImpl* PushMessagingServiceFactory::GetForProfile( + content::BrowserContext* context) { + // The Push API is not currently supported in incognito mode. + // See https://crbug.com/401439. + if (context->IsOffTheRecord()) + return nullptr; + + return static_cast( + GetInstance()->GetServiceForBrowserContext(context, true)); +} + +// static +PushMessagingServiceFactory* PushMessagingServiceFactory::GetInstance() { + return base::Singleton::get(); +} + +PushMessagingServiceFactory::PushMessagingServiceFactory() + : BrowserContextKeyedServiceFactory( + "PushMessagingProfileService", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(gcm::GCMProfileServiceFactory::GetInstance()); + DependsOn(instance_id::InstanceIDProfileServiceFactory::GetInstance()); + DependsOn(HostContentSettingsMapFactory::GetInstance()); + DependsOn(PermissionManagerFactory::GetInstance()); + DependsOn(site_engagement::SiteEngagementServiceFactory::GetInstance()); +#if BUILDFLAG(IS_CHROMEOS_ASH) + DependsOn(ash::android_sms::AndroidSmsServiceFactory::GetInstance()); + DependsOn( + ash::multidevice_setup::MultiDeviceSetupClientFactory::GetInstance()); +#endif +} + +PushMessagingServiceFactory::~PushMessagingServiceFactory() {} + +void PushMessagingServiceFactory::RestoreFactoryForTests( + content::BrowserContext* context) { + SetTestingFactory(context, + base::BindRepeating([](content::BrowserContext* context) { + return base::WrapUnique( + GetInstance()->BuildServiceInstanceFor(context)); + })); +} + +KeyedService* PushMessagingServiceFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + CHECK(!profile->IsOffTheRecord()); + return new PushMessagingServiceImpl(profile); +} + +content::BrowserContext* PushMessagingServiceFactory::GetBrowserContextToUse( + content::BrowserContext* context) const { + return chrome::GetBrowserContextOwnInstanceInIncognito(context); +} diff --git a/chromium/chrome/browser/push_messaging/push_messaging_service_factory.h b/chromium/chrome/browser/push_messaging/push_messaging_service_factory.h new file mode 100644 index 00000000000..5512ed8dfe7 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_service_factory.h @@ -0,0 +1,40 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_FACTORY_H_ +#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +class PushMessagingServiceImpl; + +class PushMessagingServiceFactory : public BrowserContextKeyedServiceFactory { + public: + static PushMessagingServiceImpl* GetForProfile( + content::BrowserContext* profile); + static PushMessagingServiceFactory* GetInstance(); + + PushMessagingServiceFactory(const PushMessagingServiceFactory&) = delete; + PushMessagingServiceFactory& operator=(const PushMessagingServiceFactory&) = + delete; + + // Undo a previous call to SetTestingFactory, such that subsequent calls to + // GetForProfile get a real push service. + void RestoreFactoryForTests(content::BrowserContext* context); + + private: + friend struct base::DefaultSingletonTraits; + + PushMessagingServiceFactory(); + ~PushMessagingServiceFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const override; + content::BrowserContext* GetBrowserContextToUse( + content::BrowserContext* context) const override; +}; + +#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_FACTORY_H_ diff --git a/chromium/chrome/browser/push_messaging/push_messaging_service_impl.cc b/chromium/chrome/browser/push_messaging/push_messaging_service_impl.cc new file mode 100644 index 00000000000..6313d399760 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_service_impl.cc @@ -0,0 +1,1684 @@ +// 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 "chrome/browser/push_messaging/push_messaging_service_impl.h" + +#include +#include +#include + +#include "base/barrier_closure.h" +#include "base/base64url.h" +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/command_line.h" +#include "base/feature_list.h" +#include "base/logging.h" +#include "base/metrics/histogram_functions.h" +#include "base/metrics/histogram_macros.h" +#include "base/strings/string_util.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/chrome_notification_types.h" +#include "chrome/browser/content_settings/host_content_settings_map_factory.h" +#include "chrome/browser/gcm/gcm_profile_service_factory.h" +#include "chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h" +#include "chrome/browser/permissions/abusive_origin_permission_revocation_request.h" +#include "chrome/browser/permissions/permission_manager_factory.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_keep_alive_types.h" +#include "chrome/browser/profiles/scoped_profile_keep_alive.h" +#include "chrome/browser/push_messaging/push_messaging_app_identifier.h" +#include "chrome/browser/push_messaging/push_messaging_constants.h" +#include "chrome/browser/push_messaging/push_messaging_features.h" +#include "chrome/browser/push_messaging/push_messaging_service_factory.h" +#include "chrome/browser/push_messaging/push_messaging_utils.h" +#include "chrome/browser/ui/chrome_pages.h" +#include "chrome/common/buildflags.h" +#include "chrome/common/chrome_features.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/common/pref_names.h" +#include "chrome/grit/generated_resources.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/gcm_driver/gcm_driver.h" +#include "components/gcm_driver/gcm_profile_service.h" +#include "components/gcm_driver/instance_id/instance_id.h" +#include "components/gcm_driver/instance_id/instance_id_driver.h" +#include "components/gcm_driver/instance_id/instance_id_profile_service.h" +#include "components/permissions/permission_manager.h" +#include "components/permissions/permission_result.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/prefs/pref_service.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/devtools_background_services_context.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/service_worker_context.h" +#include "content/public/browser/storage_partition.h" +#include "content/public/common/child_process_host.h" +#include "content/public/common/content_features.h" +#include "content/public/common/content_switches.h" +#include "third_party/blink/public/mojom/devtools/console_message.mojom.h" +#include "third_party/blink/public/mojom/permissions/permission_status.mojom.h" +#include "third_party/blink/public/mojom/push_messaging/push_messaging_status.mojom.h" +#include "ui/base/l10n/l10n_util.h" + +#if BUILDFLAG(ENABLE_BACKGROUND_MODE) +#include "chrome/browser/background/background_mode_manager.h" +#include "components/keep_alive_registry/keep_alive_types.h" +#include "components/keep_alive_registry/scoped_keep_alive.h" +#endif + +#if defined(OS_ANDROID) +#include "base/android/jni_android.h" +#include "chrome/android/chrome_jni_headers/PushMessagingServiceObserver_jni.h" +#endif + +using instance_id::InstanceID; + +namespace { + +// Scope passed to getToken to obtain GCM registration tokens. +// Must match Java GoogleCloudMessaging.INSTANCE_ID_SCOPE. +const char kGCMScope[] = "GCM"; + +const int kMaxRegistrations = 1000000; + +// Chrome does not yet support silent push messages, and requires websites to +// indicate that they will only send user-visible messages. +const char kSilentPushUnsupportedMessage[] = + "Chrome currently only supports the Push API for subscriptions that will " + "result in user-visible messages. You can indicate this by calling " + "pushManager.subscribe({userVisibleOnly: true}) instead. See " + "https://goo.gl/yqv4Q4 for more details."; + +// Message displayed in the console (as an error) when a GCM Sender ID is used +// to create a subscription, which is unsupported. The subscription request will +// have been blocked, and an exception will be thrown as well. +const char kSenderIdRegistrationDisallowedMessage[] = + "The provided application server key is not a VAPID key. Only VAPID keys " + "are supported. For more information check https://crbug.com/979235."; + +// Message displayed in the console (as a warning) when a GCM Sender ID is used +// to create a subscription, which will soon be unsupported. +const char kSenderIdRegistrationDeprecatedMessage[] = + "The provided application server key is not a VAPID key. Only VAPID keys " + "will be supported in the future. For more information check " + "https://crbug.com/979235."; + +void RecordDeliveryStatus(blink::mojom::PushEventStatus status) { + UMA_HISTOGRAM_ENUMERATION("PushMessaging.DeliveryStatus", status); +} + +void RecordPushSubcriptionChangeStatus(blink::mojom::PushEventStatus status) { + UMA_HISTOGRAM_ENUMERATION("PushMessaging.PushSubscriptionChangeStatus", + status); +} +void RecordUnsubscribeReason(blink::mojom::PushUnregistrationReason reason) { + UMA_HISTOGRAM_ENUMERATION("PushMessaging.UnregistrationReason", reason); +} + +void RecordUnsubscribeGCMResult(gcm::GCMClient::Result result) { + UMA_HISTOGRAM_ENUMERATION("PushMessaging.UnregistrationGCMResult", result); +} + +void RecordUnsubscribeIIDResult(InstanceID::Result result) { + UMA_HISTOGRAM_ENUMERATION("PushMessaging.UnregistrationIIDResult", result); +} + +blink::mojom::PermissionStatus ToPermissionStatus( + ContentSetting content_setting) { + switch (content_setting) { + case CONTENT_SETTING_ALLOW: + return blink::mojom::PermissionStatus::GRANTED; + case CONTENT_SETTING_BLOCK: + return blink::mojom::PermissionStatus::DENIED; + case CONTENT_SETTING_ASK: + return blink::mojom::PermissionStatus::ASK; + default: + break; + } + NOTREACHED(); + return blink::mojom::PermissionStatus::DENIED; +} + +void UnregisterCallbackToClosure( + base::OnceClosure closure, + blink::mojom::PushUnregistrationStatus status) { + DCHECK(closure); + std::move(closure).Run(); +} + +void LogMessageReceivedEventToDevTools( + content::DevToolsBackgroundServicesContext* devtools_context, + const PushMessagingAppIdentifier& app_identifier, + const std::string& message_id, + bool was_encrypted, + const std::string& error_message, + const std::string& payload) { + if (!devtools_context) + return; + + std::map event_metadata = { + {"Success", error_message.empty() ? "Yes" : "No"}, + {"Was Encrypted", was_encrypted ? "Yes" : "No"}}; + + if (!error_message.empty()) + event_metadata["Error Reason"] = error_message; + else if (was_encrypted) + event_metadata["Payload"] = payload; + + devtools_context->LogBackgroundServiceEvent( + app_identifier.service_worker_registration_id(), + url::Origin::Create(app_identifier.origin()), + content::DevToolsBackgroundService::kPushMessaging, + "Push message received" /* event_name */, message_id, event_metadata); +} + +PendingMessage::PendingMessage(std::string app_id, gcm::IncomingMessage message) + : app_id(std::move(app_id)), + message(std::move(message)), + received_time(base::Time::Now()) {} +PendingMessage::PendingMessage(const PendingMessage& other) = default; +PendingMessage::PendingMessage(PendingMessage&& other) = default; +PendingMessage& PendingMessage::operator=(PendingMessage&& other) = default; +PendingMessage::~PendingMessage() = default; + +} // namespace + +// static +void PushMessagingServiceImpl::InitializeForProfile(Profile* profile) { + // TODO(johnme): Consider whether push should be enabled in incognito. + if (!profile || profile->IsOffTheRecord()) + return; + + int count = PushMessagingAppIdentifier::GetCount(profile); + if (count <= 0) + return; + + PushMessagingServiceImpl* push_service = + PushMessagingServiceFactory::GetForProfile(profile); + if (push_service) { + push_service->IncreasePushSubscriptionCount(count, false /* is_pending */); + push_service->RemoveExpiredSubscriptions(); + } +} + +void PushMessagingServiceImpl::RemoveExpiredSubscriptions() { + if (!base::FeatureList::IsEnabled( + features::kPushSubscriptionWithExpirationTime)) { + return; + } + + base::RepeatingClosure barrier_closure = base::BarrierClosure( + PushMessagingAppIdentifier::GetCount(profile_), + remove_expired_subscriptions_callback_for_testing_.is_null() + ? base::DoNothing() + : std::move(remove_expired_subscriptions_callback_for_testing_)); + + for (const auto& identifier : PushMessagingAppIdentifier::GetAll(profile_)) { + if (!identifier.IsExpired()) { + base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, barrier_closure); + continue; + } + content::BrowserThread::PostBestEffortTask( + FROM_HERE, base::ThreadTaskRunnerHandle::Get(), + base::BindOnce( + &PushMessagingServiceImpl::UnexpectedChange, + weak_factory_.GetWeakPtr(), identifier, + blink::mojom::PushUnregistrationReason::SUBSCRIPTION_EXPIRED, + barrier_closure)); + } +} + +void PushMessagingServiceImpl::UnexpectedChange( + PushMessagingAppIdentifier identifier, + blink::mojom::PushUnregistrationReason reason, + base::OnceClosure completed_closure) { + auto unsubscribe_closure = + base::BindOnce(&PushMessagingServiceImpl::UnexpectedUnsubscribe, + weak_factory_.GetWeakPtr(), identifier, reason, + base::BindOnce(&UnregisterCallbackToClosure, + std::move(completed_closure))); + if (base::FeatureList::IsEnabled(features::kPushSubscriptionChangeEvent)) { + // Find old subscription and fire a `pushsubscriptionchange` event + GetPushSubscriptionFromAppIdentifier( + identifier, + base::BindOnce(&PushMessagingServiceImpl::FirePushSubscriptionChange, + weak_factory_.GetWeakPtr(), identifier, + std::move(unsubscribe_closure), + nullptr /* new_subscription */)); + } else { + std::move(unsubscribe_closure).Run(); + } +} + +PushMessagingServiceImpl::PushMessagingServiceImpl(Profile* profile) + : profile_(profile), + push_subscription_count_(0), + pending_push_subscription_count_(0), + notification_manager_(profile) { + DCHECK(profile); + HostContentSettingsMapFactory::GetForProfile(profile_)->AddObserver(this); + + registrar_.Add(this, chrome::NOTIFICATION_APP_TERMINATING, + content::NotificationService::AllSources()); + refresh_observation_.Observe(&refresher_); +} + +PushMessagingServiceImpl::~PushMessagingServiceImpl() = default; + +void PushMessagingServiceImpl::IncreasePushSubscriptionCount(int add, + bool is_pending) { + DCHECK_GT(add, 0); + if (push_subscription_count_ + pending_push_subscription_count_ == 0) + GetGCMDriver()->AddAppHandler(kPushMessagingAppIdentifierPrefix, this); + + if (is_pending) + pending_push_subscription_count_ += add; + else + push_subscription_count_ += add; +} + +void PushMessagingServiceImpl::DecreasePushSubscriptionCount(int subtract, + bool was_pending) { + DCHECK_GT(subtract, 0); + if (was_pending) { + pending_push_subscription_count_ -= subtract; + DCHECK_GE(pending_push_subscription_count_, 0); + } else { + push_subscription_count_ -= subtract; + DCHECK_GE(push_subscription_count_, 0); + } + + if (push_subscription_count_ + pending_push_subscription_count_ == 0) + GetGCMDriver()->RemoveAppHandler(kPushMessagingAppIdentifierPrefix); +} + +bool PushMessagingServiceImpl::CanHandle(const std::string& app_id) const { + return base::StartsWith(app_id, kPushMessagingAppIdentifierPrefix, + base::CompareCase::INSENSITIVE_ASCII); +} + +void PushMessagingServiceImpl::ShutdownHandler() { + // Shutdown() should come before and it removes us from the list of app + // handlers of gcm::GCMDriver so this shouldn't ever been called. + NOTREACHED(); +} + +void PushMessagingServiceImpl::OnStoreReset() { + // Delete all cached subscriptions, since they are now invalid. + for (const auto& identifier : PushMessagingAppIdentifier::GetAll(profile_)) { + RecordUnsubscribeReason( + blink::mojom::PushUnregistrationReason::GCM_STORE_RESET); + // Clear all the subscriptions in parallel, to reduce risk that shutdown + // occurs before we finish clearing them. + ClearPushSubscriptionId(profile_, identifier.origin(), + identifier.service_worker_registration_id(), + base::DoNothing()); + // TODO(johnme): Fire pushsubscriptionchange/pushsubscriptionlost SW event. + } + PushMessagingAppIdentifier::DeleteAllFromPrefs(profile_); +} + +// OnMessage methods ----------------------------------------------------------- + +void PushMessagingServiceImpl::OnMessage(const std::string& app_id, + const gcm::IncomingMessage& message) { + // We won't have time to process and act on the message. + // TODO(peter) This should be checked at the level of the GCMDriver, so that + // the message is not consumed. See https://crbug.com/612815 + if (g_browser_process->IsShuttingDown() || shutdown_started_) + return; + +#if BUILDFLAG(ENABLE_BACKGROUND_MODE) + if (g_browser_process->background_mode_manager()) { + UMA_HISTOGRAM_BOOLEAN("PushMessaging.ReceivedMessageInBackground", + g_browser_process->background_mode_manager() + ->IsBackgroundWithoutWindows()); + } + + if (!in_flight_keep_alive_) { + in_flight_keep_alive_ = std::make_unique( + KeepAliveOrigin::IN_FLIGHT_PUSH_MESSAGE, + KeepAliveRestartOption::DISABLED); + in_flight_profile_keep_alive_ = std::make_unique( + profile_, ProfileKeepAliveOrigin::kInFlightPushMessage); + } +#endif + + refresher_.GotMessageFrom(app_id); + + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByAppId(profile_, app_id); + // Drop message and unregister if app_id was unknown (maybe recently deleted). + if (app_identifier.is_null()) { + absl::optional refresh_identifier = + refresher_.FindActiveAppIdentifier(app_id); + if (!refresh_identifier) { + DeliverMessageCallback(app_id, GURL::EmptyGURL(), + -1 /* kInvalidServiceWorkerRegistrationId */, + message, false /* did_enqueue_message */, + blink::mojom::PushEventStatus::UNKNOWN_APP_ID); + return; + } + app_identifier = std::move(*refresh_identifier); + } + + LogMessageReceivedEventToDevTools( + GetDevToolsContext(app_identifier.origin()), app_identifier, + message.message_id, + /* was_encrypted= */ message.decrypted, std::string() /* error_message */, + message.decrypted ? message.raw_data : std::string()); + + if (IsPermissionSet(app_identifier.origin())) { + messages_pending_permission_check_.emplace(app_id, message); + // Start abusive origin verification only if no other verification is in + // progress. + if (!abusive_origin_revocation_request_) + CheckOriginForAbuseAndDispatchNextMessage(); + } else { + // Drop message and unregister if origin has lost push permission. + DeliverMessageCallback(app_id, app_identifier.origin(), + app_identifier.service_worker_registration_id(), + message, false /* did_enqueue_message */, + blink::mojom::PushEventStatus::PERMISSION_DENIED); + } +} + +void PushMessagingServiceImpl::CheckOriginForAbuseAndDispatchNextMessage() { + if (messages_pending_permission_check_.empty()) + return; + + PendingMessage message = + std::move(messages_pending_permission_check_.front()); + messages_pending_permission_check_.pop(); + + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByAppId(profile_, message.app_id); + + if (app_identifier.is_null()) { + CheckOriginForAbuseAndDispatchNextMessage(); + return; + } + + DCHECK(!abusive_origin_revocation_request_) + << "Create one Abusive Origin Revocation instance per request."; + abusive_origin_revocation_request_ = + std::make_unique( + profile_, app_identifier.origin(), + base::BindOnce(&PushMessagingServiceImpl::OnCheckedOriginForAbuse, + weak_factory_.GetWeakPtr(), std::move(message))); +} + +void PushMessagingServiceImpl::OnCheckedOriginForAbuse( + PendingMessage message, + AbusiveOriginPermissionRevocationRequest::Outcome outcome) { + abusive_origin_revocation_request_.reset(); + + base::UmaHistogramLongTimes("PushMessaging.CheckOriginForAbuseTime", + base::Time::Now() - message.received_time); + + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByAppId(profile_, message.app_id); + + if (app_identifier.is_null()) { + CheckOriginForAbuseAndDispatchNextMessage(); + return; + } + + const GURL& origin = app_identifier.origin(); + int64_t service_worker_registration_id = + app_identifier.service_worker_registration_id(); + + // It is possible that Notifications permission has been revoked by an user + // during abusive origin verification. + if (outcome == AbusiveOriginPermissionRevocationRequest::Outcome:: + PERMISSION_NOT_REVOKED && + IsPermissionSet(origin)) { + std::queue& delivery_queue = + message_delivery_queue_[{origin, service_worker_registration_id}]; + delivery_queue.push(std::move(message)); + + // Start delivering push messages to this service worker if this was the + // first message. Otherwise just enqueue the message to be delivered once + // all previous messages have been handled. + if (delivery_queue.size() == 1) { + DeliverNextQueuedMessageForServiceWorkerRegistration( + origin, service_worker_registration_id); + } + } else { + // Drop message and unregister if origin has lost push permission. + DeliverMessageCallback( + message.app_id, origin, service_worker_registration_id, message.message, + false /* did_enqueue_message */, + outcome == AbusiveOriginPermissionRevocationRequest::Outcome:: + PERMISSION_NOT_REVOKED + ? blink::mojom::PushEventStatus::PERMISSION_DENIED + : blink::mojom::PushEventStatus::PERMISSION_REVOKED_ABUSIVE); + } + + // Verify the next message in the queue. + CheckOriginForAbuseAndDispatchNextMessage(); +} + +void PushMessagingServiceImpl:: + DeliverNextQueuedMessageForServiceWorkerRegistration( + const GURL& origin, + int64_t service_worker_registration_id) { + MessageDeliveryQueueKey key{origin, service_worker_registration_id}; + auto iter = message_delivery_queue_.find(key); + if (iter == message_delivery_queue_.end()) + return; + + const std::queue& delivery_queue = iter->second; + CHECK(!delivery_queue.empty()); + const PendingMessage& next_message = delivery_queue.front(); + + const std::string& app_id = next_message.app_id; + const gcm::IncomingMessage& message = next_message.message; + + auto deliver_message_callback = base::BindOnce( + &PushMessagingServiceImpl::DeliverMessageCallback, + weak_factory_.GetWeakPtr(), app_id, origin, + service_worker_registration_id, message, true /* did_enqueue_message */); + + // It is possible that Notification permissions have been revoked by a user + // while handling previous messages for |origin|. + if (!IsPermissionSet(origin)) { + std::move(deliver_message_callback) + .Run(blink::mojom::PushEventStatus::PERMISSION_DENIED); + return; + } + + // The payload of a push message can be valid with content, valid with empty + // content, or null. + absl::optional payload; + if (message.decrypted) + payload = message.raw_data; + + base::UmaHistogramLongTimes("PushMessaging.DeliverQueuedMessageTime", + base::Time::Now() - next_message.received_time); + + // Inform tests observing message dispatching about the event. + if (message_dispatched_callback_for_testing_) { + message_dispatched_callback_for_testing_.Run( + app_id, origin, service_worker_registration_id, std::move(payload), + std::move(deliver_message_callback)); + return; + } + + // Dispatch the message to the appropriate Service Worker. + profile_->DeliverPushMessage(origin, service_worker_registration_id, + message.message_id, payload, + std::move(deliver_message_callback)); +} + +void PushMessagingServiceImpl::DeliverMessageCallback( + const std::string& app_id, + const GURL& requesting_origin, + int64_t service_worker_registration_id, + const gcm::IncomingMessage& message, + bool did_enqueue_message, + blink::mojom::PushEventStatus status) { + RecordDeliveryStatus(status); + + // Note: It's important that |message_handled_callback| is run or passed to + // another function before this function returns. + auto message_handled_callback = + base::BindOnce(&PushMessagingServiceImpl::DidHandleMessage, + weak_factory_.GetWeakPtr(), app_id, message.message_id); + + if (did_enqueue_message) { + message_handled_callback = base::BindOnce( + &PushMessagingServiceImpl::DidHandleEnqueuedMessage, + weak_factory_.GetWeakPtr(), requesting_origin, + service_worker_registration_id, std::move(message_handled_callback)); + } + + // A reason to automatically unsubscribe. UNKNOWN means do not unsubscribe. + blink::mojom::PushUnregistrationReason unsubscribe_reason = + blink::mojom::PushUnregistrationReason::UNKNOWN; + + // TODO(mvanouwerkerk): Show a warning in the developer console of the + // Service Worker corresponding to app_id (and/or on an internals page). + // See https://crbug.com/508516 for options. + switch (status) { + // Call EnforceUserVisibleOnlyRequirements if the message was delivered to + // the Service Worker JavaScript, even if the website's event handler failed + // (to prevent sites deliberately failing in order to avoid having to show + // notifications). + case blink::mojom::PushEventStatus::SUCCESS: + case blink::mojom::PushEventStatus::EVENT_WAITUNTIL_REJECTED: + case blink::mojom::PushEventStatus::TIMEOUT: + // Only enforce the user visible requirements if silent push has not been + // enabled through a command line flag. + if (!base::CommandLine::ForCurrentProcess()->HasSwitch( + switches::kAllowSilentPush)) { + notification_manager_.EnforceUserVisibleOnlyRequirements( + requesting_origin, service_worker_registration_id, + std::move(message_handled_callback)); + message_handled_callback = base::OnceCallback(); + } + break; + case blink::mojom::PushEventStatus::SERVICE_WORKER_ERROR: + // Do nothing, and hope the error is transient. + break; + case blink::mojom::PushEventStatus::UNKNOWN_APP_ID: + unsubscribe_reason = + blink::mojom::PushUnregistrationReason::DELIVERY_UNKNOWN_APP_ID; + break; + case blink::mojom::PushEventStatus::PERMISSION_DENIED: + unsubscribe_reason = + blink::mojom::PushUnregistrationReason::DELIVERY_PERMISSION_DENIED; + break; + case blink::mojom::PushEventStatus::NO_SERVICE_WORKER: + unsubscribe_reason = + blink::mojom::PushUnregistrationReason::DELIVERY_NO_SERVICE_WORKER; + break; + case blink::mojom::PushEventStatus::PERMISSION_REVOKED_ABUSIVE: + unsubscribe_reason = + blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED_ABUSIVE; + break; + } + + // If |message_handled_callback| was not yet used, make a + // |completion_closure_runner| which should run by default at the end of this + // function, unless it is explicitly passed to another function or disabled. + base::ScopedClosureRunner completion_closure_runner( + message_handled_callback + ? base::BindOnce(std::move(message_handled_callback), + false /* did_show_generic_notification */) + : base::DoNothing()); + + if (unsubscribe_reason != blink::mojom::PushUnregistrationReason::UNKNOWN) { + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByAppId(profile_, app_id); + UnsubscribeInternal( + unsubscribe_reason, + app_identifier.is_null() ? GURL::EmptyGURL() : app_identifier.origin(), + app_identifier.is_null() + ? -1 /* kInvalidServiceWorkerRegistrationId */ + : app_identifier.service_worker_registration_id(), + app_id, message.sender_id, + base::BindOnce(&UnregisterCallbackToClosure, + completion_closure_runner.Release())); + + if (app_identifier.is_null()) + return; + + if (auto* devtools_context = GetDevToolsContext(app_identifier.origin())) { + std::stringstream ss; + ss << unsubscribe_reason; + devtools_context->LogBackgroundServiceEvent( + app_identifier.service_worker_registration_id(), + url::Origin::Create(app_identifier.origin()), + content::DevToolsBackgroundService::kPushMessaging, + "Unsubscribed due to error" /* event_name */, message.message_id, + {{"Reason", ss.str()}}); + } + } +} + +void PushMessagingServiceImpl::DidHandleEnqueuedMessage( + const GURL& origin, + int64_t service_worker_registration_id, + base::OnceCallback message_handled_callback, + bool did_show_generic_notification) { + // Lookup the message queue for the correct service worker. + MessageDeliveryQueueKey key{origin, service_worker_registration_id}; + auto iter = message_delivery_queue_.find(key); + CHECK(iter != message_delivery_queue_.end()); + + // Remove the delivered message from the queue. + std::queue& delivery_queue = iter->second; + CHECK(!delivery_queue.empty()); + + base::UmaHistogramLongTimes( + "PushMessaging.MessageHandledTime", + base::Time::Now() - delivery_queue.front().received_time); + + delivery_queue.pop(); + if (delivery_queue.empty()) + message_delivery_queue_.erase(key); + + // This will call PushMessagingServiceImpl::DidHandleMessage(). + std::move(message_handled_callback).Run(did_show_generic_notification); + + // Deliver next message to this service worker now. We deliver them in series + // so we can check the visibility requirements after each message. + DeliverNextQueuedMessageForServiceWorkerRegistration( + origin, service_worker_registration_id); +} + +void PushMessagingServiceImpl::DidHandleMessage( + const std::string& app_id, + const std::string& push_message_id, + bool did_show_generic_notification) { +#if BUILDFLAG(ENABLE_BACKGROUND_MODE) + // Reset before running callbacks below, so tests can verify keep-alive reset. + if (message_delivery_queue_.empty()) { + in_flight_keep_alive_.reset(); + in_flight_profile_keep_alive_.reset(); + } +#endif + + if (message_callback_for_testing_) + message_callback_for_testing_.Run(); + +#if defined(OS_ANDROID) + chrome::android::Java_PushMessagingServiceObserver_onMessageHandled( + base::android::AttachCurrentThread()); +#endif + + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByAppId(profile_, app_id); + + if (app_identifier.is_null() || !did_show_generic_notification) + return; + + if (auto* devtools_context = GetDevToolsContext(app_identifier.origin())) { + devtools_context->LogBackgroundServiceEvent( + app_identifier.service_worker_registration_id(), + url::Origin::Create(app_identifier.origin()), + content::DevToolsBackgroundService::kPushMessaging, + "Generic notification shown" /* event_name */, push_message_id, + {} /* event_metadata */); + } +} + +void PushMessagingServiceImpl::SetMessageCallbackForTesting( + const base::RepeatingClosure& callback) { + message_callback_for_testing_ = callback; +} + +// Other gcm::GCMAppHandler methods -------------------------------------------- + +void PushMessagingServiceImpl::OnMessagesDeleted(const std::string& app_id) { + // TODO(mvanouwerkerk): Consider firing an event on the Service Worker + // corresponding to |app_id| to inform the app about deleted messages. +} + +void PushMessagingServiceImpl::OnSendError( + const std::string& app_id, + const gcm::GCMClient::SendErrorDetails& send_error_details) { + NOTREACHED() << "The Push API shouldn't have sent messages upstream"; +} + +void PushMessagingServiceImpl::OnSendAcknowledged( + const std::string& app_id, + const std::string& message_id) { + NOTREACHED() << "The Push API shouldn't have sent messages upstream"; +} + +void PushMessagingServiceImpl::OnMessageDecryptionFailed( + const std::string& app_id, + const std::string& message_id, + const std::string& error_message) { + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByAppId(profile_, app_id); + + if (app_identifier.is_null()) + return; + + LogMessageReceivedEventToDevTools( + GetDevToolsContext(app_identifier.origin()), app_identifier, message_id, + /* was_encrypted= */ true, error_message, "" /* payload */); +} + +// Subscribe and GetPermissionStatus methods ----------------------------------- + +void PushMessagingServiceImpl::SubscribeFromDocument( + const GURL& requesting_origin, + int64_t service_worker_registration_id, + int render_process_id, + int render_frame_id, + blink::mojom::PushSubscriptionOptionsPtr options, + bool user_gesture, + RegisterCallback callback) { + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker( + profile_, requesting_origin, service_worker_registration_id); + + // If there is no existing app identifier for the given Service Worker, + // generate a new one. This will create a new subscription on the server. + if (app_identifier.is_null()) { + app_identifier = PushMessagingAppIdentifier::Generate( + requesting_origin, service_worker_registration_id); + } + + if (push_subscription_count_ + pending_push_subscription_count_ >= + kMaxRegistrations) { + SubscribeEndWithError(std::move(callback), + blink::mojom::PushRegistrationStatus::LIMIT_REACHED); + return; + } + + content::RenderFrameHost* render_frame_host = + content::RenderFrameHost::FromID(render_process_id, render_frame_id); + + if (!render_frame_host) { + // It is possible for `render_frame_host` to be nullptr here due to a race + // (crbug.com/1057981). + SubscribeEndWithError( + std::move(callback), + blink::mojom::PushRegistrationStatus::RENDERER_SHUTDOWN); + return; + } + + if (!options->user_visible_only) { + render_frame_host->AddMessageToConsole( + blink::mojom::ConsoleMessageLevel::kError, + kSilentPushUnsupportedMessage); + + SubscribeEndWithError( + std::move(callback), + blink::mojom::PushRegistrationStatus::PERMISSION_DENIED); + return; + } + + // Push does not allow permission requests from iframes. + PermissionManagerFactory::GetForProfile(profile_)->RequestPermission( + ContentSettingsType::NOTIFICATIONS, render_frame_host, requesting_origin, + user_gesture, + base::BindOnce(&PushMessagingServiceImpl::DoSubscribe, + weak_factory_.GetWeakPtr(), std::move(app_identifier), + std::move(options), std::move(callback), render_process_id, + render_frame_id)); +} + +void PushMessagingServiceImpl::SubscribeFromWorker( + const GURL& requesting_origin, + int64_t service_worker_registration_id, + blink::mojom::PushSubscriptionOptionsPtr options, + RegisterCallback register_callback) { + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker( + profile_, requesting_origin, service_worker_registration_id); + + // If there is no existing app identifier for the given Service Worker, + // generate a new one. This will create a new subscription on the server. + if (app_identifier.is_null()) { + app_identifier = PushMessagingAppIdentifier::Generate( + requesting_origin, service_worker_registration_id); + } + + if (push_subscription_count_ + pending_push_subscription_count_ >= + kMaxRegistrations) { + SubscribeEndWithError(std::move(register_callback), + blink::mojom::PushRegistrationStatus::LIMIT_REACHED); + return; + } + + if (!IsPermissionSet(requesting_origin, options->user_visible_only)) { + SubscribeEndWithError( + std::move(register_callback), + blink::mojom::PushRegistrationStatus::PERMISSION_DENIED); + return; + } + + DoSubscribe(std::move(app_identifier), std::move(options), + std::move(register_callback), + /* render_process_id= */ -1, /* render_frame_id= */ -1, + CONTENT_SETTING_ALLOW); +} + +blink::mojom::PermissionStatus PushMessagingServiceImpl::GetPermissionStatus( + const GURL& origin, + bool user_visible) { + if (!user_visible) + return blink::mojom::PermissionStatus::DENIED; + + // Because the Push API is tied to Service Workers, many usages of the API + // won't have an embedding origin at all. Only consider the requesting + // |origin| when checking whether permission to use the API has been granted. + return ToPermissionStatus( + PermissionManagerFactory::GetForProfile(profile_) + ->GetPermissionStatus(ContentSettingsType::NOTIFICATIONS, origin, + origin) + .content_setting); +} + +bool PushMessagingServiceImpl::SupportNonVisibleMessages() { + return false; +} + +void PushMessagingServiceImpl::DoSubscribe( + PushMessagingAppIdentifier app_identifier, + blink::mojom::PushSubscriptionOptionsPtr options, + RegisterCallback register_callback, + int render_process_id, + int render_frame_id, + ContentSetting content_setting) { + if (content_setting != CONTENT_SETTING_ALLOW) { + SubscribeEndWithError( + std::move(register_callback), + blink::mojom::PushRegistrationStatus::PERMISSION_DENIED); + return; + } + + std::string application_server_key_string( + options->application_server_key.begin(), + options->application_server_key.end()); + + // TODO(peter): Move this check to the renderer process & Mojo message + // validation once the flag is always enabled, and remove the + // |render_process_id| and |render_frame_id| parameters from this method. + if (!push_messaging::IsVapidKey(application_server_key_string)) { + content::RenderFrameHost* render_frame_host = + content::RenderFrameHost::FromID(render_process_id, render_frame_id); + if (base::FeatureList::IsEnabled( + features::kPushMessagingDisallowSenderIDs)) { + if (render_frame_host) { + render_frame_host->AddMessageToConsole( + blink::mojom::ConsoleMessageLevel::kError, + kSenderIdRegistrationDisallowedMessage); + } + SubscribeEndWithError( + std::move(register_callback), + blink::mojom::PushRegistrationStatus::UNSUPPORTED_GCM_SENDER_ID); + return; + } else if (render_frame_host) { + render_frame_host->AddMessageToConsole( + blink::mojom::ConsoleMessageLevel::kWarning, + kSenderIdRegistrationDeprecatedMessage); + } + } + + IncreasePushSubscriptionCount(1, true /* is_pending */); + + // Set time to live for GCM registration + base::TimeDelta ttl = base::TimeDelta(); + + if (base::FeatureList::IsEnabled( + features::kPushSubscriptionWithExpirationTime)) { + app_identifier.set_expiration_time( + base::Time::Now() + kPushSubscriptionExpirationPeriodTimeDelta); + DCHECK(app_identifier.expiration_time()); + ttl = kPushSubscriptionExpirationPeriodTimeDelta; + } + + GetInstanceIDDriver() + ->GetInstanceID(app_identifier.app_id()) + ->GetToken( + push_messaging::NormalizeSenderInfo(application_server_key_string), + kGCMScope, ttl, {} /* flags */, + base::BindOnce(&PushMessagingServiceImpl::DidSubscribe, + weak_factory_.GetWeakPtr(), app_identifier, + application_server_key_string, + std::move(register_callback))); +} + +void PushMessagingServiceImpl::SubscribeEnd( + RegisterCallback callback, + const std::string& subscription_id, + const GURL& endpoint, + const absl::optional& expiration_time, + const std::vector& p256dh, + const std::vector& auth, + blink::mojom::PushRegistrationStatus status) { + std::move(callback).Run(subscription_id, endpoint, expiration_time, p256dh, + auth, status); +} + +void PushMessagingServiceImpl::SubscribeEndWithError( + RegisterCallback callback, + blink::mojom::PushRegistrationStatus status) { + SubscribeEnd(std::move(callback), std::string() /* subscription_id */, + GURL::EmptyGURL() /* endpoint */, + absl::nullopt /* expiration_time */, + std::vector() /* p256dh */, + std::vector() /* auth */, status); +} + +void PushMessagingServiceImpl::DidSubscribe( + const PushMessagingAppIdentifier& app_identifier, + const std::string& sender_id, + RegisterCallback callback, + const std::string& subscription_id, + InstanceID::Result result) { + DecreasePushSubscriptionCount(1, true /* was_pending */); + + blink::mojom::PushRegistrationStatus status = + blink::mojom::PushRegistrationStatus::SERVICE_ERROR; + + switch (result) { + case InstanceID::SUCCESS: { + const GURL endpoint = push_messaging::CreateEndpoint(subscription_id); + + // Make sure that this subscription has associated encryption keys prior + // to returning it to the developer - they'll need this information in + // order to send payloads to the user. + GetEncryptionInfoForAppId( + app_identifier.app_id(), sender_id, + base::BindOnce( + &PushMessagingServiceImpl::DidSubscribeWithEncryptionInfo, + weak_factory_.GetWeakPtr(), app_identifier, std::move(callback), + subscription_id, endpoint)); + return; + } + case InstanceID::INVALID_PARAMETER: + case InstanceID::DISABLED: + case InstanceID::ASYNC_OPERATION_PENDING: + case InstanceID::SERVER_ERROR: + case InstanceID::UNKNOWN_ERROR: + DLOG(ERROR) << "Push messaging subscription failed; InstanceID::Result = " + << result; + status = blink::mojom::PushRegistrationStatus::SERVICE_ERROR; + break; + case InstanceID::NETWORK_ERROR: + status = blink::mojom::PushRegistrationStatus::NETWORK_ERROR; + break; + } + + SubscribeEndWithError(std::move(callback), status); +} + +void PushMessagingServiceImpl::DidSubscribeWithEncryptionInfo( + const PushMessagingAppIdentifier& app_identifier, + RegisterCallback callback, + const std::string& subscription_id, + const GURL& endpoint, + std::string p256dh, + std::string auth_secret) { + if (p256dh.empty()) { + SubscribeEndWithError( + std::move(callback), + blink::mojom::PushRegistrationStatus::PUBLIC_KEY_UNAVAILABLE); + return; + } + + app_identifier.PersistToPrefs(profile_); + + IncreasePushSubscriptionCount(1, false /* is_pending */); + + SubscribeEnd(std::move(callback), subscription_id, endpoint, + app_identifier.expiration_time(), + std::vector(p256dh.begin(), p256dh.end()), + std::vector(auth_secret.begin(), auth_secret.end()), + blink::mojom::PushRegistrationStatus::SUCCESS_FROM_PUSH_SERVICE); +} + +// GetSubscriptionInfo methods ------------------------------------------------- + +void PushMessagingServiceImpl::GetSubscriptionInfo( + const GURL& origin, + int64_t service_worker_registration_id, + const std::string& sender_id, + const std::string& subscription_id, + SubscriptionInfoCallback callback) { + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker( + profile_, origin, service_worker_registration_id); + + if (app_identifier.is_null()) { + std::move(callback).Run( + false /* is_valid */, GURL::EmptyGURL() /*endpoint*/, + absl::nullopt /* expiration_time */, + std::vector() /* p256dh */, std::vector() /* auth */); + return; + } + + const GURL endpoint = push_messaging::CreateEndpoint(subscription_id); + const std::string& app_id = app_identifier.app_id(); + absl::optional expiration_time = app_identifier.expiration_time(); + + base::OnceCallback validate_cb = + base::BindOnce(&PushMessagingServiceImpl::DidValidateSubscription, + weak_factory_.GetWeakPtr(), app_id, sender_id, endpoint, + expiration_time, std::move(callback)); + + if (PushMessagingAppIdentifier::UseInstanceID(app_id)) { + GetInstanceIDDriver()->GetInstanceID(app_id)->ValidateToken( + push_messaging::NormalizeSenderInfo(sender_id), kGCMScope, + subscription_id, std::move(validate_cb)); + } else { + GetGCMDriver()->ValidateRegistration( + app_id, {push_messaging::NormalizeSenderInfo(sender_id)}, + subscription_id, std::move(validate_cb)); + } +} + +void PushMessagingServiceImpl::DidValidateSubscription( + const std::string& app_id, + const std::string& sender_id, + const GURL& endpoint, + const absl::optional& expiration_time, + SubscriptionInfoCallback callback, + bool is_valid) { + if (!is_valid) { + std::move(callback).Run( + false /* is_valid */, GURL::EmptyGURL() /* endpoint */, + absl::nullopt /* expiration_time */, + std::vector() /* p256dh */, std::vector() /* auth */); + return; + } + + GetEncryptionInfoForAppId( + app_id, sender_id, + base::BindOnce(&PushMessagingServiceImpl::DidGetEncryptionInfo, + weak_factory_.GetWeakPtr(), endpoint, expiration_time, + std::move(callback))); +} + +void PushMessagingServiceImpl::DidGetEncryptionInfo( + const GURL& endpoint, + const absl::optional& expiration_time, + SubscriptionInfoCallback callback, + std::string p256dh, + std::string auth_secret) const { + // I/O errors might prevent the GCM Driver from retrieving a key-pair. + bool is_valid = !p256dh.empty(); + std::move(callback).Run( + is_valid, endpoint, expiration_time, + std::vector(p256dh.begin(), p256dh.end()), + std::vector(auth_secret.begin(), auth_secret.end())); +} + +// Unsubscribe methods --------------------------------------------------------- + +void PushMessagingServiceImpl::Unsubscribe( + blink::mojom::PushUnregistrationReason reason, + const GURL& requesting_origin, + int64_t service_worker_registration_id, + const std::string& sender_id, + UnregisterCallback callback) { + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker( + profile_, requesting_origin, service_worker_registration_id); + + UnsubscribeInternal( + reason, requesting_origin, service_worker_registration_id, + app_identifier.is_null() ? std::string() : app_identifier.app_id(), + sender_id, std::move(callback)); +} + +void PushMessagingServiceImpl::UnsubscribeInternal( + blink::mojom::PushUnregistrationReason reason, + const GURL& origin, + int64_t service_worker_registration_id, + const std::string& app_id, + const std::string& sender_id, + UnregisterCallback callback) { + DCHECK(!app_id.empty() || (!origin.is_empty() && + service_worker_registration_id != + -1 /* kInvalidServiceWorkerRegistrationId */)) + << "Need an app_id and/or origin+service_worker_registration_id"; + + RecordUnsubscribeReason(reason); + + if (origin.is_empty() || + service_worker_registration_id == + -1 /* kInvalidServiceWorkerRegistrationId */) { + // Can't clear Service Worker database. + DidClearPushSubscriptionId(reason, app_id, sender_id, std::move(callback)); + return; + } + ClearPushSubscriptionId( + profile_, origin, service_worker_registration_id, + base::BindOnce(&PushMessagingServiceImpl::DidClearPushSubscriptionId, + weak_factory_.GetWeakPtr(), reason, app_id, sender_id, + std::move(callback))); +} + +void PushMessagingServiceImpl::DidClearPushSubscriptionId( + blink::mojom::PushUnregistrationReason reason, + const std::string& app_id, + const std::string& sender_id, + UnregisterCallback callback) { + if (app_id.empty()) { + // Without an |app_id|, we can neither delete the subscription from the + // PushMessagingAppIdentifier map, nor unsubscribe with the GCM Driver. + std::move(callback).Run( + blink::mojom::PushUnregistrationStatus::SUCCESS_WAS_NOT_REGISTERED); + return; + } + + // Delete the mapping for this app_id, to guarantee that no messages get + // delivered in future (even if unregistration fails). + // TODO(johnme): Instead of deleting these app ids, store them elsewhere, and + // retry unregistration if it fails due to network errors (crbug.com/465399). + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByAppId(profile_, app_id); + bool was_subscribed = !app_identifier.is_null(); + if (was_subscribed) + app_identifier.DeleteFromPrefs(profile_); + + // Run the unsubscribe callback *before* asking the InstanceIDDriver/GCMDriver + // to unsubscribe, since that's a slow process involving network retries, and + // by this point enough local state has been deleted that the subscription is + // inactive. Note that DeliverMessageCallback automatically unsubscribes if + // messages are later received for a subscription that was locally deleted, + // so as long as messages keep getting sent to it, the unsubscription should + // eventually reach GCM servers even if this particular attempt fails. + std::move(callback).Run( + was_subscribed + ? blink::mojom::PushUnregistrationStatus::SUCCESS_UNREGISTERED + : blink::mojom::PushUnregistrationStatus::SUCCESS_WAS_NOT_REGISTERED); + + if (PushMessagingAppIdentifier::UseInstanceID(app_id)) { + GetInstanceIDDriver()->GetInstanceID(app_id)->DeleteID( + base::BindOnce(&PushMessagingServiceImpl::DidDeleteID, + weak_factory_.GetWeakPtr(), app_id, was_subscribed)); + + } else { + auto unregister_callback = + base::BindOnce(&PushMessagingServiceImpl::DidUnregister, + weak_factory_.GetWeakPtr(), was_subscribed); +#if defined(OS_ANDROID) + // On Android the backend is different, and requires the original sender_id. + // DidGetSenderIdUnexpectedUnsubscribe and + // DidDeleteServiceWorkerRegistration sometimes call us with an empty one. + if (sender_id.empty()) { + std::move(unregister_callback).Run(gcm::GCMClient::INVALID_PARAMETER); + } else { + GetGCMDriver()->UnregisterWithSenderId( + app_id, push_messaging::NormalizeSenderInfo(sender_id), + std::move(unregister_callback)); + } +#else + GetGCMDriver()->Unregister(app_id, std::move(unregister_callback)); +#endif + } +} + +void PushMessagingServiceImpl::DidUnregister(bool was_subscribed, + gcm::GCMClient::Result result) { + RecordUnsubscribeGCMResult(result); + DidUnsubscribe(std::string() /* app_id_when_instance_id */, was_subscribed); +} + +void PushMessagingServiceImpl::DidDeleteID(const std::string& app_id, + bool was_subscribed, + InstanceID::Result result) { + RecordUnsubscribeIIDResult(result); + // DidUnsubscribe must be run asynchronously when passing a non-empty + // |app_id_when_instance_id|, since it calls + // InstanceIDDriver::RemoveInstanceID which deletes the InstanceID itself. + // Calling that immediately would cause a use-after-free in our caller. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(&PushMessagingServiceImpl::DidUnsubscribe, + weak_factory_.GetWeakPtr(), app_id, was_subscribed)); +} + +void PushMessagingServiceImpl::DidUnsubscribe( + const std::string& app_id_when_instance_id, + bool was_subscribed) { + if (!app_id_when_instance_id.empty()) + GetInstanceIDDriver()->RemoveInstanceID(app_id_when_instance_id); + + if (was_subscribed) + DecreasePushSubscriptionCount(1, false /* was_pending */); + + if (!unsubscribe_callback_for_testing_.is_null()) + std::move(unsubscribe_callback_for_testing_).Run(); +} + +void PushMessagingServiceImpl::SetUnsubscribeCallbackForTesting( + base::OnceClosure callback) { + unsubscribe_callback_for_testing_ = std::move(callback); +} + +// DidDeleteServiceWorkerRegistration methods ---------------------------------- + +void PushMessagingServiceImpl::DidDeleteServiceWorkerRegistration( + const GURL& origin, + int64_t service_worker_registration_id) { + const PushMessagingAppIdentifier& app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker( + profile_, origin, service_worker_registration_id); + if (app_identifier.is_null()) { + if (!service_worker_unregistered_callback_for_testing_.is_null()) + service_worker_unregistered_callback_for_testing_.Run(); + return; + } + // Note this will not fully unsubscribe pre-InstanceID subscriptions on + // Android from GCM, as that requires a sender_id. (Ideally we'd fetch it + // from the SWDB in some "before_unregistered" SWObserver event.) + UnsubscribeInternal( + blink::mojom::PushUnregistrationReason::SERVICE_WORKER_UNREGISTERED, + origin, service_worker_registration_id, app_identifier.app_id(), + std::string() /* sender_id */, + base::BindOnce(&UnregisterCallbackToClosure, + service_worker_unregistered_callback_for_testing_.is_null() + ? base::DoNothing() + : service_worker_unregistered_callback_for_testing_)); +} + +void PushMessagingServiceImpl::SetServiceWorkerUnregisteredCallbackForTesting( + base::RepeatingClosure callback) { + service_worker_unregistered_callback_for_testing_ = std::move(callback); +} + +// DidDeleteServiceWorkerDatabase methods -------------------------------------- + +void PushMessagingServiceImpl::DidDeleteServiceWorkerDatabase() { + std::vector app_identifiers = + PushMessagingAppIdentifier::GetAll(profile_); + + base::RepeatingClosure completed_closure = base::BarrierClosure( + app_identifiers.size(), + service_worker_database_wiped_callback_for_testing_.is_null() + ? base::DoNothing() + : service_worker_database_wiped_callback_for_testing_); + + for (const PushMessagingAppIdentifier& app_identifier : app_identifiers) { + // Note this will not fully unsubscribe pre-InstanceID subscriptions on + // Android from GCM, as that requires a sender_id. We can't fetch those from + // the Service Worker database anymore as it's been deleted. + UnsubscribeInternal( + blink::mojom::PushUnregistrationReason::SERVICE_WORKER_DATABASE_WIPED, + app_identifier.origin(), + app_identifier.service_worker_registration_id(), + app_identifier.app_id(), std::string() /* sender_id */, + base::BindOnce(&UnregisterCallbackToClosure, completed_closure)); + } +} + +void PushMessagingServiceImpl::SetServiceWorkerDatabaseWipedCallbackForTesting( + base::RepeatingClosure callback) { + service_worker_database_wiped_callback_for_testing_ = std::move(callback); +} + +// OnContentSettingChanged methods --------------------------------------------- + +void PushMessagingServiceImpl::OnContentSettingChanged( + const ContentSettingsPattern& primary_pattern, + const ContentSettingsPattern& secondary_pattern, + ContentSettingsTypeSet content_type_set) { + DCHECK(primary_pattern.IsValid()); + if (!content_type_set.Contains(ContentSettingsType::NOTIFICATIONS)) + return; + + std::vector all_app_identifiers = + PushMessagingAppIdentifier::GetAll(profile_); + + base::RepeatingClosure barrier_closure = base::BarrierClosure( + all_app_identifiers.size(), + content_setting_changed_callback_for_testing_.is_null() + ? base::DoNothing() + : content_setting_changed_callback_for_testing_); + + for (const PushMessagingAppIdentifier& app_identifier : all_app_identifiers) { + if (!primary_pattern.Matches(app_identifier.origin())) { + barrier_closure.Run(); + continue; + } + + if (IsPermissionSet(app_identifier.origin())) { + barrier_closure.Run(); + continue; + } + + UnexpectedChange(app_identifier, + blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED, + barrier_closure); + } +} + +void PushMessagingServiceImpl::UnexpectedUnsubscribe( + const PushMessagingAppIdentifier& app_identifier, + blink::mojom::PushUnregistrationReason reason, + UnregisterCallback unregister_callback) { + // When `pushsubscriptionchange` is supported by default, get |sender_id| from + // GetPushSubscriptionFromAppIdentifier callback and do not get the info from + // IO twice + bool need_sender_id = false; +#if defined(OS_ANDROID) + need_sender_id = + !PushMessagingAppIdentifier::UseInstanceID(app_identifier.app_id()); +#endif + if (need_sender_id) { + GetSenderId( + profile_, app_identifier.origin(), + app_identifier.service_worker_registration_id(), + base::BindOnce( + &PushMessagingServiceImpl::DidGetSenderIdUnexpectedUnsubscribe, + weak_factory_.GetWeakPtr(), app_identifier, reason, + std::move(unregister_callback))); + } else { + UnsubscribeInternal(reason, app_identifier.origin(), + app_identifier.service_worker_registration_id(), + app_identifier.app_id(), + std::string() /* sender_id */, + std::move(unregister_callback)); + } +} + +void PushMessagingServiceImpl::GetPushSubscriptionFromAppIdentifier( + const PushMessagingAppIdentifier& app_identifier, + base::OnceCallback + subscription_cb) { + GetSWData(profile_, app_identifier.origin(), + app_identifier.service_worker_registration_id(), + base::BindOnce(&PushMessagingServiceImpl::DidGetSWData, + weak_factory_.GetWeakPtr(), app_identifier, + std::move(subscription_cb))); +} + +void PushMessagingServiceImpl::DidGetSWData( + const PushMessagingAppIdentifier& app_identifier, + base::OnceCallback subscription_cb, + const std::string& sender_id, + const std::string& subscription_id) { + // SW Database was corrupted, return immediately + if (sender_id.empty() || subscription_id.empty()) { + std::move(subscription_cb).Run(nullptr /* subscription */); + return; + } + GetSubscriptionInfo( + app_identifier.origin(), app_identifier.service_worker_registration_id(), + sender_id, subscription_id, + base::BindOnce( + &PushMessagingServiceImpl::GetPushSubscriptionFromAppIdentifierEnd, + weak_factory_.GetWeakPtr(), std::move(subscription_cb), sender_id)); +} + +void PushMessagingServiceImpl::GetPushSubscriptionFromAppIdentifierEnd( + base::OnceCallback callback, + const std::string& sender_id, + bool is_valid, + const GURL& endpoint, + const absl::optional& expiration_time, + const std::vector& p256dh, + const std::vector& auth) { + if (!is_valid) { + // TODO(viviy): Log error in UMA + std::move(callback).Run(nullptr /* subscription */); + return; + } + + std::move(callback).Run(blink::mojom::PushSubscription::New( + endpoint, expiration_time, push_messaging::MakeOptions(sender_id), p256dh, + auth)); +} + +void PushMessagingServiceImpl::FirePushSubscriptionChange( + const PushMessagingAppIdentifier& app_identifier, + base::OnceClosure completed_closure, + blink::mojom::PushSubscriptionPtr new_subscription, + blink::mojom::PushSubscriptionPtr old_subscription) { + // Ensure |completed_closure| is run after this function + base::ScopedClosureRunner scoped_closure(std::move(completed_closure)); + + if (!base::FeatureList::IsEnabled(features::kPushSubscriptionChangeEvent)) + return; + + if (app_identifier.is_null()) { + FirePushSubscriptionChangeCallback( + app_identifier, blink::mojom::PushEventStatus::UNKNOWN_APP_ID); + return; + } + + profile_->FirePushSubscriptionChangeEvent( + app_identifier.origin(), app_identifier.service_worker_registration_id(), + std::move(new_subscription), std::move(old_subscription), + base::BindOnce( + &PushMessagingServiceImpl::FirePushSubscriptionChangeCallback, + weak_factory_.GetWeakPtr(), app_identifier)); +} + +void PushMessagingServiceImpl::FirePushSubscriptionChangeCallback( + const PushMessagingAppIdentifier& app_identifier, + blink::mojom::PushEventStatus status) { + // Log Data in UMA + RecordPushSubcriptionChangeStatus(status); +} + +void PushMessagingServiceImpl::DidGetSenderIdUnexpectedUnsubscribe( + const PushMessagingAppIdentifier& app_identifier, + blink::mojom::PushUnregistrationReason reason, + UnregisterCallback callback, + const std::string& sender_id) { + // Unsubscribe the PushMessagingAppIdentifier with the push service. + // It's possible for GetSenderId to have failed and sender_id to be empty, if + // cookies (and the SW database) for an origin got cleared before permissions + // are cleared for the origin. In that case for legacy GCM registrations on + // Android, Unsubscribe will just delete the app identifier to block future + // messages. + // TODO(johnme): Auto-unregister before SW DB is cleared (crbug.com/402458). + UnsubscribeInternal(reason, app_identifier.origin(), + app_identifier.service_worker_registration_id(), + app_identifier.app_id(), sender_id, std::move(callback)); +} + +void PushMessagingServiceImpl::SetContentSettingChangedCallbackForTesting( + base::RepeatingClosure callback) { + content_setting_changed_callback_for_testing_ = std::move(callback); +} + +// KeyedService methods ------------------------------------------------------- + +void PushMessagingServiceImpl::Shutdown() { + GetGCMDriver()->RemoveAppHandler(kPushMessagingAppIdentifierPrefix); + HostContentSettingsMapFactory::GetForProfile(profile_)->RemoveObserver(this); +} + +// content::NotificationObserver methods --------------------------------------- + +void PushMessagingServiceImpl::Observe( + int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) { + DCHECK_EQ(chrome::NOTIFICATION_APP_TERMINATING, type); + shutdown_started_ = true; +#if BUILDFLAG(ENABLE_BACKGROUND_MODE) + in_flight_keep_alive_.reset(); + in_flight_profile_keep_alive_.reset(); +#endif // BUILDFLAG(ENABLE_BACKGROUND_MODE) +} + +// OnSubscriptionInvalidation methods ------------------------------------------ + +void PushMessagingServiceImpl::OnSubscriptionInvalidation( + const std::string& app_id) { + DCHECK(base::FeatureList::IsEnabled(features::kPushSubscriptionChangeEvent)) + << "It is not allowed to call this method when " + "features::kPushSubscriptionChangeEvent is disabled."; + PushMessagingAppIdentifier old_app_identifier = + PushMessagingAppIdentifier::FindByAppId(profile_, app_id); + if (old_app_identifier.is_null()) + return; + + GetSenderId(profile_, old_app_identifier.origin(), + old_app_identifier.service_worker_registration_id(), + base::BindOnce(&PushMessagingServiceImpl::GetOldSubscription, + weak_factory_.GetWeakPtr(), old_app_identifier)); +} + +void PushMessagingServiceImpl::GetOldSubscription( + PushMessagingAppIdentifier old_app_identifier, + const std::string& sender_id) { + GetPushSubscriptionFromAppIdentifier( + old_app_identifier, + base::BindOnce(&PushMessagingServiceImpl::StartRefresh, + weak_factory_.GetWeakPtr(), old_app_identifier, + sender_id)); +} + +void PushMessagingServiceImpl::StartRefresh( + PushMessagingAppIdentifier old_app_identifier, + const std::string& sender_id, + blink::mojom::PushSubscriptionPtr old_subscription) { + // Generate a new app_identifier with the same information, but a different + // app_id. Expiration time will be overwritten by DoSubscribe, if the flag + // features::kPushSubscriptionWithExpiration time is enabled + PushMessagingAppIdentifier new_app_identifier = + PushMessagingAppIdentifier::Generate( + old_app_identifier.origin(), + old_app_identifier.service_worker_registration_id(), + absl::nullopt /* expiration_time */); + + refresher_.Refresh(old_app_identifier, new_app_identifier.app_id(), + sender_id); + + UpdateSubscription( + new_app_identifier, push_messaging::MakeOptions(sender_id), + base::BindOnce(&PushMessagingServiceImpl::DidUpdateSubscription, + weak_factory_.GetWeakPtr(), new_app_identifier.app_id(), + old_app_identifier.app_id(), std::move(old_subscription), + sender_id)); +} + +void PushMessagingServiceImpl::UpdateSubscription( + PushMessagingAppIdentifier app_identifier, + blink::mojom::PushSubscriptionOptionsPtr options, + RegisterCallback callback) { + // After getting a new GCM registration, update the |subscription_id| in SW + // database before running the callback + auto register_callback = base::BindOnce( + [](RegisterCallback cb, Profile* profile, PushMessagingAppIdentifier ai, + const std::string& registration_id, const GURL& endpoint, + const absl::optional& expiration_time, + const std::vector& p256dh, const std::vector& auth, + blink::mojom::PushRegistrationStatus status) { + base::OnceClosure closure = + base::BindOnce(std::move(cb), registration_id, endpoint, + expiration_time, p256dh, auth, status); + base::ScopedClosureRunner closure_runner(std::move(closure)); + if (status == + blink::mojom::PushRegistrationStatus::SUCCESS_FROM_PUSH_SERVICE) { + UpdatePushSubscriptionId(profile, ai.origin(), + ai.service_worker_registration_id(), + registration_id, closure_runner.Release()); + } + }, + std::move(callback), profile_, app_identifier); + // Subscribe using the new subscription information, this will overwrite + // the expiration time of |app_identifier| + DoSubscribe(app_identifier, std::move(options), std::move(register_callback), + -1 /* render_process_id */, -1 /* render_frame_id */, + CONTENT_SETTING_ALLOW); +} + +void PushMessagingServiceImpl::DidUpdateSubscription( + const std::string& new_app_id, + const std::string& old_app_id, + blink::mojom::PushSubscriptionPtr old_subscription, + const std::string& sender_id, + const std::string& registration_id, + const GURL& endpoint, + const absl::optional& expiration_time, + const std::vector& p256dh, + const std::vector& auth, + blink::mojom::PushRegistrationStatus status) { + // TODO(crbug.com/1122545): Currently, if |status| is unsuccessful, the old + // subscription remains in SW database and preferences and the refresh is + // aborted. Instead, one should abort the refresh and retry to refresh + // periodically. + if (status != + blink::mojom::PushRegistrationStatus::SUCCESS_FROM_PUSH_SERVICE) { + return; + } + + // Old subscription is now replaced locally by the new subscription + refresher_.OnSubscriptionUpdated(new_app_id); + + PushMessagingAppIdentifier new_app_identifier = + PushMessagingAppIdentifier::FindByAppId(profile_, new_app_id); + + // Callback for testing + base::OnceClosure callback = + (invalidation_callback_for_testing_) + ? std::move(invalidation_callback_for_testing_) + : base::DoNothing(); + + FirePushSubscriptionChange( + new_app_identifier, std::move(callback), + blink::mojom::PushSubscription::New( + endpoint, expiration_time, push_messaging::MakeOptions(sender_id), + p256dh, auth), + std::move(old_subscription)); +} + +// PushMessagingRefresher::Observer methods ------------------------------------ + +void PushMessagingServiceImpl::OnOldSubscriptionExpired( + const std::string& app_id, + const std::string& sender_id) { + // Unsubscribe without clearing SW database, since values of the new + // subscription are already saved there. + // After unsubscribing, the refresher will get notified. + UnsubscribeInternal( + blink::mojom::PushUnregistrationReason::REFRESH_FINISHED, + GURL::EmptyGURL() /* origin */, -1 /* service_worker_registration_id */, + app_id, sender_id, + base::BindOnce(&UnregisterCallbackToClosure, + base::BindOnce(&PushMessagingRefresher::OnUnsubscribed, + refresher_.GetWeakPtr(), app_id))); +} + +void PushMessagingServiceImpl::OnRefreshFinished( + const PushMessagingAppIdentifier& app_identifier) { + // TODO(viviy): Log data in UMA +} + +void PushMessagingServiceImpl::SetInvalidationCallbackForTesting( + base::OnceClosure callback) { + invalidation_callback_for_testing_ = std::move(callback); +} + +// Helper methods -------------------------------------------------------------- + +void PushMessagingServiceImpl::SetRemoveExpiredSubscriptionsCallbackForTesting( + base::OnceClosure closure) { + remove_expired_subscriptions_callback_for_testing_ = std::move(closure); +} + +// Assumes user_visible always since this is just meant to check +// if the permission was previously granted and not revoked. +bool PushMessagingServiceImpl::IsPermissionSet(const GURL& origin, + bool user_visible) { + return GetPermissionStatus(origin, user_visible) == + blink::mojom::PermissionStatus::GRANTED; +} + +void PushMessagingServiceImpl::GetEncryptionInfoForAppId( + const std::string& app_id, + const std::string& sender_id, + gcm::GCMEncryptionProvider::EncryptionInfoCallback callback) { + if (PushMessagingAppIdentifier::UseInstanceID(app_id)) { + GetInstanceIDDriver()->GetInstanceID(app_id)->GetEncryptionInfo( + push_messaging::NormalizeSenderInfo(sender_id), std::move(callback)); + } else { + GetGCMDriver()->GetEncryptionInfo(app_id, std::move(callback)); + } +} + +gcm::GCMDriver* PushMessagingServiceImpl::GetGCMDriver() const { + gcm::GCMProfileService* gcm_profile_service = + gcm::GCMProfileServiceFactory::GetForProfile(profile_); + CHECK(gcm_profile_service); + CHECK(gcm_profile_service->driver()); + return gcm_profile_service->driver(); +} + +instance_id::InstanceIDDriver* PushMessagingServiceImpl::GetInstanceIDDriver() + const { + instance_id::InstanceIDProfileService* instance_id_profile_service = + instance_id::InstanceIDProfileServiceFactory::GetForProfile(profile_); + CHECK(instance_id_profile_service); + CHECK(instance_id_profile_service->driver()); + return instance_id_profile_service->driver(); +} + +content::DevToolsBackgroundServicesContext* +PushMessagingServiceImpl::GetDevToolsContext(const GURL& origin) const { + auto* storage_partition = profile_->GetStoragePartitionForUrl(origin); + if (!storage_partition) + return nullptr; + + auto* devtools_context = + storage_partition->GetDevToolsBackgroundServicesContext(); + + if (!devtools_context->IsRecording( + content::DevToolsBackgroundService::kPushMessaging)) { + return nullptr; + } + + return devtools_context; +} diff --git a/chromium/chrome/browser/push_messaging/push_messaging_service_impl.h b/chromium/chrome/browser/push_messaging/push_messaging_service_impl.h new file mode 100644 index 00000000000..a99e6e578f4 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_service_impl.h @@ -0,0 +1,475 @@ +// 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 CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_IMPL_H_ +#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_IMPL_H_ + +#include +#include +#include +#include +#include + +#include "base/callback.h" +#include "base/callback_helpers.h" +#include "base/containers/flat_map.h" +#include "base/gtest_prod_util.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/scoped_observation.h" +#include "base/time/time.h" +#include "chrome/browser/permissions/abusive_origin_permission_revocation_request.h" +#include "chrome/browser/push_messaging/push_messaging_notification_manager.h" +#include "chrome/browser/push_messaging/push_messaging_refresher.h" +#include "chrome/common/buildflags.h" +#include "components/content_settings/core/browser/content_settings_observer.h" +#include "components/content_settings/core/common/content_settings.h" +#include "components/content_settings/core/common/content_settings_types.h" +#include "components/gcm_driver/common/gcm_message.h" +#include "components/gcm_driver/crypto/gcm_encryption_provider.h" +#include "components/gcm_driver/gcm_app_handler.h" +#include "components/gcm_driver/gcm_client.h" +#include "components/gcm_driver/instance_id/instance_id.h" +#include "components/keyed_service/core/keyed_service.h" +#include "content/public/browser/notification_observer.h" +#include "content/public/browser/notification_registrar.h" +#include "content/public/browser/push_messaging_service.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#include "third_party/blink/public/mojom/push_messaging/push_messaging.mojom-forward.h" + +class GURL; +class Profile; +class PushMessagingAppIdentifier; +class PushMessagingServiceTest; +class ScopedKeepAlive; +class ScopedProfileKeepAlive; + +namespace blink { +namespace mojom { +enum class PushEventStatus; +enum class PushRegistrationStatus; +} // namespace mojom +} // namespace blink + +namespace content { +class DevToolsBackgroundServicesContext; +} // namespace content + +namespace gcm { +class GCMDriver; +} // namespace gcm + +namespace instance_id { +class InstanceIDDriver; +} // namespace instance_id + +namespace { +struct PendingMessage { + PendingMessage(std::string app_id, gcm::IncomingMessage message); + PendingMessage(const PendingMessage& other); + PendingMessage(PendingMessage&& other); + ~PendingMessage(); + + PendingMessage& operator=(PendingMessage&& other); + + std::string app_id; + gcm::IncomingMessage message; + base::Time received_time; +}; +} // namespace + +class PushMessagingServiceImpl : public content::PushMessagingService, + public gcm::GCMAppHandler, + public content_settings::Observer, + public KeyedService, + public content::NotificationObserver, + public PushMessagingRefresher::Observer { + public: + // If any Service Workers are using push, starts GCM and adds an app handler. + static void InitializeForProfile(Profile* profile); + + explicit PushMessagingServiceImpl(Profile* profile); + + PushMessagingServiceImpl(const PushMessagingServiceImpl&) = delete; + PushMessagingServiceImpl& operator=(const PushMessagingServiceImpl&) = delete; + + ~PushMessagingServiceImpl() override; + + // Check and remove subscriptions that are expired when |this| is initialized + void RemoveExpiredSubscriptions(); + + // Gets the permission status for the given |origin|. + blink::mojom::PermissionStatus GetPermissionStatus(const GURL& origin, + bool user_visible); + + // gcm::GCMAppHandler implementation. + void ShutdownHandler() override; + void OnStoreReset() override; + void OnMessage(const std::string& app_id, + const gcm::IncomingMessage& message) override; + void OnMessagesDeleted(const std::string& app_id) override; + void OnSendError( + const std::string& app_id, + const gcm::GCMClient::SendErrorDetails& send_error_details) override; + void OnSendAcknowledged(const std::string& app_id, + const std::string& message_id) override; + void OnMessageDecryptionFailed(const std::string& app_id, + const std::string& message_id, + const std::string& error_message) override; + bool CanHandle(const std::string& app_id) const override; + + // content::PushMessagingService implementation: + void SubscribeFromDocument(const GURL& requesting_origin, + int64_t service_worker_registration_id, + int render_process_id, + int render_frame_id, + blink::mojom::PushSubscriptionOptionsPtr options, + bool user_gesture, + RegisterCallback callback) override; + void SubscribeFromWorker(const GURL& requesting_origin, + int64_t service_worker_registration_id, + blink::mojom::PushSubscriptionOptionsPtr options, + RegisterCallback callback) override; + void GetSubscriptionInfo(const GURL& origin, + int64_t service_worker_registration_id, + const std::string& sender_id, + const std::string& subscription_id, + SubscriptionInfoCallback callback) override; + void Unsubscribe(blink::mojom::PushUnregistrationReason reason, + const GURL& requesting_origin, + int64_t service_worker_registration_id, + const std::string& sender_id, + UnregisterCallback) override; + bool SupportNonVisibleMessages() override; + void DidDeleteServiceWorkerRegistration( + const GURL& origin, + int64_t service_worker_registration_id) override; + void DidDeleteServiceWorkerDatabase() override; + + // content_settings::Observer implementation. + void OnContentSettingChanged( + const ContentSettingsPattern& primary_pattern, + const ContentSettingsPattern& secondary_pattern, + ContentSettingsTypeSet content_type_set) override; + + // Fires the `pushsubscriptionchange` event to the associated service worker + // of |app_identifier|, which is the app identifier for |old_subscription| + // whereas |new_subscription| can be either null e.g. when a subscription is + // lost due to permission changes or a new subscription when it was refreshed. + void FirePushSubscriptionChange( + const PushMessagingAppIdentifier& app_identifier, + base::OnceClosure completed_closure, + blink::mojom::PushSubscriptionPtr new_subscription, + blink::mojom::PushSubscriptionPtr old_subscription); + + // KeyedService implementation. + void Shutdown() override; + + // content::NotificationObserver implementation + void Observe(int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) override; + + // WARNING: Only call this function if features::kPushSubscriptionChangeEvent + // is enabled, will be later used by the Push Service to trigger subscription + // refreshes + void OnSubscriptionInvalidation(const std::string& app_id); + + // PushMessagingRefresher::Observer implementation + // Initiate unsubscribe task when old subscription becomes invalid + void OnOldSubscriptionExpired(const std::string& app_id, + const std::string& sender_id) override; + void OnRefreshFinished( + const PushMessagingAppIdentifier& app_identifier) override; + + void SetMessageCallbackForTesting(const base::RepeatingClosure& callback); + void SetUnsubscribeCallbackForTesting(base::OnceClosure callback); + void SetInvalidationCallbackForTesting(base::OnceClosure callback); + void SetContentSettingChangedCallbackForTesting( + base::RepeatingClosure callback); + void SetServiceWorkerUnregisteredCallbackForTesting( + base::RepeatingClosure callback); + void SetServiceWorkerDatabaseWipedCallbackForTesting( + base::RepeatingClosure callback); + void SetRemoveExpiredSubscriptionsCallbackForTesting( + base::OnceClosure closure); + + private: + friend class PushMessagingBrowserTestBase; + friend class PushMessagingServiceTest; + FRIEND_TEST_ALL_PREFIXES(PushMessagingServiceTest, NormalizeSenderInfo); + FRIEND_TEST_ALL_PREFIXES(PushMessagingServiceTest, PayloadEncryptionTest); + FRIEND_TEST_ALL_PREFIXES(PushMessagingServiceTest, + TestMultipleIncomingPushMessages); + + // A subscription is pending until it has succeeded or failed. + void IncreasePushSubscriptionCount(int add, bool is_pending); + void DecreasePushSubscriptionCount(int subtract, bool was_pending); + + // OnMessage methods --------------------------------------------------------- + + void DeliverMessageCallback(const std::string& app_id, + const GURL& requesting_origin, + int64_t service_worker_registration_id, + const gcm::IncomingMessage& message, + bool did_enqueue_message, + blink::mojom::PushEventStatus status); + + void DidHandleEnqueuedMessage( + const GURL& origin, + int64_t service_worker_registration_id, + base::OnceCallback message_handled_callback, + bool did_show_generic_notification); + + void DidHandleMessage(const std::string& app_id, + const std::string& push_message_id, + bool did_show_generic_notification); + + void OnCheckedOriginForAbuse( + PendingMessage message, + AbusiveOriginPermissionRevocationRequest::Outcome outcome); + + void DeliverNextQueuedMessageForServiceWorkerRegistration( + const GURL& origin, + int64_t service_worker_registration_id); + + void CheckOriginForAbuseAndDispatchNextMessage(); + + // Subscribe methods --------------------------------------------------------- + + void DoSubscribe(PushMessagingAppIdentifier app_identifier, + blink::mojom::PushSubscriptionOptionsPtr options, + RegisterCallback callback, + int render_process_id, + int render_frame_id, + ContentSetting permission_status); + + void SubscribeEnd(RegisterCallback callback, + const std::string& subscription_id, + const GURL& endpoint, + const absl::optional& expiration_time, + const std::vector& p256dh, + const std::vector& auth, + blink::mojom::PushRegistrationStatus status); + + void SubscribeEndWithError(RegisterCallback callback, + blink::mojom::PushRegistrationStatus status); + + void DidSubscribe(const PushMessagingAppIdentifier& app_identifier, + const std::string& sender_id, + RegisterCallback callback, + const std::string& subscription_id, + instance_id::InstanceID::Result result); + + void DidSubscribeWithEncryptionInfo( + const PushMessagingAppIdentifier& app_identifier, + RegisterCallback callback, + const std::string& subscription_id, + const GURL& endpoint, + std::string p256dh, + std::string auth_secret); + + // GetSubscriptionInfo methods ----------------------------------------------- + + void DidValidateSubscription( + const std::string& app_id, + const std::string& sender_id, + const GURL& endpoint, + const absl::optional& expiration_time, + SubscriptionInfoCallback callback, + bool is_valid); + + void DidGetEncryptionInfo(const GURL& endpoint, + const absl::optional& expiration_time, + SubscriptionInfoCallback callback, + std::string p256dh, + std::string auth_secret) const; + + // Unsubscribe methods ------------------------------------------------------- + + // |origin|, |service_worker_registration_id| and |app_id| should be provided + // whenever they can be obtained. It's valid for |origin| to be empty and + // |service_worker_registration_id| to be kInvalidServiceWorkerRegistrationId, + // or for app_id to be empty, but not both at once. + void UnsubscribeInternal(blink::mojom::PushUnregistrationReason reason, + const GURL& origin, + int64_t service_worker_registration_id, + const std::string& app_id, + const std::string& sender_id, + UnregisterCallback callback); + + void DidClearPushSubscriptionId(blink::mojom::PushUnregistrationReason reason, + const std::string& app_id, + const std::string& sender_id, + UnregisterCallback callback); + + void DidUnregister(bool was_subscribed, gcm::GCMClient::Result result); + void DidDeleteID(const std::string& app_id, + bool was_subscribed, + instance_id::InstanceID::Result result); + void DidUnsubscribe(const std::string& app_id_when_instance_id, + bool was_subscribed); + + // OnContentSettingChanged methods ------------------------------------------- + + void GetPushSubscriptionFromAppIdentifier( + const PushMessagingAppIdentifier& app_identifier, + base::OnceCallback callback); + + void DidGetSWData( + const PushMessagingAppIdentifier& app_identifier, + base::OnceCallback callback, + const std::string& sender_id, + const std::string& subscription_id); + + void GetPushSubscriptionFromAppIdentifierEnd( + base::OnceCallback callback, + const std::string& sender_id, + bool is_valid, + const GURL& endpoint, + const absl::optional& expiration_time, + const std::vector& p256dh, + const std::vector& auth); + + // OnSubscriptionInvalidation methods----------------------------------------- + + void GetOldSubscription(PushMessagingAppIdentifier old_app_identifier, + const std::string& sender_id); + + // After gathering all relavent information to start the refresh, + // generate a new app id and initiate refresh + void StartRefresh(PushMessagingAppIdentifier old_app_identifier, + const std::string& sender_id, + blink::mojom::PushSubscriptionPtr old_subscription); + + // Makes a new susbcription and replaces the old subscription by new + // subscription in preferences and service worker database + void UpdateSubscription(PushMessagingAppIdentifier app_identifier, + blink::mojom::PushSubscriptionOptionsPtr options, + RegisterCallback callback); + + // After the subscription is updated, fire a `pushsubscriptionchange` event + // and notify the |refresher_| + void DidUpdateSubscription(const std::string& new_app_id, + const std::string& old_app_id, + blink::mojom::PushSubscriptionPtr old_subscription, + const std::string& sender_id, + const std::string& registration_id, + const GURL& endpoint, + const absl::optional& expiration_time, + const std::vector& p256dh, + const std::vector& auth, + blink::mojom::PushRegistrationStatus status); + // Helper methods ------------------------------------------------------------ + + // The subscription given in |identifier| will be unsubscribed (and a + // `pushsubscriptionchange` event fires if + // features::kPushSubscriptionChangeEvent is enabled) + void UnexpectedChange(PushMessagingAppIdentifier identifier, + blink::mojom::PushUnregistrationReason reason, + base::OnceClosure completed_closure); + + void UnexpectedUnsubscribe(const PushMessagingAppIdentifier& app_identifier, + blink::mojom::PushUnregistrationReason reason, + UnregisterCallback unregister_callback); + + void DidGetSenderIdUnexpectedUnsubscribe( + const PushMessagingAppIdentifier& app_identifier, + blink::mojom::PushUnregistrationReason reason, + UnregisterCallback callback, + const std::string& sender_id); + + void FirePushSubscriptionChangeCallback( + const PushMessagingAppIdentifier& app_identifier, + blink::mojom::PushEventStatus status); + + // Checks if a given origin is allowed to use Push. + bool IsPermissionSet(const GURL& origin, bool user_visible = true); + + // Wrapper around {GCMDriver, InstanceID}::GetEncryptionInfo. + void GetEncryptionInfoForAppId( + const std::string& app_id, + const std::string& sender_id, + gcm::GCMEncryptionProvider::EncryptionInfoCallback callback); + + gcm::GCMDriver* GetGCMDriver() const; + + instance_id::InstanceIDDriver* GetInstanceIDDriver() const; + + content::DevToolsBackgroundServicesContext* GetDevToolsContext( + const GURL& origin) const; + + // Testing methods ----------------------------------------------------------- + + using PushEventCallback = + base::OnceCallback; + using MessageDispatchedCallback = + base::RepeatingCallback payload, + PushEventCallback callback)>; + + // Callback to be invoked when a message has been dispatched. Enables tests to + // observe message delivery instead of delivering it to the Service Worker. + void SetMessageDispatchedCallbackForTesting( + const MessageDispatchedCallback& callback) { + message_dispatched_callback_for_testing_ = callback; + } + + raw_ptr profile_; + std::unique_ptr + abusive_origin_revocation_request_; + std::queue messages_pending_permission_check_; + + // {Origin, ServiceWokerRegistratonId} key for message delivery queue. This + // ensures that we only deliver one message at a time per ServiceWorker. + using MessageDeliveryQueueKey = std::pair; + + // Queue of pending messages per ServiceWorkerRegstration to be delivered one + // at a time. This allows us to enforce visibility requirements. + base::flat_map> + message_delivery_queue_; + + int push_subscription_count_; + int pending_push_subscription_count_; + + base::RepeatingClosure message_callback_for_testing_; + base::OnceClosure unsubscribe_callback_for_testing_; + base::RepeatingClosure content_setting_changed_callback_for_testing_; + base::RepeatingClosure service_worker_unregistered_callback_for_testing_; + base::RepeatingClosure service_worker_database_wiped_callback_for_testing_; + base::OnceClosure remove_expired_subscriptions_callback_for_testing_; + base::OnceClosure invalidation_callback_for_testing_; + + PushMessagingNotificationManager notification_manager_; + + PushMessagingRefresher refresher_; + + base::ScopedObservation + refresh_observation_{this}; + + MessageDispatchedCallback message_dispatched_callback_for_testing_; + +#if BUILDFLAG(ENABLE_BACKGROUND_MODE) + // KeepAlive registered while we have in-flight push messages, to make sure + // we can finish processing them without being interrupted by BrowserProcess + // teardown. + std::unique_ptr in_flight_keep_alive_; + + // Same as ScopedKeepAlive, but prevents |profile_| from getting deleted. + std::unique_ptr in_flight_profile_keep_alive_; +#endif + + content::NotificationRegistrar registrar_; + + // True when shutdown has started. Do not allow processing of incoming + // messages when this is true. + bool shutdown_started_ = false; + + base::WeakPtrFactory weak_factory_{this}; +}; + +#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_IMPL_H_ diff --git a/chromium/chrome/browser/push_messaging/push_messaging_service_unittest.cc b/chromium/chrome/browser/push_messaging/push_messaging_service_unittest.cc new file mode 100644 index 00000000000..612589a5107 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_service_unittest.cc @@ -0,0 +1,480 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/public/browser/push_messaging_service.h" + +#include + +#include +#include + +#include "base/bind.h" +#include "base/command_line.h" +#include "base/cxx17_backports.h" +#include "base/run_loop.h" +#include "base/test/bind.h" +#include "base/test/metrics/histogram_tester.h" +#include "base/test/scoped_feature_list.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "chrome/browser/content_settings/host_content_settings_map_factory.h" +#include "chrome/browser/gcm/gcm_profile_service_factory.h" +#include "chrome/browser/permissions/permission_manager_factory.h" +#include "chrome/browser/push_messaging/push_messaging_app_identifier.h" +#include "chrome/browser/push_messaging/push_messaging_features.h" +#include "chrome/browser/push_messaging/push_messaging_service_factory.h" +#include "chrome/browser/push_messaging/push_messaging_service_impl.h" +#include "chrome/browser/push_messaging/push_messaging_utils.h" +#include "chrome/test/base/testing_profile.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/gcm_driver/crypto/gcm_crypto_test_helpers.h" +#include "components/gcm_driver/fake_gcm_client_factory.h" +#include "components/gcm_driver/fake_gcm_profile_service.h" +#include "components/gcm_driver/gcm_profile_service.h" +#include "components/permissions/permission_manager.h" +#include "content/public/common/content_features.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#include "third_party/blink/public/mojom/push_messaging/push_messaging_status.mojom.h" + +#if defined(OS_ANDROID) +#include "components/gcm_driver/instance_id/instance_id_android.h" +#include "components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.h" +#endif // OS_ANDROID + +namespace { + +const char kTestOrigin[] = "https://example.com"; +const char kTestSenderId[] = "1234567890"; +const int64_t kTestServiceWorkerId = 42; +const char kTestPayload[] = "Hello, world!"; + +// NIST P-256 public key in uncompressed format per SEC1 2.3.3. +const uint8_t kTestP256Key[] = { + 0x04, 0x55, 0x52, 0x6A, 0xA5, 0x6E, 0x8E, 0xAA, 0x47, 0x97, 0x36, + 0x10, 0xC1, 0x66, 0x3C, 0x1E, 0x65, 0xBF, 0xA1, 0x7B, 0xEE, 0x48, + 0xC9, 0xC6, 0xBB, 0xBF, 0x02, 0x18, 0x53, 0x72, 0x1D, 0x0C, 0x7B, + 0xA9, 0xE3, 0x11, 0xB7, 0x03, 0x52, 0x21, 0xD3, 0x71, 0x90, 0x13, + 0xA8, 0xC1, 0xCF, 0xED, 0x20, 0xF7, 0x1F, 0xD1, 0x7F, 0xF2, 0x76, + 0xB6, 0x01, 0x20, 0xD8, 0x35, 0xA5, 0xD9, 0x3C, 0x43, 0xFD}; + +static_assert(sizeof(kTestP256Key) == 65, + "The fake public key must be a valid P-256 uncompressed point."); + +// URL-safe base64 encoded version of the |kTestP256Key|. +const char kTestEncodedP256Key[] = + "BFVSaqVujqpHlzYQwWY8HmW_oXvuSMnGu78CGFNyHQx7qeMRtwNSIdNxkBOowc_tIPcf0X_ydr" + "YBINg1pdk8Q_0"; + +// Implementation of the TestingProfile that provides the Push Messaging Service +// and the Permission Manager, both of which are required for the tests. +class PushMessagingTestingProfile : public TestingProfile { + public: + PushMessagingTestingProfile() = default; + + PushMessagingTestingProfile(const PushMessagingTestingProfile&) = delete; + PushMessagingTestingProfile& operator=(const PushMessagingTestingProfile&) = + delete; + + ~PushMessagingTestingProfile() override = default; + + PushMessagingServiceImpl* GetPushMessagingService() override { + return PushMessagingServiceFactory::GetForProfile(this); + } + + permissions::PermissionManager* GetPermissionControllerDelegate() override { + return PermissionManagerFactory::GetForProfile(this); + } +}; + +std::unique_ptr BuildFakeGCMProfileService( + content::BrowserContext* context) { + return gcm::FakeGCMProfileService::Build(static_cast(context)); +} + +constexpr base::TimeDelta kPushEventHandleTime = base::Seconds(10); + +} // namespace + +class PushMessagingServiceTest : public ::testing::Test { + public: + PushMessagingServiceTest() { + // Always allow push notifications in the profile. + HostContentSettingsMap* host_content_settings_map = + HostContentSettingsMapFactory::GetForProfile(&profile_); + host_content_settings_map->SetDefaultContentSetting( + ContentSettingsType::NOTIFICATIONS, CONTENT_SETTING_ALLOW); + + // Override the GCM Profile service so that we can send fake messages. + gcm::GCMProfileServiceFactory::GetInstance()->SetTestingFactory( + &profile_, base::BindRepeating(&BuildFakeGCMProfileService)); + } + + ~PushMessagingServiceTest() override = default; + + // Callback to use when the subscription may have been subscribed. + void DidRegister(std::string* subscription_id_out, + GURL* endpoint_out, + absl::optional* expiration_time_out, + std::vector* p256dh_out, + std::vector* auth_out, + base::OnceClosure done_callback, + const std::string& registration_id, + const GURL& endpoint, + const absl::optional& expiration_time, + const std::vector& p256dh, + const std::vector& auth, + blink::mojom::PushRegistrationStatus status) { + EXPECT_EQ(blink::mojom::PushRegistrationStatus::SUCCESS_FROM_PUSH_SERVICE, + status); + + *subscription_id_out = registration_id; + *expiration_time_out = expiration_time; + *endpoint_out = endpoint; + *p256dh_out = p256dh; + *auth_out = auth; + + std::move(done_callback).Run(); + } + + // Callback to use when observing messages dispatched by the push service. + void DidDispatchMessage( + std::string* app_id_out, + GURL* origin_out, + int64_t* service_worker_registration_id_out, + absl::optional* payload_out, + const std::string& app_id, + const GURL& origin, + int64_t service_worker_registration_id, + absl::optional payload, + PushMessagingServiceImpl::PushEventCallback callback) { + *app_id_out = app_id; + *origin_out = origin; + *service_worker_registration_id_out = service_worker_registration_id; + *payload_out = std::move(payload); + } + + class TestPushSubscription { + public: + std::string subscription_id_; + GURL endpoint_; + absl::optional expiration_time_; + std::vector p256dh_; + std::vector auth_; + TestPushSubscription(const std::string& subscription_id, + const GURL& endpoint, + const absl::optional& expiration_time, + const std::vector& p256dh, + const std::vector& auth) + : subscription_id_(subscription_id), + endpoint_(endpoint), + expiration_time_(expiration_time), + p256dh_(p256dh), + auth_(auth) {} + TestPushSubscription() = default; + }; + + void Subscribe(PushMessagingServiceImpl* push_service, + const GURL& origin, + TestPushSubscription* subscription = nullptr) { + std::string subscription_id; + GURL endpoint; + absl::optional expiration_time; + std::vector p256dh, auth; + + base::RunLoop run_loop; + + auto options = blink::mojom::PushSubscriptionOptions::New(); + options->user_visible_only = true; + options->application_server_key = std::vector( + kTestSenderId, + kTestSenderId + sizeof(kTestSenderId) / sizeof(char) - 1); + + push_service->SubscribeFromWorker( + origin, kTestServiceWorkerId, std::move(options), + base::BindOnce(&PushMessagingServiceTest::DidRegister, + base::Unretained(this), &subscription_id, &endpoint, + &expiration_time, &p256dh, &auth, + run_loop.QuitClosure())); + + EXPECT_EQ(0u, subscription_id.size()); // this must be asynchronous + + run_loop.Run(); + + ASSERT_GT(subscription_id.size(), 0u); + ASSERT_TRUE(endpoint.is_valid()); + ASSERT_GT(endpoint.spec().size(), 0u); + ASSERT_GT(p256dh.size(), 0u); + ASSERT_GT(auth.size(), 0u); + + if (subscription) { + subscription->subscription_id_ = subscription_id; + subscription->endpoint_ = endpoint; + subscription->p256dh_ = p256dh; + subscription->auth_ = auth; + } + } + + protected: + PushMessagingTestingProfile* profile() { return &profile_; } + + content::BrowserTaskEnvironment& task_environment() { + return task_environment_; + } + + private: + content::BrowserTaskEnvironment task_environment_{ + base::test::TaskEnvironment::TimeSource::MOCK_TIME}; + PushMessagingTestingProfile profile_; + +#if defined(OS_ANDROID) + instance_id::InstanceIDAndroid::ScopedBlockOnAsyncTasksForTesting + block_async_; +#endif // OS_ANDROID +}; + +// Fails too often on Linux TSAN builder: http://crbug.com/1211350. +#if defined(OS_LINUX) && defined(THREAD_SANITIZER) +#define MAYBE_PayloadEncryptionTest DISABLED_PayloadEncryptionTest +#else +#define MAYBE_PayloadEncryptionTest PayloadEncryptionTest +#endif +TEST_F(PushMessagingServiceTest, MAYBE_PayloadEncryptionTest) { + PushMessagingServiceImpl* push_service = profile()->GetPushMessagingService(); + ASSERT_TRUE(push_service); + + const GURL origin(kTestOrigin); + + // (1) Make sure that |kExampleOrigin| has access to use Push Messaging. + ASSERT_EQ(blink::mojom::PermissionStatus::GRANTED, + push_service->GetPermissionStatus(origin, true /* user_visible */)); + + // (2) Subscribe for Push Messaging, and verify that we've got the required + // information in order to be able to create encrypted messages. + TestPushSubscription subscription; + Subscribe(push_service, origin, &subscription); + + // (3) Encrypt a message using the public key and authentication secret that + // are associated with the subscription. + + gcm::IncomingMessage message; + message.sender_id = kTestSenderId; + + ASSERT_TRUE(gcm::CreateEncryptedPayloadForTesting( + kTestPayload, + base::StringPiece(reinterpret_cast(subscription.p256dh_.data()), + subscription.p256dh_.size()), + base::StringPiece(reinterpret_cast(subscription.auth_.data()), + subscription.auth_.size()), + &message)); + + ASSERT_GT(message.raw_data.size(), 0u); + ASSERT_NE(kTestPayload, message.raw_data); + ASSERT_FALSE(message.decrypted); + + // (4) Find the app_id that has been associated with the subscription. + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker(profile(), origin, + kTestServiceWorkerId); + + ASSERT_FALSE(app_identifier.is_null()); + + std::string app_id; + GURL dispatched_origin; + int64_t service_worker_registration_id; + absl::optional payload; + + // (5) Observe message dispatchings from the Push Messaging service, and + // then dispatch the |message| on the GCM driver as if it had actually + // been received by Google Cloud Messaging. + push_service->SetMessageDispatchedCallbackForTesting(base::BindRepeating( + &PushMessagingServiceTest::DidDispatchMessage, base::Unretained(this), + &app_id, &dispatched_origin, &service_worker_registration_id, &payload)); + + gcm::FakeGCMProfileService* fake_profile_service = + static_cast( + gcm::GCMProfileServiceFactory::GetForProfile(profile())); + + fake_profile_service->DispatchMessage(app_identifier.app_id(), message); + + base::RunLoop().RunUntilIdle(); + + // (6) Verify that the message, as received by the Push Messaging Service, has + // indeed been decrypted by the GCM Driver, and has been forwarded to the + // Service Worker that has been associated with the subscription. + EXPECT_EQ(app_identifier.app_id(), app_id); + EXPECT_EQ(origin, dispatched_origin); + EXPECT_EQ(service_worker_registration_id, kTestServiceWorkerId); + + EXPECT_TRUE(payload); + EXPECT_EQ(kTestPayload, *payload); +} + +TEST_F(PushMessagingServiceTest, NormalizeSenderInfo) { + PushMessagingServiceImpl* push_service = profile()->GetPushMessagingService(); + ASSERT_TRUE(push_service); + + std::string p256dh(kTestP256Key, kTestP256Key + base::size(kTestP256Key)); + ASSERT_EQ(65u, p256dh.size()); + + // NIST P-256 public keys in uncompressed format will be encoded using the + // URL-safe base64 encoding by the normalization function. + EXPECT_EQ(kTestEncodedP256Key, push_messaging::NormalizeSenderInfo(p256dh)); + + // Any other value, binary or not, will be passed through as-is. + EXPECT_EQ("1234567890", push_messaging::NormalizeSenderInfo("1234567890")); + EXPECT_EQ("foo@bar.com", push_messaging::NormalizeSenderInfo("foo@bar.com")); + + p256dh[0] = 0x05; // invalidate |p256dh| as a public key. + + EXPECT_EQ(p256dh, push_messaging::NormalizeSenderInfo(p256dh)); +} + +// Fails too often on Linux TSAN builder: http://crbug.com/1211350. +#if defined(OS_LINUX) && defined(THREAD_SANITIZER) +#define MAYBE_RemoveExpiredSubscriptions DISABLED_RemoveExpiredSubscriptions +#else +#define MAYBE_RemoveExpiredSubscriptions RemoveExpiredSubscriptions +#endif +TEST_F(PushMessagingServiceTest, MAYBE_RemoveExpiredSubscriptions) { + // (1) Enable push subscriptions with expiration time and + // `pushsubscriptionchange` events + base::test::ScopedFeatureList scoped_feature_list_; + scoped_feature_list_.InitWithFeatures( + /* enabled features */ + {features::kPushSubscriptionWithExpirationTime, + features::kPushSubscriptionChangeEvent}, + /* disabled features */ + {}); + + // (2) Set up push service and test origin + PushMessagingServiceImpl* push_service = profile()->GetPushMessagingService(); + ASSERT_TRUE(push_service); + const GURL origin(kTestOrigin); + + // (3) Subscribe origin to push service and find corresponding + // |app_identifier| + Subscribe(push_service, origin); + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker(profile(), origin, + kTestServiceWorkerId); + ASSERT_FALSE(app_identifier.is_null()); + + // (4) Manually set the time as expired, save the time in preferences + app_identifier.set_expiration_time(base::Time::UnixEpoch()); + app_identifier.PersistToPrefs(profile()); + ASSERT_EQ(1u, PushMessagingAppIdentifier::GetCount(profile())); + + // (3) Remove all expired subscriptions + base::RunLoop run_loop; + push_service->SetRemoveExpiredSubscriptionsCallbackForTesting( + run_loop.QuitClosure()); + push_service->RemoveExpiredSubscriptions(); + run_loop.Run(); + + // (5) We expect the subscription to be deleted + ASSERT_EQ(0u, PushMessagingAppIdentifier::GetCount(profile())); + PushMessagingAppIdentifier deleted_identifier = + PushMessagingAppIdentifier::FindByAppId(profile(), + app_identifier.app_id()); + EXPECT_TRUE(deleted_identifier.is_null()); +} + +TEST_F(PushMessagingServiceTest, TestMultipleIncomingPushMessages) { + base::HistogramTester histograms; + PushMessagingServiceImpl* push_service = profile()->GetPushMessagingService(); + ASSERT_TRUE(push_service); + + // Subscribe |origin| to push service. + const GURL origin(kTestOrigin); + Subscribe(push_service, origin); + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker(profile(), origin, + kTestServiceWorkerId); + ASSERT_FALSE(app_identifier.is_null()); + + // Setup decrypted test message. + gcm::IncomingMessage message; + message.sender_id = kTestSenderId; + message.raw_data = "testdata"; + message.decrypted = true; + + // Setup callbacks for dispatch and handled push events. + auto dispatched_run_loop = std::make_unique(); + auto handled_run_loop = std::make_unique(); + PushMessagingServiceImpl::PushEventCallback handle_push_event; + + push_service->SetMessageDispatchedCallbackForTesting( + base::BindLambdaForTesting( + [&](const std::string& app_id, const GURL& origin, + int64_t service_worker_registration_id, + absl::optional payload, + PushMessagingServiceImpl::PushEventCallback callback) { + handle_push_event = std::move(callback); + dispatched_run_loop->Quit(); + })); + + push_service->SetMessageCallbackForTesting( + base::BindLambdaForTesting([&]() { handled_run_loop->Quit(); })); + + // Simulate two incoming push messages at the same time. + push_service->OnMessage(app_identifier.app_id(), message); + push_service->OnMessage(app_identifier.app_id(), message); + + // First wait until we dispatched the first push message. + dispatched_run_loop->Run(); + dispatched_run_loop = std::make_unique(); + auto handled_first = std::move(handle_push_event); + handle_push_event = PushMessagingServiceImpl::PushEventCallback(); + + histograms.ExpectUniqueTimeSample("PushMessaging.CheckOriginForAbuseTime", + base::Seconds(0), + /*expected_bucket_count=*/1); + histograms.ExpectUniqueTimeSample("PushMessaging.DeliverQueuedMessageTime", + base::Seconds(0), + /*expected_bucket_count=*/1); + + // Run all tasks until idle so we can verify that we don't dispatch the second + // push message until the first one is handled. + base::RunLoop().RunUntilIdle(); + EXPECT_FALSE(handle_push_event); + + // Simulate handling the first push event takes some time. + task_environment().FastForwardBy(kPushEventHandleTime); + + // Now signal that the first push event has been handled and wait until we + // checked for visibility requirements. + std::move(handled_first).Run(blink::mojom::PushEventStatus::SUCCESS); + handled_run_loop->Run(); + handled_run_loop = std::make_unique(); + + histograms.ExpectUniqueTimeSample("PushMessaging.MessageHandledTime", + kPushEventHandleTime, + /*expected_bucket_count=*/1); + + // Simulate handling the second push event takes some time. + task_environment().FastForwardBy(kPushEventHandleTime); + + // Now wait until we dispatched the second push message and handle it too. + dispatched_run_loop->Run(); + std::move(handle_push_event).Run(blink::mojom::PushEventStatus::SUCCESS); + handled_run_loop->Run(); + + // Checking origins for abuse happens immediately on receiving a push message + // one at a time. Both messages do that instantly in this test. + histograms.ExpectTimeBucketCount("PushMessaging.CheckOriginForAbuseTime", + base::Seconds(0), + /*count=*/2); + // Delivering messages should be done in series so the second message should + // have waited for the first one to be handled. + histograms.ExpectTimeBucketCount("PushMessaging.DeliverQueuedMessageTime", + kPushEventHandleTime, + /*count=*/1); + // The total time from receiving until handling of the second message. + histograms.ExpectTimeBucketCount("PushMessaging.MessageHandledTime", + kPushEventHandleTime * 2, + /*count=*/1); +} diff --git a/chromium/chrome/browser/push_messaging/push_messaging_utils.cc b/chromium/chrome/browser/push_messaging/push_messaging_utils.cc new file mode 100644 index 00000000000..b54bf14acf3 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_utils.cc @@ -0,0 +1,44 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/push_messaging/push_messaging_utils.h" +#include "base/base64url.h" +#include "chrome/browser/push_messaging/push_messaging_constants.h" +#include "url/gurl.h" + +namespace push_messaging { + +GURL CreateEndpoint(const std::string& subscription_id) { + const GURL endpoint(kPushMessagingGcmEndpoint + subscription_id); + DCHECK(endpoint.is_valid()); + return endpoint; +} + +blink::mojom::PushSubscriptionOptionsPtr MakeOptions( + const std::string& sender_id) { + return blink::mojom::PushSubscriptionOptions::New( + /*user_visible_only=*/true, + std::vector(sender_id.begin(), sender_id.end())); +} + +bool IsVapidKey(const std::string& application_server_key) { + // VAPID keys are NIST P-256 public keys in uncompressed format (64 bytes), + // verified through its length and the 0x04 prefix. + return application_server_key.size() == 65 && + application_server_key[0] == 0x04; +} + +std::string NormalizeSenderInfo(const std::string& application_server_key) { + if (!IsVapidKey(application_server_key)) + return application_server_key; + + std::string encoded_application_server_key; + base::Base64UrlEncode(application_server_key, + base::Base64UrlEncodePolicy::OMIT_PADDING, + &encoded_application_server_key); + + return encoded_application_server_key; +} + +} // namespace push_messaging diff --git a/chromium/chrome/browser/push_messaging/push_messaging_utils.h b/chromium/chrome/browser/push_messaging/push_messaging_utils.h new file mode 100644 index 00000000000..805deeab47e --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_utils.h @@ -0,0 +1,34 @@ +// Copyright 2020 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 CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_UTILS_H_ +#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_UTILS_H_ + +#include +#include "third_party/blink/public/mojom/push_messaging/push_messaging.mojom.h" + +class GURL; + +namespace push_messaging { + +// Returns the URL used to send push messages to the subscription identified +// by |subscription_id|. +GURL CreateEndpoint(const std::string& subscription_id); + +// Checks size and prefix to determine whether it is a VAPID key +bool IsVapidKey(const std::string& application_server_key); + +// Normalizes the |sender_info|. In most cases the |sender_info| will be +// passed through to the GCM Driver as-is, but NIST P-256 application server +// keys have to be encoded using the URL-safe variant of the base64 encoding. +std::string NormalizeSenderInfo(const std::string& sender_info); + +// Currently |user_visible_only| is always true, once silent pushes are +// enabled, get this information from SW database. +blink::mojom::PushSubscriptionOptionsPtr MakeOptions( + const std::string& sender_id); + +} // namespace push_messaging + +#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_UTILS_H_ diff --git a/chromium/chrome/browser/signin/DEPS b/chromium/chrome/browser/signin/DEPS new file mode 100644 index 00000000000..e2cc84fd222 --- /dev/null +++ b/chromium/chrome/browser/signin/DEPS @@ -0,0 +1,3 @@ +include_rules = [ + "+ash/components/account_manager", +] diff --git a/chromium/chrome/browser/signin/DIR_METADATA b/chromium/chrome/browser/signin/DIR_METADATA new file mode 100644 index 00000000000..95ad5b66d16 --- /dev/null +++ b/chromium/chrome/browser/signin/DIR_METADATA @@ -0,0 +1 @@ +mixins: "//components/signin/COMMON_METADATA" diff --git a/chromium/chrome/browser/signin/OWNERS b/chromium/chrome/browser/signin/OWNERS new file mode 100644 index 00000000000..017d5a003db --- /dev/null +++ b/chromium/chrome/browser/signin/OWNERS @@ -0,0 +1,5 @@ +file://components/signin/OWNERS + +per-file chrome_proximity_auth_*=xiyuan@chromium.org +per-file easy_unlock_*=xiyuan@chromium.org +per-file signin_profile_attributes_updater*=msalama@chromium.org diff --git a/chromium/chrome/browser/signin/about_signin_internals_factory.cc b/chromium/chrome/browser/signin/about_signin_internals_factory.cc new file mode 100644 index 00000000000..7e62dc93ebf --- /dev/null +++ b/chromium/chrome/browser/signin/about_signin_internals_factory.cc @@ -0,0 +1,58 @@ +// 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 "chrome/browser/signin/about_signin_internals_factory.h" + +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/signin/account_consistency_mode_manager_factory.h" +#include "chrome/browser/signin/account_reconcilor_factory.h" +#include "chrome/browser/signin/chrome_signin_client_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/signin_error_controller_factory.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/signin/core/browser/about_signin_internals.h" + +AboutSigninInternalsFactory::AboutSigninInternalsFactory() + : BrowserContextKeyedServiceFactory( + "AboutSigninInternals", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(ChromeSigninClientFactory::GetInstance()); + DependsOn(SigninErrorControllerFactory::GetInstance()); + DependsOn(AccountReconcilorFactory::GetInstance()); + DependsOn(IdentityManagerFactory::GetInstance()); + DependsOn(AccountConsistencyModeManagerFactory::GetInstance()); +} + +AboutSigninInternalsFactory::~AboutSigninInternalsFactory() {} + +// static +AboutSigninInternals* AboutSigninInternalsFactory::GetForProfile( + Profile* profile) { + return static_cast( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +// static +AboutSigninInternalsFactory* AboutSigninInternalsFactory::GetInstance() { + return base::Singleton::get(); +} + +void AboutSigninInternalsFactory::RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* user_prefs) { + AboutSigninInternals::RegisterPrefs(user_prefs); +} + +KeyedService* AboutSigninInternalsFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + AboutSigninInternals* service = new AboutSigninInternals( + IdentityManagerFactory::GetForProfile(profile), + SigninErrorControllerFactory::GetForProfile(profile), + AccountConsistencyModeManager::GetMethodForProfile(profile), + ChromeSigninClientFactory::GetForProfile(profile), + AccountReconcilorFactory::GetForProfile(profile)); + return service; +} diff --git a/chromium/chrome/browser/signin/about_signin_internals_factory.h b/chromium/chrome/browser/signin/about_signin_internals_factory.h new file mode 100644 index 00000000000..c1b7cb5a71d --- /dev/null +++ b/chromium/chrome/browser/signin/about_signin_internals_factory.h @@ -0,0 +1,40 @@ +// 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 CHROME_BROWSER_SIGNIN_ABOUT_SIGNIN_INTERNALS_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_ABOUT_SIGNIN_INTERNALS_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +class AboutSigninInternals; +class Profile; + +// Singleton that owns all AboutSigninInternals and associates them with +// Profiles. +class AboutSigninInternalsFactory : public BrowserContextKeyedServiceFactory { + public: + // Returns the instance of AboutSigninInternals associated with this profile, + // creating one if none exists. + static AboutSigninInternals* GetForProfile(Profile* profile); + + // Returns an instance of the AboutSigninInternalsFactory singleton. + static AboutSigninInternalsFactory* GetInstance(); + + // Implementation of BrowserContextKeyedServiceFactory. + void RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) override; + + private: + friend struct base::DefaultSingletonTraits; + + AboutSigninInternalsFactory(); + ~AboutSigninInternalsFactory() override; + + // BrowserContextKeyedServiceFactory + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* context) const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_ABOUT_SIGNIN_INTERNALS_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/account_consistency_mode_manager.cc b/chromium/chrome/browser/signin/account_consistency_mode_manager.cc new file mode 100644 index 00000000000..6c1f0f04720 --- /dev/null +++ b/chromium/chrome/browser/signin/account_consistency_mode_manager.cc @@ -0,0 +1,208 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/account_consistency_mode_manager.h" + +#include + +#include "base/command_line.h" +#include "base/logging.h" +#include "base/metrics/field_trial_params.h" +#include "base/metrics/histogram_macros.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profiles_state.h" +#include "chrome/browser/signin/account_consistency_mode_manager_factory.h" +#include "chrome/browser/signin/chrome_signin_client_factory.h" +#include "chrome/common/pref_names.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/prefs/pref_service.h" +#include "components/signin/public/base/signin_buildflags.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "google_apis/google_api_keys.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "chrome/browser/ash/account_manager/account_manager_util.h" +#endif + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) && BUILDFLAG(ENABLE_MIRROR) +#error "Dice and Mirror cannot be both enabled." +#endif + +#if !BUILDFLAG(ENABLE_DICE_SUPPORT) && !BUILDFLAG(ENABLE_MIRROR) +#error "Either Dice or Mirror should be enabled." +#endif + +using signin::AccountConsistencyMethod; + +namespace { + +// By default, DICE is not enabled in builds lacking an API key. May be set to +// true for tests. +bool g_ignore_missing_oauth_client_for_testing = false; + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +const char kAllowBrowserSigninArgument[] = "allow-browser-signin"; + +bool IsBrowserSigninAllowedByCommandLine() { + base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); + if (command_line->HasSwitch(kAllowBrowserSigninArgument)) { + std::string allowBrowserSignin = + command_line->GetSwitchValueASCII(kAllowBrowserSigninArgument); + return base::ToLowerASCII(allowBrowserSignin) == "true"; + } + // If the commandline flag is not provided, the default is true. + return true; +} + +// Returns true if Desktop Identity Consistency can be enabled for this build +// (i.e. if OAuth client ID and client secret are configured). +bool CanEnableDiceForBuild() { + if (g_ignore_missing_oauth_client_for_testing || + google_apis::HasOAuthClientConfigured()) { + return true; + } + + // Only log this once. + static bool logged_warning = []() { + LOG(WARNING) << "Desktop Identity Consistency cannot be enabled as no " + "OAuth client ID and client secret have been configured."; + return true; + }(); + ALLOW_UNUSED_LOCAL(logged_warning); + + return false; +} +#endif + +} // namespace + +// static +AccountConsistencyModeManager* AccountConsistencyModeManager::GetForProfile( + Profile* profile) { + return AccountConsistencyModeManagerFactory::GetForProfile(profile); +} + +AccountConsistencyModeManager::AccountConsistencyModeManager(Profile* profile) + : profile_(profile), + account_consistency_(signin::AccountConsistencyMethod::kDisabled), + account_consistency_initialized_(false) { + DCHECK(profile_); + DCHECK(ShouldBuildServiceForProfile(profile)); + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + PrefService* prefs = profile->GetPrefs(); + // Propagate settings changes from the previous launch to the signin-allowed + // pref. + bool signin_allowed = IsDiceSignInAllowed() && + prefs->GetBoolean(prefs::kSigninAllowedOnNextStartup); + prefs->SetBoolean(prefs::kSigninAllowed, signin_allowed); + + UMA_HISTOGRAM_BOOLEAN("Signin.SigninAllowed", signin_allowed); +#endif + + account_consistency_ = ComputeAccountConsistencyMethod(profile_); + DCHECK_EQ(account_consistency_, ComputeAccountConsistencyMethod(profile_)); + account_consistency_initialized_ = true; +} + +AccountConsistencyModeManager::~AccountConsistencyModeManager() {} + +// static +void AccountConsistencyModeManager::RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) { + registry->RegisterBooleanPref(prefs::kSigninAllowedOnNextStartup, true); +} + +// static +AccountConsistencyMethod AccountConsistencyModeManager::GetMethodForProfile( + Profile* profile) { + if (!ShouldBuildServiceForProfile(profile)) + return AccountConsistencyMethod::kDisabled; + + return AccountConsistencyModeManager::GetForProfile(profile) + ->GetAccountConsistencyMethod(); +} + +// static +bool AccountConsistencyModeManager::IsDiceEnabledForProfile(Profile* profile) { + return GetMethodForProfile(profile) == AccountConsistencyMethod::kDice; +} + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +// static +bool AccountConsistencyModeManager::IsDiceSignInAllowed() { + return CanEnableDiceForBuild() && IsBrowserSigninAllowedByCommandLine(); +} +#endif // BUILDFLAG(ENABLE_DICE_SUPPORT) + +// static +bool AccountConsistencyModeManager::IsMirrorEnabledForProfile( + Profile* profile) { + return GetMethodForProfile(profile) == AccountConsistencyMethod::kMirror; +} + +// static +void AccountConsistencyModeManager::SetIgnoreMissingOAuthClientForTesting() { + g_ignore_missing_oauth_client_for_testing = true; +} + +// static +bool AccountConsistencyModeManager::ShouldBuildServiceForProfile( + Profile* profile) { + return profile->IsRegularProfile(); +} + +AccountConsistencyMethod +AccountConsistencyModeManager::GetAccountConsistencyMethod() { +#if BUILDFLAG(IS_CHROMEOS_ASH) + // TODO(https://crbug.com/860671): ChromeOS should use the cached value. + // Changing the value dynamically is not supported. + return ComputeAccountConsistencyMethod(profile_); +#else + // The account consistency method should not change during the lifetime of a + // profile. We always return the cached value, but still check that it did not + // change, in order to detect inconsisent states. See https://crbug.com/860471 + CHECK(account_consistency_initialized_); + CHECK_EQ(ComputeAccountConsistencyMethod(profile_), account_consistency_); + return account_consistency_; +#endif +} + +// static +signin::AccountConsistencyMethod +AccountConsistencyModeManager::ComputeAccountConsistencyMethod( + Profile* profile) { + DCHECK(ShouldBuildServiceForProfile(profile)); + +#if BUILDFLAG(IS_CHROMEOS_ASH) + if (!ash::IsAccountManagerAvailable(profile)) + return AccountConsistencyMethod::kDisabled; +#endif + +#if BUILDFLAG(IS_CHROMEOS_LACROS) + // Account consistency is unavailable on Managed Guest Sessions and Public + // Sessions. + if (profiles::IsPublicSession()) + return AccountConsistencyMethod::kDisabled; +#endif + +#if BUILDFLAG(ENABLE_MIRROR) + return AccountConsistencyMethod::kMirror; +#endif + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + if (!profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed)) { + VLOG(1) << "Desktop Identity Consistency disabled as sign-in to Chrome" + "is not allowed"; + return AccountConsistencyMethod::kDisabled; + } + + return AccountConsistencyMethod::kDice; +#endif + + NOTREACHED(); + return AccountConsistencyMethod::kDisabled; +} diff --git a/chromium/chrome/browser/signin/account_consistency_mode_manager.h b/chromium/chrome/browser/signin/account_consistency_mode_manager.h new file mode 100644 index 00000000000..8ca66aae9b9 --- /dev/null +++ b/chromium/chrome/browser/signin/account_consistency_mode_manager.h @@ -0,0 +1,97 @@ +// Copyright 2017 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 CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_H_ +#define CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_H_ + +#include "base/feature_list.h" +#include "base/gtest_prod_util.h" +#include "base/memory/raw_ptr.h" +#include "build/buildflag.h" +#include "components/keyed_service/core/keyed_service.h" +#include "components/prefs/pref_member.h" +#include "components/signin/public/base/account_consistency_method.h" +#include "components/signin/public/base/signin_buildflags.h" + +namespace user_prefs { +class PrefRegistrySyncable; +} + +class Profile; + +// Manages the account consistency mode for each profile. +class AccountConsistencyModeManager : public KeyedService { + public: + // Returns the AccountConsistencyModeManager associated with this profile. + // May return nullptr if there is none (e.g. in incognito). + static AccountConsistencyModeManager* GetForProfile(Profile* profile); + + explicit AccountConsistencyModeManager(Profile* profile); + + AccountConsistencyModeManager(const AccountConsistencyModeManager&) = delete; + AccountConsistencyModeManager& operator=( + const AccountConsistencyModeManager&) = delete; + + ~AccountConsistencyModeManager() override; + + static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry); + + // Helper method, shorthand for calling GetAccountConsistencyMethod(). + // TODO(crbug.com/1232361): Migrate usages to + // `IdentityManager::GetAccountConsistency`. + static signin::AccountConsistencyMethod GetMethodForProfile(Profile* profile); + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + // This is a pre-requisite of IsDiceEnabledForProfile(), independent of + // particular profile type or profile prefs. + static bool IsDiceSignInAllowed(); +#endif + + // If true, then account management is done through Gaia webpages. + // Can only be used on the UI thread. + // Returns false if |profile| is in Guest or Incognito mode. + // A given |profile| will have only one of Mirror or Dice consistency + // behaviour enabled. + static bool IsDiceEnabledForProfile(Profile* profile); + + // Returns |true| if Mirror account consistency is enabled for |profile|. + // Can only be used on the UI thread. + // A given |profile| will have only one of Mirror or Dice consistency + // behaviour enabled. + static bool IsMirrorEnabledForProfile(Profile* profile); + + // By default, Desktop Identity Consistency (aka Dice) is not enabled in + // builds lacking an API key. For testing, set to have Dice enabled in tests. + static void SetIgnoreMissingOAuthClientForTesting(); + + // Returns true is the AccountConsistencyModeManager should be instantiated + // for the profile. Guest, incognito and system sessions do not instantiate + // the service. + static bool ShouldBuildServiceForProfile(Profile* profile); + + private: + FRIEND_TEST_ALL_PREFIXES(AccountConsistencyModeManagerTest, + MigrateAtCreation); + FRIEND_TEST_ALL_PREFIXES(AccountConsistencyModeManagerTest, + SigninAllowedChangesDiceState); + FRIEND_TEST_ALL_PREFIXES(AccountConsistencyModeManagerTest, + AllowBrowserSigninSwitch); + FRIEND_TEST_ALL_PREFIXES(AccountConsistencyModeManagerTest, + DiceEnabledForNewProfiles); + + // Returns the account consistency method for the current profile. + signin::AccountConsistencyMethod GetAccountConsistencyMethod(); + + // Computes the account consistency method for the current profile. This is + // only called from the constructor, the account consistency method cannot + // change during the lifetime of a profile. + static signin::AccountConsistencyMethod ComputeAccountConsistencyMethod( + Profile* profile); + + raw_ptr profile_; + signin::AccountConsistencyMethod account_consistency_; + bool account_consistency_initialized_; +}; + +#endif // CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_H_ diff --git a/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.cc b/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.cc new file mode 100644 index 00000000000..10178f34732 --- /dev/null +++ b/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.cc @@ -0,0 +1,53 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/account_consistency_mode_manager_factory.h" + +#include "base/check.h" +#include "build/build_config.h" +#include "chrome/browser/profiles/profile.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +// static +AccountConsistencyModeManagerFactory* +AccountConsistencyModeManagerFactory::GetInstance() { + return base::Singleton::get(); +} + +// static +AccountConsistencyModeManager* +AccountConsistencyModeManagerFactory::GetForProfile(Profile* profile) { + DCHECK(profile); + return static_cast( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +AccountConsistencyModeManagerFactory::AccountConsistencyModeManagerFactory() + : BrowserContextKeyedServiceFactory( + "AccountConsistencyModeManager", + BrowserContextDependencyManager::GetInstance()) {} + +AccountConsistencyModeManagerFactory::~AccountConsistencyModeManagerFactory() = + default; + +KeyedService* AccountConsistencyModeManagerFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + DCHECK(!context->IsOffTheRecord()); + Profile* profile = Profile::FromBrowserContext(context); + + return AccountConsistencyModeManager::ShouldBuildServiceForProfile(profile) + ? new AccountConsistencyModeManager(profile) + : nullptr; +} + +void AccountConsistencyModeManagerFactory::RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) { + AccountConsistencyModeManager::RegisterProfilePrefs(registry); +} + +bool AccountConsistencyModeManagerFactory::ServiceIsCreatedWithBrowserContext() + const { + return true; +} diff --git a/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.h b/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.h new file mode 100644 index 00000000000..7990d84bad5 --- /dev/null +++ b/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.h @@ -0,0 +1,35 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +class AccountConsistencyModeManagerFactory + : public BrowserContextKeyedServiceFactory { + public: + // Returns an instance of the factory singleton. + static AccountConsistencyModeManagerFactory* GetInstance(); + + static AccountConsistencyModeManager* GetForProfile(Profile* profile); + + private: + friend struct base::DefaultSingletonTraits< + AccountConsistencyModeManagerFactory>; + + AccountConsistencyModeManagerFactory(); + ~AccountConsistencyModeManagerFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* context) const override; + void RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) override; + bool ServiceIsCreatedWithBrowserContext() const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/account_consistency_mode_manager_unittest.cc b/chromium/chrome/browser/signin/account_consistency_mode_manager_unittest.cc new file mode 100644 index 00000000000..d0a4ee97b83 --- /dev/null +++ b/chromium/chrome/browser/signin/account_consistency_mode_manager_unittest.cc @@ -0,0 +1,259 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/account_consistency_mode_manager.h" + +#include +#include + +#include "base/command_line.h" +#include "base/test/scoped_command_line.h" +#include "build/build_config.h" +#include "build/buildflag.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/prefs/browser_prefs.h" +#include "chrome/common/pref_names.h" +#include "chrome/test/base/testing_profile.h" +#include "components/prefs/pref_notifier_impl.h" +#include "components/prefs/testing_pref_store.h" +#include "components/signin/public/base/account_consistency_method.h" +#include "components/signin/public/base/signin_buildflags.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/sync_preferences/testing_pref_service_syncable.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +std::unique_ptr BuildTestingProfile(bool is_new_profile) { + TestingProfile::Builder profile_builder; + profile_builder.SetIsNewProfile(is_new_profile); + std::unique_ptr profile = profile_builder.Build(); + EXPECT_EQ(is_new_profile, profile->IsNewProfile()); + return profile; +} + +} // namespace + +// Check the default account consistency method. +TEST(AccountConsistencyModeManagerTest, DefaultValue) { + content::BrowserTaskEnvironment task_environment; + std::unique_ptr profile = + BuildTestingProfile(/*is_new_profile=*/false); + + signin::AccountConsistencyMethod method = +#if BUILDFLAG(ENABLE_MIRROR) + signin::AccountConsistencyMethod::kMirror; +#elif BUILDFLAG(ENABLE_DICE_SUPPORT) + signin::AccountConsistencyMethod::kDice; +#else +#error Either Dice or Mirror should be enabled +#endif + + EXPECT_EQ(method, + AccountConsistencyModeManager::GetMethodForProfile(profile.get())); + EXPECT_EQ( + method == signin::AccountConsistencyMethod::kMirror, + AccountConsistencyModeManager::IsMirrorEnabledForProfile(profile.get())); + EXPECT_EQ( + method == signin::AccountConsistencyMethod::kDice, + AccountConsistencyModeManager::IsDiceEnabledForProfile(profile.get())); +} + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +// Checks that changing the signin-allowed pref changes the Dice state on next +// startup. +TEST(AccountConsistencyModeManagerTest, SigninAllowedChangesDiceState) { + content::BrowserTaskEnvironment task_environment; + std::unique_ptr profile = + BuildTestingProfile(/*is_new_profile=*/false); + + { + // First startup. + AccountConsistencyModeManager manager(profile.get()); + EXPECT_TRUE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed)); + EXPECT_TRUE( + profile->GetPrefs()->GetBoolean(prefs::kSigninAllowedOnNextStartup)); + EXPECT_EQ(signin::AccountConsistencyMethod::kDice, + manager.GetAccountConsistencyMethod()); + + // User changes their settings. + profile->GetPrefs()->SetBoolean(prefs::kSigninAllowedOnNextStartup, false); + // Dice should remain in the same state until restart. + EXPECT_EQ(signin::AccountConsistencyMethod::kDice, + manager.GetAccountConsistencyMethod()); + } + + { + // Second startup. + AccountConsistencyModeManager manager(profile.get()); + // The signin-allowed pref should be disabled. + EXPECT_FALSE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed)); + EXPECT_FALSE( + profile->GetPrefs()->GetBoolean(prefs::kSigninAllowedOnNextStartup)); + // Dice should be disabled. + EXPECT_EQ(signin::AccountConsistencyMethod::kDisabled, + manager.GetAccountConsistencyMethod()); + } +} + +TEST(AccountConsistencyModeManagerTest, AllowBrowserSigninSwitch) { + content::BrowserTaskEnvironment task_environment; + std::unique_ptr profile = + BuildTestingProfile(/*is_new_profile=*/false); + { + base::test::ScopedCommandLine scoped_command_line; + scoped_command_line.GetProcessCommandLine()->AppendSwitchASCII( + "allow-browser-signin", "false"); + AccountConsistencyModeManager manager(profile.get()); + EXPECT_FALSE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed)); + // Dice should be disabled. + EXPECT_EQ(signin::AccountConsistencyMethod::kDisabled, + manager.GetAccountConsistencyMethod()); + } + + { + base::test::ScopedCommandLine scoped_command_line; + scoped_command_line.GetProcessCommandLine()->AppendSwitchASCII( + "allow-browser-signin", "true"); + AccountConsistencyModeManager manager(profile.get()); + EXPECT_TRUE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed)); + // Dice should be enabled. + EXPECT_EQ(signin::AccountConsistencyMethod::kDice, + manager.GetAccountConsistencyMethod()); + } + + { + AccountConsistencyModeManager manager(profile.get()); + EXPECT_TRUE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed)); + EXPECT_TRUE( + profile->GetPrefs()->GetBoolean(prefs::kSigninAllowedOnNextStartup)); + // Dice should be enabled. + EXPECT_EQ(signin::AccountConsistencyMethod::kDice, + manager.GetAccountConsistencyMethod()); + } +} + +// Checks that Dice is enabled for new profiles. +TEST(AccountConsistencyModeManagerTest, DiceEnabledForNewProfiles) { + content::BrowserTaskEnvironment task_environment; + std::unique_ptr profile = + BuildTestingProfile(/*is_new_profile=*/false); + AccountConsistencyModeManager manager(profile.get()); + EXPECT_EQ(signin::AccountConsistencyMethod::kDice, + manager.GetAccountConsistencyMethod()); +} + +TEST(AccountConsistencyModeManagerTest, DiceOnlyForRegularProfile) { + content::BrowserTaskEnvironment task_environment; + + { + // Regular profile. + TestingProfile profile; + EXPECT_TRUE( + AccountConsistencyModeManager::IsDiceEnabledForProfile(&profile)); + EXPECT_EQ(signin::AccountConsistencyMethod::kDice, + AccountConsistencyModeManager::GetMethodForProfile(&profile)); + EXPECT_TRUE( + AccountConsistencyModeManager::ShouldBuildServiceForProfile(&profile)); + + // Incognito profile. + Profile* incognito_profile = + profile.GetPrimaryOTRProfile(/*create_if_needed=*/true); + EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile( + incognito_profile)); + EXPECT_FALSE( + AccountConsistencyModeManager::GetForProfile(incognito_profile)); + EXPECT_EQ( + signin::AccountConsistencyMethod::kDisabled, + AccountConsistencyModeManager::GetMethodForProfile(incognito_profile)); + EXPECT_FALSE(AccountConsistencyModeManager::ShouldBuildServiceForProfile( + incognito_profile)); + + // Non-primary off-the-record profile. + Profile* otr_profile = profile.GetOffTheRecordProfile( + Profile::OTRProfileID::CreateUniqueForTesting(), + /*create_if_needed=*/true); + EXPECT_FALSE( + AccountConsistencyModeManager::IsDiceEnabledForProfile(otr_profile)); + EXPECT_FALSE(AccountConsistencyModeManager::GetForProfile(otr_profile)); + EXPECT_EQ(signin::AccountConsistencyMethod::kDisabled, + AccountConsistencyModeManager::GetMethodForProfile(otr_profile)); + EXPECT_FALSE(AccountConsistencyModeManager::ShouldBuildServiceForProfile( + otr_profile)); + } + + // Guest profile. + { + TestingProfile::Builder profile_builder; + profile_builder.SetGuestSession(); + std::unique_ptr profile = profile_builder.Build(); + ASSERT_TRUE(profile->IsGuestSession()); + EXPECT_FALSE( + AccountConsistencyModeManager::IsDiceEnabledForProfile(profile.get())); + EXPECT_EQ( + signin::AccountConsistencyMethod::kDisabled, + AccountConsistencyModeManager::GetMethodForProfile(profile.get())); + EXPECT_FALSE(AccountConsistencyModeManager::ShouldBuildServiceForProfile( + profile.get())); + } +} +#endif // BUILDFLAG(ENABLE_DICE_SUPPORT) + +#if BUILDFLAG(ENABLE_MIRROR) +// Mirror is enabled by default on Chrome OS, unless specified otherwise. +TEST(AccountConsistencyModeManagerTest, MirrorEnabledByDefault) { + // Creation of this object sets the current thread's id as UI thread. + content::BrowserTaskEnvironment task_environment; + + TestingProfile profile; + EXPECT_TRUE( + AccountConsistencyModeManager::IsMirrorEnabledForProfile(&profile)); + EXPECT_FALSE( + AccountConsistencyModeManager::IsDiceEnabledForProfile(&profile)); + EXPECT_EQ(signin::AccountConsistencyMethod::kMirror, + AccountConsistencyModeManager::GetMethodForProfile(&profile)); +} + +TEST(AccountConsistencyModeManagerTest, MirrorDisabledForGuestSession) { + // Creation of this object sets the current thread's id as UI thread. + content::BrowserTaskEnvironment task_environment; + + TestingProfile profile; + profile.SetGuestSession(true); + EXPECT_FALSE( + AccountConsistencyModeManager::IsMirrorEnabledForProfile(&profile)); + EXPECT_FALSE( + AccountConsistencyModeManager::IsDiceEnabledForProfile(&profile)); + EXPECT_EQ(signin::AccountConsistencyMethod::kDisabled, + AccountConsistencyModeManager::GetMethodForProfile(&profile)); +} + +TEST(AccountConsistencyModeManagerTest, MirrorDisabledForOffTheRecordProfile) { + // Creation of this object sets the current thread's id as UI thread. + content::BrowserTaskEnvironment task_environment; + + TestingProfile profile; + Profile* incognito_profile = + profile.GetPrimaryOTRProfile(/*create_if_needed=*/true); + EXPECT_FALSE(AccountConsistencyModeManager::IsMirrorEnabledForProfile( + incognito_profile)); + EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile( + incognito_profile)); + EXPECT_EQ( + signin::AccountConsistencyMethod::kDisabled, + AccountConsistencyModeManager::GetMethodForProfile(incognito_profile)); + + Profile* otr_profile = profile.GetOffTheRecordProfile( + Profile::OTRProfileID::CreateUniqueForTesting(), + /*create_if_needed=*/true); + EXPECT_FALSE( + AccountConsistencyModeManager::IsMirrorEnabledForProfile(otr_profile)); + EXPECT_FALSE( + AccountConsistencyModeManager::IsDiceEnabledForProfile(otr_profile)); + EXPECT_EQ(signin::AccountConsistencyMethod::kDisabled, + AccountConsistencyModeManager::GetMethodForProfile(otr_profile)); +} + +#endif // BUILDFLAG(ENABLE_MIRROR) diff --git a/chromium/chrome/browser/signin/account_id_from_account_info.cc b/chromium/chrome/browser/signin/account_id_from_account_info.cc new file mode 100644 index 00000000000..c801c7ac983 --- /dev/null +++ b/chromium/chrome/browser/signin/account_id_from_account_info.cc @@ -0,0 +1,24 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/account_id_from_account_info.h" +#include "build/chromeos_buildflags.h" +#include "google_apis/gaia/gaia_auth_util.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "components/user_manager/known_user.h" +#endif + +AccountId AccountIdFromAccountInfo(const CoreAccountInfo& account_info) { +#if BUILDFLAG(IS_CHROMEOS_ASH) + return user_manager::known_user::GetAccountId( + account_info.email, account_info.gaia, AccountType::GOOGLE); +#else + if (account_info.email.empty() || account_info.gaia.empty()) + return EmptyAccountId(); + + return AccountId::FromUserEmailGaiaId( + gaia::CanonicalizeEmail(account_info.email), account_info.gaia); +#endif +} diff --git a/chromium/chrome/browser/signin/account_id_from_account_info.h b/chromium/chrome/browser/signin/account_id_from_account_info.h new file mode 100644 index 00000000000..cdd6836878d --- /dev/null +++ b/chromium/chrome/browser/signin/account_id_from_account_info.h @@ -0,0 +1,18 @@ +// Copyright 2019 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 CHROME_BROWSER_SIGNIN_ACCOUNT_ID_FROM_ACCOUNT_INFO_H_ +#define CHROME_BROWSER_SIGNIN_ACCOUNT_ID_FROM_ACCOUNT_INFO_H_ + +#include "components/account_id/account_id.h" +#include "components/signin/public/identity_manager/account_info.h" + +// Returns AccountID populated from |account_info|. +// NOTE: This utility is in //chrome rather than being part of +// //components/signin/public because it is only //chrome that needs to go back +// and forth between AccountId and AccountInfo, and it is outside the scope of +// //components/signin/public to have knowledge about AccountId. +AccountId AccountIdFromAccountInfo(const CoreAccountInfo& account_info); + +#endif // CHROME_BROWSER_SIGNIN_ACCOUNT_ID_FROM_ACCOUNT_INFO_H_ diff --git a/chromium/chrome/browser/signin/account_id_from_account_info_unittest.cc b/chromium/chrome/browser/signin/account_id_from_account_info_unittest.cc new file mode 100644 index 00000000000..356e3385e88 --- /dev/null +++ b/chromium/chrome/browser/signin/account_id_from_account_info_unittest.cc @@ -0,0 +1,20 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/account_id_from_account_info.h" +#include "testing/gtest/include/gtest/gtest.h" + +class AccountIdFromAccountInfoTest : public testing::Test {}; + +// Tests that AccountIdFromAccountInfo() passes along a canonicalized email to +// AccountId. +TEST(AccountIdFromAccountInfoTest, + AccountIdFromAccountInfo_CanonicalizesRawEmail) { + AccountInfo info; + info.email = "test.email@gmail.com"; + info.gaia = "test_id"; + + EXPECT_EQ("testemail@gmail.com", + AccountIdFromAccountInfo(info).GetUserEmail()); +} diff --git a/chromium/chrome/browser/signin/account_investigator_factory.cc b/chromium/chrome/browser/signin/account_investigator_factory.cc new file mode 100644 index 00000000000..731c8b45a8b --- /dev/null +++ b/chromium/chrome/browser/signin/account_investigator_factory.cc @@ -0,0 +1,57 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/account_investigator_factory.h" + +#include "base/memory/singleton.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/prefs/pref_service_factory.h" +#include "components/signin/core/browser/account_investigator.h" +#include "components/signin/public/identity_manager/identity_manager.h" + +// static +AccountInvestigatorFactory* AccountInvestigatorFactory::GetInstance() { + return base::Singleton::get(); +} + +// static +AccountInvestigator* AccountInvestigatorFactory::GetForProfile( + Profile* profile) { + return static_cast( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +AccountInvestigatorFactory::AccountInvestigatorFactory() + : BrowserContextKeyedServiceFactory( + "AccountInvestigator", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(IdentityManagerFactory::GetInstance()); +} + +AccountInvestigatorFactory::~AccountInvestigatorFactory() {} + +KeyedService* AccountInvestigatorFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile(Profile::FromBrowserContext(context)); + AccountInvestigator* investigator = new AccountInvestigator( + profile->GetPrefs(), IdentityManagerFactory::GetForProfile(profile)); + investigator->Initialize(); + return investigator; +} + +void AccountInvestigatorFactory::RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) { + AccountInvestigator::RegisterPrefs(registry); +} + +bool AccountInvestigatorFactory::ServiceIsCreatedWithBrowserContext() const { + return true; +} + +bool AccountInvestigatorFactory::ServiceIsNULLWhileTesting() const { + return true; +} diff --git a/chromium/chrome/browser/signin/account_investigator_factory.h b/chromium/chrome/browser/signin/account_investigator_factory.h new file mode 100644 index 00000000000..6f1d56caec5 --- /dev/null +++ b/chromium/chrome/browser/signin/account_investigator_factory.h @@ -0,0 +1,44 @@ +// Copyright 2016 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 CHROME_BROWSER_SIGNIN_ACCOUNT_INVESTIGATOR_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_ACCOUNT_INVESTIGATOR_FACTORY_H_ + +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +class Profile; +class AccountInvestigator; + +namespace base { +template +struct DefaultSingletonTraits; +} // namespace base + +// Factory for BrowserKeyedService AccountInvestigator. +class AccountInvestigatorFactory : public BrowserContextKeyedServiceFactory { + public: + static AccountInvestigator* GetForProfile(Profile* profile); + + static AccountInvestigatorFactory* GetInstance(); + + AccountInvestigatorFactory(const AccountInvestigatorFactory&) = delete; + AccountInvestigatorFactory& operator=(const AccountInvestigatorFactory&) = + delete; + + private: + friend struct base::DefaultSingletonTraits; + + AccountInvestigatorFactory(); + ~AccountInvestigatorFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* context) const override; + void RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) override; + bool ServiceIsCreatedWithBrowserContext() const override; + bool ServiceIsNULLWhileTesting() const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_ACCOUNT_INVESTIGATOR_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/account_reconcilor_factory.cc b/chromium/chrome/browser/signin/account_reconcilor_factory.cc new file mode 100644 index 00000000000..9b47faae104 --- /dev/null +++ b/chromium/chrome/browser/signin/account_reconcilor_factory.cc @@ -0,0 +1,212 @@ +// 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 "chrome/browser/signin/account_reconcilor_factory.h" + +#include +#include + +#include "base/check.h" +#include "base/feature_list.h" +#include "base/notreached.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/signin/chrome_signin_client_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/signin/core/browser/account_reconcilor.h" +#include "components/signin/core/browser/account_reconcilor_delegate.h" +#include "components/signin/core/browser/mirror_account_reconcilor_delegate.h" +#include "components/signin/public/base/account_consistency_method.h" +#include "components/signin/public/base/signin_buildflags.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "base/metrics/histogram_macros.h" +#include "base/time/time.h" +#include "chrome/browser/ash/account_manager/account_manager_util.h" +#include "chrome/browser/lifetime/application_lifetime.h" +#include "chromeos/tpm/install_attributes.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/prefs/pref_service.h" +#include "components/signin/core/browser/active_directory_account_reconcilor_delegate.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/user_manager/user_manager.h" +#include "google_apis/gaia/google_service_auth_error.h" +#endif + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +#include "components/signin/core/browser/dice_account_reconcilor_delegate.h" +#endif + +namespace { + +#if BUILDFLAG(IS_CHROMEOS_ASH) +class ChromeOSLimitedAccessAccountReconcilorDelegate + : public signin::MirrorAccountReconcilorDelegate { + public: + enum class ReconcilorBehavior { + kChild, + kEnterprise, + }; + + ChromeOSLimitedAccessAccountReconcilorDelegate( + ReconcilorBehavior reconcilor_behavior, + signin::IdentityManager* identity_manager) + : signin::MirrorAccountReconcilorDelegate(identity_manager), + reconcilor_behavior_(reconcilor_behavior) {} + + ChromeOSLimitedAccessAccountReconcilorDelegate( + const ChromeOSLimitedAccessAccountReconcilorDelegate&) = delete; + ChromeOSLimitedAccessAccountReconcilorDelegate& operator=( + const ChromeOSLimitedAccessAccountReconcilorDelegate&) = delete; + + base::TimeDelta GetReconcileTimeout() const override { + switch (reconcilor_behavior_) { + case ReconcilorBehavior::kChild: + return base::Seconds(10); + case ReconcilorBehavior::kEnterprise: + // 60 seconds is enough to cover about 99% of all reconcile cases. + return base::Seconds(60); + default: + NOTREACHED(); + return MirrorAccountReconcilorDelegate::GetReconcileTimeout(); + } + } + + void OnReconcileError(const GoogleServiceAuthError& error) override { + // If |error| is |GoogleServiceAuthError::State::NONE| or a transient error. + if (!error.IsPersistentError()) { + return; + } + + if (!GetIdentityManager()->HasAccountWithRefreshTokenInPersistentErrorState( + GetIdentityManager()->GetPrimaryAccountId( + signin::ConsentLevel::kSignin))) { + return; + } + + // Mark the account to require an online sign in. + const user_manager::User* primary_user = + user_manager::UserManager::Get()->GetPrimaryUser(); + DCHECK(primary_user); + user_manager::UserManager::Get()->SaveForceOnlineSignin( + primary_user->GetAccountId(), true /* force_online_signin */); + + if (reconcilor_behavior_ == ReconcilorBehavior::kChild) { + UMA_HISTOGRAM_BOOLEAN( + "ChildAccountReconcilor.ForcedUserExitOnReconcileError", true); + } + // Force a logout. + chrome::AttemptUserExit(); + } + + private: + const ReconcilorBehavior reconcilor_behavior_; +}; +#endif // BUILDFLAG(IS_CHROMEOS_ASH) + +} // namespace + +AccountReconcilorFactory::AccountReconcilorFactory() + : BrowserContextKeyedServiceFactory( + "AccountReconcilor", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(ChromeSigninClientFactory::GetInstance()); + DependsOn(IdentityManagerFactory::GetInstance()); +} + +AccountReconcilorFactory::~AccountReconcilorFactory() {} + +// static +AccountReconcilor* AccountReconcilorFactory::GetForProfile(Profile* profile) { + return static_cast( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +// static +AccountReconcilorFactory* AccountReconcilorFactory::GetInstance() { + return base::Singleton::get(); +} + +KeyedService* AccountReconcilorFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile); + SigninClient* signin_client = + ChromeSigninClientFactory::GetForProfile(profile); + AccountReconcilor* reconcilor = + new AccountReconcilor(identity_manager, signin_client, + CreateAccountReconcilorDelegate(profile)); + reconcilor->Initialize(true /* start_reconcile_if_tokens_available */); + return reconcilor; +} + +void AccountReconcilorFactory::RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) { +#if BUILDFLAG(IS_CHROMEOS_ASH) + registry->RegisterBooleanPref(prefs::kForceLogoutUnauthenticatedUserEnabled, + false); +#endif +} + +// static +std::unique_ptr +AccountReconcilorFactory::CreateAccountReconcilorDelegate(Profile* profile) { + signin::AccountConsistencyMethod account_consistency = + AccountConsistencyModeManager::GetMethodForProfile(profile); + switch (account_consistency) { + case signin::AccountConsistencyMethod::kMirror: +#if BUILDFLAG(IS_CHROMEOS_ASH) + // Only for child accounts on Chrome OS, use the specialized Mirror + // delegate. + if (profile->IsChild()) { + return std::make_unique( + ChromeOSLimitedAccessAccountReconcilorDelegate::ReconcilorBehavior:: + kChild, + IdentityManagerFactory::GetForProfile(profile)); + } + + // Only for Active Directory accounts on Chrome OS. + // TODO(https://crbug.com/993317): Remove the check for + // |IsAccountManagerAvailable| after fixing https://crbug.com/1008349 and + // https://crbug.com/993317. + if (ash::IsAccountManagerAvailable(profile) && + chromeos::InstallAttributes::Get()->IsActiveDirectoryManaged()) { + return std::make_unique< + signin::ActiveDirectoryAccountReconcilorDelegate>(); + } + + if (profile->GetPrefs()->GetBoolean( + prefs::kForceLogoutUnauthenticatedUserEnabled)) { + return std::make_unique( + ChromeOSLimitedAccessAccountReconcilorDelegate::ReconcilorBehavior:: + kEnterprise, + IdentityManagerFactory::GetForProfile(profile)); + } + + return std::make_unique( + IdentityManagerFactory::GetForProfile(profile)); +#else + return std::make_unique( + IdentityManagerFactory::GetForProfile(profile)); +#endif + + case signin::AccountConsistencyMethod::kDisabled: + return std::make_unique(); + + case signin::AccountConsistencyMethod::kDice: +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + return std::make_unique(); +#else + NOTREACHED(); + return nullptr; +#endif + } + + NOTREACHED(); + return nullptr; +} diff --git a/chromium/chrome/browser/signin/account_reconcilor_factory.h b/chromium/chrome/browser/signin/account_reconcilor_factory.h new file mode 100644 index 00000000000..1358fa13cac --- /dev/null +++ b/chromium/chrome/browser/signin/account_reconcilor_factory.h @@ -0,0 +1,57 @@ +// 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 CHROME_BROWSER_SIGNIN_ACCOUNT_RECONCILOR_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_ACCOUNT_RECONCILOR_FACTORY_H_ + +#include + +#include "base/memory/singleton.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +namespace signin { +class IdentityManager; +} + +namespace signin { +class AccountReconcilorDelegate; +} + +class AccountReconcilor; +class Profile; +class SigninClient; + +// Singleton that owns all AccountReconcilors and associates them with +// Profiles. Listens for the Profile's destruction notification and cleans up. +class AccountReconcilorFactory : public BrowserContextKeyedServiceFactory { + public: + // Returns the instance of AccountReconcilor associated with this profile + // (creating one if none exists). Returns NULL if this profile cannot have an + // AccountReconcilor (for example, if |profile| is incognito). + static AccountReconcilor* GetForProfile(Profile* profile); + + // Returns an instance of the factory singleton. + static AccountReconcilorFactory* GetInstance(); + + // BrowserContextKeyedServiceFactory: + void RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) override; + + private: + friend struct base::DefaultSingletonTraits; + friend class DummyAccountReconcilorWithDelegate; // For testing. + + AccountReconcilorFactory(); + ~AccountReconcilorFactory() override; + + // Creates the AccountReconcilorDelegate. + static std::unique_ptr + CreateAccountReconcilorDelegate(Profile* profile); + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_ACCOUNT_RECONCILOR_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/chrome_device_id_helper.cc b/chromium/chrome/browser/signin/chrome_device_id_helper.cc new file mode 100644 index 00000000000..7b28246d24a --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_device_id_helper.cc @@ -0,0 +1,87 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/chrome_device_id_helper.h" + +#include + +#include "build/chromeos_buildflags.h" +#include "chrome/browser/profiles/profile.h" +#include "components/signin/public/base/device_id_helper.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "base/command_line.h" +#include "base/guid.h" +#include "base/logging.h" +#include "chrome/browser/ash/profiles/profile_helper.h" +#include "components/prefs/pref_service.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/base/signin_switches.h" +#include "components/user_manager/known_user.h" +#include "components/user_manager/user_manager.h" +#endif + +std::string GetSigninScopedDeviceIdForProfile(Profile* profile) { +#if BUILDFLAG(IS_CHROMEOS_ASH) + if (base::CommandLine::ForCurrentProcess()->HasSwitch( + switches::kDisableSigninScopedDeviceId)) { + return std::string(); + } + + // UserManager may not exist in unit_tests. + if (!user_manager::UserManager::IsInitialized()) + return std::string(); + + const user_manager::User* user = + chromeos::ProfileHelper::Get()->GetUserByProfile(profile); + if (!user) + return std::string(); + + const std::string signin_scoped_device_id = + user_manager::known_user::GetDeviceId(user->GetAccountId()); + LOG_IF(ERROR, signin_scoped_device_id.empty()) + << "Device ID is not set for user."; + return signin_scoped_device_id; +#else + return signin::GetSigninScopedDeviceId(profile->GetPrefs()); +#endif +} + +#if BUILDFLAG(IS_CHROMEOS_ASH) + +std::string GenerateSigninScopedDeviceId(bool for_ephemeral) { + constexpr char kEphemeralUserDeviceIDPrefix[] = "t_"; + std::string guid = base::GenerateGUID(); + return for_ephemeral ? kEphemeralUserDeviceIDPrefix + guid : guid; +} + +void MigrateSigninScopedDeviceId(Profile* profile) { + // UserManager may not exist in unit_tests. + if (!user_manager::UserManager::IsInitialized()) + return; + + const user_manager::User* user = + chromeos::ProfileHelper::Get()->GetUserByProfile(profile); + if (!user) + return; + const AccountId account_id = user->GetAccountId(); + if (user_manager::known_user::GetDeviceId(account_id).empty()) { + const std::string legacy_device_id = profile->GetPrefs()->GetString( + prefs::kGoogleServicesSigninScopedDeviceId); + if (!legacy_device_id.empty()) { + // Need to move device ID from the old location to the new one, if it has + // not been done yet. + user_manager::known_user::SetDeviceId(account_id, legacy_device_id); + } else { + user_manager::known_user::SetDeviceId( + account_id, GenerateSigninScopedDeviceId( + user_manager::UserManager::Get() + ->IsUserNonCryptohomeDataEphemeral(account_id))); + } + } + profile->GetPrefs()->SetString(prefs::kGoogleServicesSigninScopedDeviceId, + std::string()); +} + +#endif diff --git a/chromium/chrome/browser/signin/chrome_device_id_helper.h b/chromium/chrome/browser/signin/chrome_device_id_helper.h new file mode 100644 index 00000000000..9e84d416528 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_device_id_helper.h @@ -0,0 +1,36 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_SIGNIN_CHROME_DEVICE_ID_HELPER_H_ +#define CHROME_BROWSER_SIGNIN_CHROME_DEVICE_ID_HELPER_H_ + +#include + +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" + +class Profile; + +// Returns the device ID that is scoped to single signin. +// All refresh tokens for |profile| are annotated with this device ID when they +// are requested. +// On non-ChromeOS platforms, this is equivalent to: +// signin::GetSigninScopedDeviceId(profile->GetPrefs()); +std::string GetSigninScopedDeviceIdForProfile(Profile* profile); + +#if BUILDFLAG(IS_CHROMEOS_ASH) + +// Helper method. The device ID should generally be obtained through +// GetSigninScopedDeviceIdForProfile(). +// If |for_ephemeral| is true, special kind of device ID for ephemeral users is +// generated. +std::string GenerateSigninScopedDeviceId(bool for_ephemeral); + +// Moves any existing device ID out of the pref service into the UserManager, +// and creates a new ID if it is empty. +void MigrateSigninScopedDeviceId(Profile* profile); + +#endif + +#endif // CHROME_BROWSER_SIGNIN_CHROME_DEVICE_ID_HELPER_H_ diff --git a/chromium/chrome/browser/signin/chrome_device_id_helper_unittest.cc b/chromium/chrome/browser/signin/chrome_device_id_helper_unittest.cc new file mode 100644 index 00000000000..3e2f2e9517b --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_device_id_helper_unittest.cc @@ -0,0 +1,40 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/chrome_device_id_helper.h" + +#include + +#include "base/strings/string_util.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +const char kEpehemeralPrefix[] = "t_"; + +TEST(DeviceIdHelper, NotEphemeral) { + std::string device_id = + GenerateSigninScopedDeviceId(false /* for_ephemeral */); + // Not empty. + EXPECT_FALSE(device_id.empty()); + // No ephemeral prefix. + EXPECT_FALSE(base::StartsWith(device_id, kEpehemeralPrefix, + base::CompareCase::SENSITIVE)); + // ID is unique. + EXPECT_NE(device_id, GenerateSigninScopedDeviceId(false /* for_ephemeral */)); +} + +TEST(DeviceIdHelper, Ephemeral) { + std::string device_id = + GenerateSigninScopedDeviceId(true /* for_ephemeral */); + // Ephemeral prefix. + EXPECT_TRUE(base::StartsWith(device_id, kEpehemeralPrefix, + base::CompareCase::SENSITIVE)); + // Not empty. + EXPECT_NE(device_id, kEpehemeralPrefix); + // ID is unique. + EXPECT_NE(device_id, GenerateSigninScopedDeviceId(true /* for_ephemeral */)); +} +#endif diff --git a/chromium/chrome/browser/signin/chrome_signin_client.cc b/chromium/chrome/browser/signin/chrome_signin_client.cc new file mode 100644 index 00000000000..3a5ea106adb --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_client.cc @@ -0,0 +1,369 @@ +// 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 "chrome/browser/signin/chrome_signin_client.h" + +#include + +#include +#include +#include + +#include "base/bind.h" +#include "base/strings/utf_string_conversions.h" +#include "build/build_config.h" +#include "build/buildflag.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/content_settings/cookie_settings_factory.h" +#include "chrome/browser/content_settings/host_content_settings_map_factory.h" +#include "chrome/browser/enterprise/util/managed_browser_utils.h" +#include "chrome/browser/profiles/profile_attributes_entry.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/profiles/profile_metrics.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/signin/chrome_device_id_helper.h" +#include "chrome/browser/signin/force_signin_verifier.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/signin_features.h" +#include "chrome/browser/signin/signin_util.h" +#include "chrome/common/buildflags.h" +#include "chrome/common/channel_info.h" +#include "chrome/common/pref_names.h" +#include "components/content_settings/core/browser/cookie_settings.h" +#include "components/metrics/metrics_service.h" +#include "components/policy/core/browser/browser_policy_connector.h" +#include "components/prefs/pref_service.h" +#include "components/signin/core/browser/cookie_settings_util.h" +#include "components/signin/public/base/signin_buildflags.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/access_token_info.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/scope_set.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/network_service_instance.h" +#include "content/public/browser/storage_partition.h" +#include "google_apis/gaia/gaia_constants.h" +#include "google_apis/gaia/gaia_urls.h" +#include "url/gurl.h" + +#if BUILDFLAG(ENABLE_SUPERVISED_USERS) +#include "chrome/browser/supervised_user/supervised_user_constants.h" +#endif + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "chrome/browser/ash/net/delay_network_call.h" +#include "chromeos/network/network_handler.h" +#endif + +#if BUILDFLAG(IS_CHROMEOS_LACROS) +#include "chrome/browser/lacros/account_manager/account_manager_util.h" +#include "chromeos/crosapi/mojom/account_manager.mojom.h" +#include "chromeos/lacros/lacros_service.h" +#include "components/account_manager_core/account.h" +#include "components/account_manager_core/account_manager_util.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#endif + +#if !defined(OS_ANDROID) +#include "chrome/browser/profiles/profile_window.h" +#endif + +#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH) +#include "chrome/browser/ui/browser_list.h" +#include "chrome/browser/ui/profile_picker.h" +#endif + +namespace { + +// List of sources for which sign out is always allowed. +signin_metrics::ProfileSignout kAlwaysAllowedSignoutSources[] = { + // Allowed, because data has not been synced yet. + signin_metrics::ProfileSignout::ABORT_SIGNIN, + // Allowed, because only used on Android and the primary account must be + // cleared when the account is removed from device + signin_metrics::ProfileSignout::ACCOUNT_REMOVED_FROM_DEVICE, + // Allowed to force finish the account id migration. + signin_metrics::ACCOUNT_ID_MIGRATION, + // Allowed, for tests. + signin_metrics::ProfileSignout::FORCE_SIGNOUT_ALWAYS_ALLOWED_FOR_TEST}; + +SigninClient::SignoutDecision IsSignoutAllowed( + Profile* profile, + const signin_metrics::ProfileSignout signout_source) { + if (signin_util::IsUserSignoutAllowedForProfile(profile)) + return SigninClient::SignoutDecision::ALLOW_SIGNOUT; + + auto* identity_manager = + IdentityManagerFactory::GetForProfileIfExists(profile); + if (identity_manager && + !identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) { + return SigninClient::SignoutDecision::ALLOW_SIGNOUT; + } + + for (const auto& always_allowed_source : kAlwaysAllowedSignoutSources) { + if (signout_source == always_allowed_source) + return SigninClient::SignoutDecision::ALLOW_SIGNOUT; + } + + return SigninClient::SignoutDecision::DISALLOW_SIGNOUT; +} + +} // namespace + +ChromeSigninClient::ChromeSigninClient(Profile* profile) : profile_(profile) { +#if !BUILDFLAG(IS_CHROMEOS_ASH) + content::GetNetworkConnectionTracker()->AddNetworkConnectionObserver(this); +#endif +} + +ChromeSigninClient::~ChromeSigninClient() { +#if !BUILDFLAG(IS_CHROMEOS_ASH) + content::GetNetworkConnectionTracker()->RemoveNetworkConnectionObserver(this); +#endif +} + +void ChromeSigninClient::DoFinalInit() { + VerifySyncToken(); +} + +// static +bool ChromeSigninClient::ProfileAllowsSigninCookies(Profile* profile) { + content_settings::CookieSettings* cookie_settings = + CookieSettingsFactory::GetForProfile(profile).get(); + return signin::SettingsAllowSigninCookies(cookie_settings); +} + +PrefService* ChromeSigninClient::GetPrefs() { return profile_->GetPrefs(); } + +scoped_refptr +ChromeSigninClient::GetURLLoaderFactory() { + if (url_loader_factory_for_testing_) + return url_loader_factory_for_testing_; + + return profile_->GetDefaultStoragePartition() + ->GetURLLoaderFactoryForBrowserProcess(); +} + +network::mojom::CookieManager* ChromeSigninClient::GetCookieManager() { + return profile_->GetDefaultStoragePartition() + ->GetCookieManagerForBrowserProcess(); +} + +bool ChromeSigninClient::AreSigninCookiesAllowed() { + return ProfileAllowsSigninCookies(profile_); +} + +bool ChromeSigninClient::AreSigninCookiesDeletedOnExit() { + content_settings::CookieSettings* cookie_settings = + CookieSettingsFactory::GetForProfile(profile_).get(); + return signin::SettingsDeleteSigninCookiesOnExit(cookie_settings); +} + +void ChromeSigninClient::AddContentSettingsObserver( + content_settings::Observer* observer) { + HostContentSettingsMapFactory::GetForProfile(profile_) + ->AddObserver(observer); +} + +void ChromeSigninClient::RemoveContentSettingsObserver( + content_settings::Observer* observer) { + HostContentSettingsMapFactory::GetForProfile(profile_) + ->RemoveObserver(observer); +} + +void ChromeSigninClient::PreSignOut( + base::OnceCallback on_signout_decision_reached, + signin_metrics::ProfileSignout signout_source_metric) { + DCHECK(on_signout_decision_reached); + DCHECK(!on_signout_decision_reached_) << "SignOut already in-progress!"; + on_signout_decision_reached_ = std::move(on_signout_decision_reached); + +#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH) + // `signout_source_metric` is `signin_metrics::ABORT_SIGNIN` if the user + // declines sync in the signin process. In case the user accepts the managed + // account but declines sync, we should keep the window open. + bool user_declines_sync_after_consenting_to_management = + signout_source_metric == signin_metrics::ABORT_SIGNIN && + chrome::enterprise_util::UserAcceptedAccountManagement(profile_); + // These sign out won't remove the policy cache, keep the window opened. + bool keep_window_opened = + signout_source_metric == + signin_metrics::GOOGLE_SERVICE_NAME_PATTERN_CHANGED || + signout_source_metric == signin_metrics::SERVER_FORCED_DISABLE || + signout_source_metric == signin_metrics::SIGNOUT_PREF_CHANGED || + user_declines_sync_after_consenting_to_management; + if (signin_util::IsForceSigninEnabled() && !profile_->IsSystemProfile() && + !profile_->IsGuestSession() && !profile_->IsChild() && + !keep_window_opened) { + if (signout_source_metric == + signin_metrics::SIGNIN_PREF_CHANGED_DURING_SIGNIN) { + // SIGNIN_PREF_CHANGED_DURING_SIGNIN will be triggered when + // IdentityManager is initialized before window opening, there is no need + // to close window. Call OnCloseBrowsersSuccess to continue sign out and + // show UserManager afterwards. + should_display_user_manager_ = false; // Don't show UserManager twice. + OnCloseBrowsersSuccess(signout_source_metric, profile_->GetPath()); + } else { + BrowserList::CloseAllBrowsersWithProfile( + profile_, + base::BindRepeating(&ChromeSigninClient::OnCloseBrowsersSuccess, + base::Unretained(this), signout_source_metric), + base::BindRepeating(&ChromeSigninClient::OnCloseBrowsersAborted, + base::Unretained(this)), + signout_source_metric == signin_metrics::ABORT_SIGNIN || + signout_source_metric == + signin_metrics::AUTHENTICATION_FAILED_WITH_FORCE_SIGNIN || + signout_source_metric == signin_metrics::TRANSFER_CREDENTIALS); + } + } else { +#else + { +#endif + std::move(on_signout_decision_reached_) + .Run(IsSignoutAllowed(profile_, signout_source_metric)); + } +} + +#if !BUILDFLAG(IS_CHROMEOS_ASH) +void ChromeSigninClient::OnConnectionChanged( + network::mojom::ConnectionType type) { + if (type == network::mojom::ConnectionType::CONNECTION_NONE) + return; + + for (base::OnceClosure& callback : delayed_callbacks_) + std::move(callback).Run(); + + delayed_callbacks_.clear(); +} +#endif + +void ChromeSigninClient::DelayNetworkCall(base::OnceClosure callback) { +#if BUILDFLAG(IS_CHROMEOS_ASH) + // Do not make network requests in unit tests. chromeos::NetworkHandler should + // not be used and is not expected to have been initialized in unit tests. + if (url_loader_factory_for_testing_ && + !chromeos::NetworkHandler::IsInitialized()) { + std::move(callback).Run(); + return; + } + chromeos::DelayNetworkCall( + base::Milliseconds(chromeos::kDefaultNetworkRetryDelayMS), + std::move(callback)); + return; +#else + // Don't bother if we don't have any kind of network connection. + network::mojom::ConnectionType type; + bool sync = content::GetNetworkConnectionTracker()->GetConnectionType( + &type, base::BindOnce(&ChromeSigninClient::OnConnectionChanged, + weak_ptr_factory_.GetWeakPtr())); + if (!sync || type == network::mojom::ConnectionType::CONNECTION_NONE) { + // Connection type cannot be retrieved synchronously so delay the callback. + delayed_callbacks_.push_back(std::move(callback)); + } else { + std::move(callback).Run(); + } +#endif +} + +std::unique_ptr ChromeSigninClient::CreateGaiaAuthFetcher( + GaiaAuthConsumer* consumer, + gaia::GaiaSource source) { + return std::make_unique(consumer, source, + GetURLLoaderFactory()); +} + +void ChromeSigninClient::VerifySyncToken() { +#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH) + // We only verifiy the token once when Profile is just created. + if (signin_util::IsForceSigninEnabled() && !force_signin_verifier_) + force_signin_verifier_ = std::make_unique( + profile_, IdentityManagerFactory::GetForProfile(profile_)); +#endif +} + +bool ChromeSigninClient::IsNonEnterpriseUser(const std::string& username) { + return policy::BrowserPolicyConnector::IsNonEnterpriseUser(username); +} + +#if BUILDFLAG(IS_CHROMEOS_LACROS) +// Returns the account that must be auto-signed-in to the Main Profile in +// Lacros. +// This is, when available, the account used to sign into the Chrome OS +// session. This may be a Gaia account or a Microsoft Active Directory +// account. This field will be null for Guest sessions, Managed Guest +// sessions, Demo mode, and Kiosks. Note that this is different from the +// concept of a Primary Account in the browser. A user may not be signed into +// a Lacros browser Profile, or may be signed into a browser Profile with an +// account which is different from the account which they used to sign into +// the device - aka Device Account. +// Also note that this will be null for Secondary / non-Main Profiles in +// Lacros, because they do not start with the Chrome OS Device Account +// signed-in by default. +absl::optional +ChromeSigninClient::GetInitialPrimaryAccount() { + if (!profile_->IsMainProfile()) + return absl::nullopt; + + const crosapi::mojom::AccountPtr& device_account = + chromeos::LacrosService::Get()->init_params()->device_account; + if (!device_account) + return absl::nullopt; + + return account_manager::FromMojoAccount(device_account); +} +#endif + +void ChromeSigninClient::SetURLLoaderFactoryForTest( + scoped_refptr url_loader_factory) { + url_loader_factory_for_testing_ = url_loader_factory; +} + +void ChromeSigninClient::OnCloseBrowsersSuccess( + const signin_metrics::ProfileSignout signout_source_metric, + const base::FilePath& profile_path) { +#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH) + if (signin_util::IsForceSigninEnabled() && force_signin_verifier_.get()) { + force_signin_verifier_->Cancel(); + } +#endif + + std::move(on_signout_decision_reached_) + .Run(IsSignoutAllowed(profile_, signout_source_metric)); + + LockForceSigninProfile(profile_path); + // After sign out, lock the profile and show UserManager if necessary. + if (should_display_user_manager_) { + ShowUserManager(profile_path); + } else { + should_display_user_manager_ = true; + } +} + +void ChromeSigninClient::OnCloseBrowsersAborted( + const base::FilePath& profile_path) { + should_display_user_manager_ = true; + + // Disallow sign-out (aborted). + std::move(on_signout_decision_reached_) + .Run(SignoutDecision::DISALLOW_SIGNOUT); +} + +void ChromeSigninClient::LockForceSigninProfile( + const base::FilePath& profile_path) { + ProfileAttributesEntry* entry = + g_browser_process->profile_manager() + ->GetProfileAttributesStorage() + .GetProfileAttributesWithPath(profile_->GetPath()); + if (!entry) + return; + entry->LockForceSigninProfile(true); +} + +void ChromeSigninClient::ShowUserManager(const base::FilePath& profile_path) { +#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH) + ProfilePicker::Show(ProfilePicker::EntryPoint::kProfileLocked); +#endif +} diff --git a/chromium/chrome/browser/signin/chrome_signin_client.h b/chromium/chrome/browser/signin/chrome_signin_client.h new file mode 100644 index 00000000000..cef8fd3c32e --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_client.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 CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_H_ +#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_H_ + +#include +#include +#include + +#include "base/memory/raw_ptr.h" +#include "base/memory/scoped_refptr.h" +#include "base/memory/weak_ptr.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "components/signin/public/base/signin_client.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#include "services/network/public/mojom/network_change_manager.mojom-forward.h" + +#if !BUILDFLAG(IS_CHROMEOS_ASH) +#include "services/network/public/cpp/network_connection_tracker.h" +#endif + +#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH) +class ForceSigninVerifier; +#endif +class Profile; + +class ChromeSigninClient + : public SigninClient +#if !BUILDFLAG(IS_CHROMEOS_ASH) + , + public network::NetworkConnectionTracker::NetworkConnectionObserver +#endif +{ + public: + explicit ChromeSigninClient(Profile* profile); + + ChromeSigninClient(const ChromeSigninClient&) = delete; + ChromeSigninClient& operator=(const ChromeSigninClient&) = delete; + + ~ChromeSigninClient() override; + + void DoFinalInit() override; + + // Utility method. + static bool ProfileAllowsSigninCookies(Profile* profile); + + // SigninClient implementation. + PrefService* GetPrefs() override; + void PreSignOut( + base::OnceCallback on_signout_decision_reached, + signin_metrics::ProfileSignout signout_source_metric) override; + scoped_refptr GetURLLoaderFactory() override; + network::mojom::CookieManager* GetCookieManager() override; + bool AreSigninCookiesAllowed() override; + bool AreSigninCookiesDeletedOnExit() override; + void AddContentSettingsObserver( + content_settings::Observer* observer) override; + void RemoveContentSettingsObserver( + content_settings::Observer* observer) override; + void DelayNetworkCall(base::OnceClosure callback) override; + std::unique_ptr CreateGaiaAuthFetcher( + GaiaAuthConsumer* consumer, + gaia::GaiaSource source) override; + bool IsNonEnterpriseUser(const std::string& username) override; + +#if !BUILDFLAG(IS_CHROMEOS_ASH) + // network::NetworkConnectionTracker::NetworkConnectionObserver + // implementation. + void OnConnectionChanged(network::mojom::ConnectionType type) override; +#endif + +#if BUILDFLAG(IS_CHROMEOS_LACROS) + absl::optional GetInitialPrimaryAccount() override; +#endif + + // Used in tests to override the URLLoaderFactory returned by + // GetURLLoaderFactory(). + void SetURLLoaderFactoryForTest( + scoped_refptr url_loader_factory); + + protected: + virtual void ShowUserManager(const base::FilePath& profile_path); + virtual void LockForceSigninProfile(const base::FilePath& profile_path); + + private: + void VerifySyncToken(); + void OnCloseBrowsersSuccess( + const signin_metrics::ProfileSignout signout_source_metric, + const base::FilePath& profile_path); + void OnCloseBrowsersAborted(const base::FilePath& profile_path); + + raw_ptr profile_; + + // Stored callback from PreSignOut(); + base::OnceCallback on_signout_decision_reached_; + +#if !BUILDFLAG(IS_CHROMEOS_ASH) + std::list delayed_callbacks_; +#endif + + bool should_display_user_manager_ = true; +#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH) + std::unique_ptr force_signin_verifier_; +#endif + + scoped_refptr + url_loader_factory_for_testing_; + + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_H_ diff --git a/chromium/chrome/browser/signin/chrome_signin_client_factory.cc b/chromium/chrome/browser/signin/chrome_signin_client_factory.cc new file mode 100644 index 00000000000..62c0d638cbd --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_client_factory.cc @@ -0,0 +1,34 @@ +// 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 "chrome/browser/signin/chrome_signin_client_factory.h" + +#include "chrome/browser/net/profile_network_context_service_factory.h" +#include "chrome/browser/profiles/profile.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" + +ChromeSigninClientFactory::ChromeSigninClientFactory() + : BrowserContextKeyedServiceFactory( + "ChromeSigninClient", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(ProfileNetworkContextServiceFactory::GetInstance()); +} + +ChromeSigninClientFactory::~ChromeSigninClientFactory() {} + +// static +SigninClient* ChromeSigninClientFactory::GetForProfile(Profile* profile) { + return static_cast( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +// static +ChromeSigninClientFactory* ChromeSigninClientFactory::GetInstance() { + return base::Singleton::get(); +} + +KeyedService* ChromeSigninClientFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + return new ChromeSigninClient(Profile::FromBrowserContext(context)); +} diff --git a/chromium/chrome/browser/signin/chrome_signin_client_factory.h b/chromium/chrome/browser/signin/chrome_signin_client_factory.h new file mode 100644 index 00000000000..e88d3f23b00 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_client_factory.h @@ -0,0 +1,37 @@ +// 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 CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "chrome/browser/signin/chrome_signin_client.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +class Profile; + +// Singleton that owns all ChromeSigninClients and associates them with +// Profiles. +class ChromeSigninClientFactory : public BrowserContextKeyedServiceFactory { + public: + // Returns the instance of SigninClient associated with this profile + // (creating one if none exists). Returns NULL if this profile cannot have an + // SigninClient (for example, if |profile| is incognito). + static SigninClient* GetForProfile(Profile* profile); + + // Returns an instance of the factory singleton. + static ChromeSigninClientFactory* GetInstance(); + + private: + friend struct base::DefaultSingletonTraits; + + ChromeSigninClientFactory(); + ~ChromeSigninClientFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/chrome_signin_client_test_util.cc b/chromium/chrome/browser/signin/chrome_signin_client_test_util.cc new file mode 100644 index 00000000000..e0412419de1 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_client_test_util.cc @@ -0,0 +1,21 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/chrome_signin_client_test_util.h" + +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/chrome_signin_client.h" +#include "components/keyed_service/core/keyed_service.h" +#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" +#include "services/network/test/test_url_loader_factory.h" + +std::unique_ptr BuildChromeSigninClientWithURLLoader( + network::TestURLLoaderFactory* test_url_loader_factory, + content::BrowserContext* context) { + Profile* profile = Profile::FromBrowserContext(context); + auto signin_client = std::make_unique(profile); + signin_client->SetURLLoaderFactoryForTest( + test_url_loader_factory->GetSafeWeakWrapper()); + return signin_client; +} diff --git a/chromium/chrome/browser/signin/chrome_signin_client_test_util.h b/chromium/chrome/browser/signin/chrome_signin_client_test_util.h new file mode 100644 index 00000000000..5c76977980f --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_client_test_util.h @@ -0,0 +1,26 @@ +// Copyright 2016 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 CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_TEST_UTIL_H_ +#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_TEST_UTIL_H_ + +#include + +class KeyedService; + +namespace content { +class BrowserContext; +} + +namespace network { +class TestURLLoaderFactory; +} + +// Creates a ChromeSigninClient using the supplied +// |test_url_loader_factory| and |context|. +std::unique_ptr BuildChromeSigninClientWithURLLoader( + network::TestURLLoaderFactory* test_url_loader_factory, + content::BrowserContext* context); + +#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_TEST_UTIL_H_ diff --git a/chromium/chrome/browser/signin/chrome_signin_client_unittest.cc b/chromium/chrome/browser/signin/chrome_signin_client_unittest.cc new file mode 100644 index 00000000000..ed2ac0db988 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_client_unittest.cc @@ -0,0 +1,438 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/chrome_signin_client.h" +#include +#include + +#include "base/bind.h" +#include "base/cxx17_backports.h" +#include "base/feature_list.h" +#include "base/memory/raw_ptr.h" +#include "base/notreached.h" +#include "base/run_loop.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/enterprise/util/managed_browser_utils.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_attributes_entry.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/signin/chrome_signin_client_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/signin_features.h" +#include "chrome/browser/signin/signin_util.h" +#include "chrome/common/pref_names.h" +#include "chrome/test/base/testing_browser_process.h" +#include "chrome/test/base/testing_profile.h" +#include "chrome/test/base/testing_profile_manager.h" +#include "components/prefs/pref_service.h" +#include "components/signin/public/base/consent_level.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "content/public/browser/network_service_instance.h" +#include "content/public/test/browser_task_environment.h" +#include "services/network/test/test_network_connection_tracker.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if !defined(OS_ANDROID) +#include "chrome/test/base/browser_with_test_window_test.h" +#endif + +// ChromeOS has its own network delay logic. +#if !BUILDFLAG(IS_CHROMEOS_ASH) + +namespace { + +class CallbackTester { + public: + CallbackTester() : called_(0) {} + + void Increment(); + void IncrementAndUnblock(base::RunLoop* run_loop); + bool WasCalledExactlyOnce(); + + private: + int called_; +}; + +void CallbackTester::Increment() { + called_++; +} + +void CallbackTester::IncrementAndUnblock(base::RunLoop* run_loop) { + Increment(); + run_loop->QuitWhenIdle(); +} + +bool CallbackTester::WasCalledExactlyOnce() { + return called_ == 1; +} + +} // namespace + +class ChromeSigninClientTest : public testing::Test { + public: + ChromeSigninClientTest() { + // Create a signed-in profile. + TestingProfile::Builder builder; + profile_ = builder.Build(); + + signin_client_ = ChromeSigninClientFactory::GetForProfile(profile()); + } + + protected: + void SetUpNetworkConnection(bool respond_synchronously, + network::mojom::ConnectionType connection_type) { + auto* tracker = network::TestNetworkConnectionTracker::GetInstance(); + tracker->SetRespondSynchronously(respond_synchronously); + tracker->SetConnectionType(connection_type); + } + + void SetConnectionType(network::mojom::ConnectionType connection_type) { + network::TestNetworkConnectionTracker::GetInstance()->SetConnectionType( + connection_type); + } + + Profile* profile() { return profile_.get(); } + SigninClient* signin_client() { return signin_client_; } + + private: + content::BrowserTaskEnvironment task_environment_; + std::unique_ptr profile_; + raw_ptr signin_client_; +}; + +TEST_F(ChromeSigninClientTest, DelayNetworkCallRunsImmediatelyWithNetwork) { + SetUpNetworkConnection(true, network::mojom::ConnectionType::CONNECTION_3G); + CallbackTester tester; + signin_client()->DelayNetworkCall( + base::BindOnce(&CallbackTester::Increment, base::Unretained(&tester))); + ASSERT_TRUE(tester.WasCalledExactlyOnce()); +} + +TEST_F(ChromeSigninClientTest, DelayNetworkCallRunsAfterGetConnectionType) { + SetUpNetworkConnection(false, network::mojom::ConnectionType::CONNECTION_3G); + + base::RunLoop run_loop; + CallbackTester tester; + signin_client()->DelayNetworkCall( + base::BindOnce(&CallbackTester::IncrementAndUnblock, + base::Unretained(&tester), &run_loop)); + ASSERT_FALSE(tester.WasCalledExactlyOnce()); + run_loop.Run(); // Wait for IncrementAndUnblock(). + ASSERT_TRUE(tester.WasCalledExactlyOnce()); +} + +TEST_F(ChromeSigninClientTest, DelayNetworkCallRunsAfterNetworkChange) { + SetUpNetworkConnection(true, network::mojom::ConnectionType::CONNECTION_NONE); + + base::RunLoop run_loop; + CallbackTester tester; + signin_client()->DelayNetworkCall( + base::BindOnce(&CallbackTester::IncrementAndUnblock, + base::Unretained(&tester), &run_loop)); + + ASSERT_FALSE(tester.WasCalledExactlyOnce()); + SetConnectionType(network::mojom::ConnectionType::CONNECTION_3G); + run_loop.Run(); // Wait for IncrementAndUnblock(). + ASSERT_TRUE(tester.WasCalledExactlyOnce()); +} + +#if !defined(OS_ANDROID) + +class MockChromeSigninClient : public ChromeSigninClient { + public: + explicit MockChromeSigninClient(Profile* profile) + : ChromeSigninClient(profile) {} + + MOCK_METHOD1(ShowUserManager, void(const base::FilePath&)); + MOCK_METHOD1(LockForceSigninProfile, void(const base::FilePath&)); + + MOCK_METHOD3(SignOutCallback, + void(signin_metrics::ProfileSignout, + signin_metrics::SignoutDelete, + SigninClient::SignoutDecision signout_decision)); +}; + +class ChromeSigninClientSignoutTest : public BrowserWithTestWindowTest { + public: + ChromeSigninClientSignoutTest() : forced_signin_setter_(true) {} + void SetUp() override { + BrowserWithTestWindowTest::SetUp(); + CreateClient(browser()->profile()); + } + + void TearDown() override { + BrowserWithTestWindowTest::TearDown(); + TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr); + } + + void CreateClient(Profile* profile) { + client_ = std::make_unique(profile); + } + + void PreSignOut(signin_metrics::ProfileSignout source_metric, + signin_metrics::SignoutDelete delete_metric) { + client_->PreSignOut(base::BindOnce(&MockChromeSigninClient::SignOutCallback, + base::Unretained(client_.get()), + source_metric, delete_metric), + source_metric); + } + + signin_util::ScopedForceSigninSetterForTesting forced_signin_setter_; + std::unique_ptr client_; +}; + +TEST_F(ChromeSigninClientSignoutTest, SignOut) { + signin_metrics::ProfileSignout source_metric = + signin_metrics::ProfileSignout::USER_CLICKED_SIGNOUT_SETTINGS; + signin_metrics::SignoutDelete delete_metric = + signin_metrics::SignoutDelete::kIgnoreMetric; + + EXPECT_CALL(*client_, ShowUserManager(browser()->profile()->GetPath())) + .Times(1); + EXPECT_CALL(*client_, LockForceSigninProfile(browser()->profile()->GetPath())) + .Times(1); + EXPECT_CALL( + *client_, + SignOutCallback(source_metric, delete_metric, + SigninClient::SignoutDecision::ALLOW_SIGNOUT)) + .Times(1); + + PreSignOut(source_metric, delete_metric); +} + +TEST_F(ChromeSigninClientSignoutTest, SignOutWithoutForceSignin) { + signin_util::ScopedForceSigninSetterForTesting signin_setter(false); + CreateClient(browser()->profile()); + + signin_metrics::ProfileSignout source_metric = + signin_metrics::ProfileSignout::USER_CLICKED_SIGNOUT_SETTINGS; + signin_metrics::SignoutDelete delete_metric = + signin_metrics::SignoutDelete::kIgnoreMetric; + + EXPECT_CALL(*client_, ShowUserManager(browser()->profile()->GetPath())) + .Times(0); + EXPECT_CALL(*client_, LockForceSigninProfile(browser()->profile()->GetPath())) + .Times(0); + EXPECT_CALL( + *client_, + SignOutCallback(source_metric, delete_metric, + SigninClient::SignoutDecision::ALLOW_SIGNOUT)) + .Times(1); + PreSignOut(source_metric, delete_metric); +} + +class ChromeSigninClientSignoutSourceTest + : public ::testing::WithParamInterface, + public ChromeSigninClientSignoutTest { + protected: + signin::IdentityTestEnvironment* identity_test_env() { + return &identity_test_env_; + } + + private: + signin::IdentityTestEnvironment identity_test_env_; +}; + +// Returns true if signout can be disallowed by policy for the given source. +bool IsSignoutDisallowedByPolicy( + Profile* profile, + signin_metrics::ProfileSignout signout_source) { + auto* identity_manager = + IdentityManagerFactory::GetForProfileIfExists(profile); + if (identity_manager && + !identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) { + return false; + } + + switch (signout_source) { + // NOTE: SIGNOUT_TEST == SIGNOUT_PREF_CHANGED. + case signin_metrics::ProfileSignout::SIGNOUT_PREF_CHANGED: + case signin_metrics::ProfileSignout::GOOGLE_SERVICE_NAME_PATTERN_CHANGED: + case signin_metrics::ProfileSignout::SIGNIN_PREF_CHANGED_DURING_SIGNIN: + case signin_metrics::ProfileSignout::USER_CLICKED_SIGNOUT_SETTINGS: + case signin_metrics::ProfileSignout::SERVER_FORCED_DISABLE: + case signin_metrics::ProfileSignout::TRANSFER_CREDENTIALS: + case signin_metrics::ProfileSignout:: + AUTHENTICATION_FAILED_WITH_FORCE_SIGNIN: + case signin_metrics::ProfileSignout::SIGNIN_NOT_ALLOWED_ON_PROFILE_INIT: + case signin_metrics::ProfileSignout::USER_TUNED_OFF_SYNC_FROM_DICE_UI: + return true; + case signin_metrics::ProfileSignout::ACCOUNT_REMOVED_FROM_DEVICE: + case signin_metrics::ProfileSignout:: + IOS_ACCOUNT_REMOVED_FROM_DEVICE_AFTER_RESTORE: + // TODO(msarda): Add more of the above cases to this "false" branch. + // For now only ACCOUNT_REMOVED_FROM_DEVICE is here to preserve the status + // quo. Additional internal sources of sign-out will be moved here in a + // follow up CL. + return false; + case signin_metrics::ProfileSignout::ABORT_SIGNIN: + // Allow signout because data has not been synced yet. + return false; + case signin_metrics::ProfileSignout::FORCE_SIGNOUT_ALWAYS_ALLOWED_FOR_TEST: + // Allow signout for tests that want to force it. + return false; + case signin_metrics::ProfileSignout::ACCOUNT_ID_MIGRATION: + // Allowed to force finish the account id migration. + return false; + case signin_metrics::ProfileSignout::USER_DELETED_ACCOUNT_COOKIES: + case signin_metrics::ProfileSignout::MOBILE_IDENTITY_CONSISTENCY_ROLLBACK: + // There's no special-casing for these in ChromeSigninClient, as they only + // happen when there's no sync account and policies aren't enforced. + // PrimaryAccountManager won't actually invoke PreSignOut in this case, + // thus it is fine for ChromeSigninClient to not have any special-casing. + return true; + case signin_metrics::ProfileSignout::NUM_PROFILE_SIGNOUT_METRICS: + NOTREACHED(); + return false; + } +} + +TEST_P(ChromeSigninClientSignoutSourceTest, UserSignoutAllowed) { + signin_metrics::ProfileSignout signout_source = GetParam(); + + TestingProfile::Builder builder; + builder.SetGuestSession(); + std::unique_ptr profile = builder.Build(); + + CreateClient(profile.get()); + ASSERT_TRUE(signin_util::IsUserSignoutAllowedForProfile(profile.get())); + + // Verify IdentityManager gets callback indicating sign-out is always allowed. + signin_metrics::SignoutDelete delete_metric = + signin_metrics::SignoutDelete::kIgnoreMetric; + EXPECT_CALL( + *client_, + SignOutCallback(signout_source, delete_metric, + SigninClient::SignoutDecision::ALLOW_SIGNOUT)) + .Times(1); + + PreSignOut(signout_source, delete_metric); +} + +#if defined(OS_WIN) || defined(OS_LINUX) || defined(OS_CHROMEOS) || \ + defined(OS_MAC) +TEST_P(ChromeSigninClientSignoutSourceTest, UserSignoutDisallowed) { + signin_metrics::ProfileSignout signout_source = GetParam(); + + TestingProfile::Builder builder; + builder.SetGuestSession(); + std::unique_ptr profile = builder.Build(); + + CreateClient(profile.get()); + + ASSERT_TRUE(signin_util::IsUserSignoutAllowedForProfile(profile.get())); + signin_util::SetUserSignoutAllowedForProfile(profile.get(), false); + ASSERT_FALSE(signin_util::IsUserSignoutAllowedForProfile(profile.get())); + + // Verify IdentityManager gets callback indicating sign-out is disallowed iff + // the source of the sign-out is a user-action. + SigninClient::SignoutDecision signout_decision = + IsSignoutDisallowedByPolicy(profile.get(), signout_source) + ? SigninClient::SignoutDecision::DISALLOW_SIGNOUT + : SigninClient::SignoutDecision::ALLOW_SIGNOUT; + signin_metrics::SignoutDelete delete_metric = + signin_metrics::SignoutDelete::kIgnoreMetric; + EXPECT_CALL(*client_, + SignOutCallback(signout_source, delete_metric, signout_decision)) + .Times(1); + + PreSignOut(signout_source, delete_metric); +} + +TEST_P(ChromeSigninClientSignoutSourceTest, UserSignoutDisallowedWithSync) { + signin_metrics::ProfileSignout signout_source = GetParam(); + + TestingProfile::Builder builder; + builder.SetGuestSession(); + std::unique_ptr profile = builder.Build(); + + CreateClient(profile.get()); + + ASSERT_TRUE(signin_util::IsUserSignoutAllowedForProfile(profile.get())); + signin_util::SetUserSignoutAllowedForProfile(profile.get(), false); + ASSERT_FALSE(signin_util::IsUserSignoutAllowedForProfile(profile.get())); + + // Verify IdentityManager gets callback indicating sign-out is disallowed iff + // the source of the sign-out is a user-action. + SigninClient::SignoutDecision signout_decision = + IsSignoutDisallowedByPolicy(profile.get(), signout_source) + ? SigninClient::SignoutDecision::DISALLOW_SIGNOUT + : SigninClient::SignoutDecision::ALLOW_SIGNOUT; + signin_metrics::SignoutDelete delete_metric = + signin_metrics::SignoutDelete::kIgnoreMetric; + identity_test_env()->MakePrimaryAccountAvailable("bob@example.com", + signin::ConsentLevel::kSync); + EXPECT_CALL(*client_, + SignOutCallback(signout_source, delete_metric, signout_decision)) + .Times(1); + + PreSignOut(signout_source, delete_metric); +} + +TEST_P(ChromeSigninClientSignoutSourceTest, + UserSignoutDisallowedAccountManagementAccepted) { + base::test::ScopedFeatureList features(kAccountPoliciesLoadedWithoutSync); + signin_metrics::ProfileSignout signout_source = GetParam(); + + TestingProfile::Builder builder; + builder.SetGuestSession(); + std::unique_ptr profile = builder.Build(); + + CreateClient(profile.get()); + + ASSERT_TRUE(signin_util::IsUserSignoutAllowedForProfile(profile.get())); + signin_util::SetUserSignoutAllowedForProfile(profile.get(), false); + ASSERT_FALSE(signin_util::IsUserSignoutAllowedForProfile(profile.get())); + + // Verify IdentityManager gets callback indicating sign-out is disallowed iff + // the source of the sign-out is a user-action. + SigninClient::SignoutDecision signout_decision = + IsSignoutDisallowedByPolicy(profile.get(), signout_source) + ? SigninClient::SignoutDecision::DISALLOW_SIGNOUT + : SigninClient::SignoutDecision::ALLOW_SIGNOUT; + signin_metrics::SignoutDelete delete_metric = + signin_metrics::SignoutDelete::kIgnoreMetric; + EXPECT_CALL(*client_, + SignOutCallback(signout_source, delete_metric, signout_decision)) + .Times(1); + + PreSignOut(signout_source, delete_metric); +} +#endif + +const signin_metrics::ProfileSignout kSignoutSources[] = { + signin_metrics::ProfileSignout::SIGNOUT_PREF_CHANGED, + signin_metrics::ProfileSignout::GOOGLE_SERVICE_NAME_PATTERN_CHANGED, + signin_metrics::ProfileSignout::SIGNIN_PREF_CHANGED_DURING_SIGNIN, + signin_metrics::ProfileSignout::USER_CLICKED_SIGNOUT_SETTINGS, + signin_metrics::ProfileSignout::ABORT_SIGNIN, + signin_metrics::ProfileSignout::SERVER_FORCED_DISABLE, + signin_metrics::ProfileSignout::TRANSFER_CREDENTIALS, + signin_metrics::ProfileSignout::AUTHENTICATION_FAILED_WITH_FORCE_SIGNIN, + signin_metrics::ProfileSignout::USER_TUNED_OFF_SYNC_FROM_DICE_UI, + signin_metrics::ProfileSignout::ACCOUNT_REMOVED_FROM_DEVICE, + signin_metrics::ProfileSignout::SIGNIN_NOT_ALLOWED_ON_PROFILE_INIT, + signin_metrics::ProfileSignout::FORCE_SIGNOUT_ALWAYS_ALLOWED_FOR_TEST, + signin_metrics::ProfileSignout::USER_DELETED_ACCOUNT_COOKIES, + signin_metrics::ProfileSignout::MOBILE_IDENTITY_CONSISTENCY_ROLLBACK, + signin_metrics::ProfileSignout::ACCOUNT_ID_MIGRATION, + signin_metrics::ProfileSignout:: + IOS_ACCOUNT_REMOVED_FROM_DEVICE_AFTER_RESTORE, +}; +static_assert(base::size(kSignoutSources) == + signin_metrics::ProfileSignout::NUM_PROFILE_SIGNOUT_METRICS, + "kSignoutSources should enumerate all ProfileSignout values"); + +INSTANTIATE_TEST_SUITE_P(AllSignoutSources, + ChromeSigninClientSignoutSourceTest, + testing::ValuesIn(kSignoutSources)); + +#endif // !defined(OS_ANDROID) +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) diff --git a/chromium/chrome/browser/signin/chrome_signin_helper.cc b/chromium/chrome/browser/signin/chrome_signin_helper.cc new file mode 100644 index 00000000000..b85dde43783 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_helper.cc @@ -0,0 +1,712 @@ +// 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 "chrome/browser/signin/chrome_signin_helper.h" + +#include +#include + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/logging.h" +#include "base/memory/ref_counted.h" +#include "base/metrics/histogram_functions.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/supports_user_data.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/prefs/incognito_mode_prefs.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_io_data.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/signin/account_reconcilor_factory.h" +#include "chrome/browser/signin/chrome_signin_client.h" +#include "chrome/browser/signin/chrome_signin_client_factory.h" +#include "chrome/browser/signin/cookie_reminter_factory.h" +#include "chrome/browser/signin/dice_response_handler.h" +#include "chrome/browser/signin/header_modification_delegate_impl.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/process_dice_header_delegate_impl.h" +#include "chrome/browser/signin/signin_features.h" +#include "chrome/browser/tab_contents/tab_util.h" +#include "chrome/browser/ui/profile_picker.h" +#include "chrome/browser/ui/webui/signin/login_ui_service.h" +#include "chrome/browser/ui/webui/signin/login_ui_service_factory.h" +#include "chrome/browser/ui/webui/signin/signin_ui_error.h" +#include "chrome/common/url_constants.h" +#include "components/account_manager_core/account_manager_facade.h" +#include "components/signin/core/browser/account_reconcilor.h" +#include "components/signin/core/browser/cookie_reminter.h" +#include "components/signin/public/base/account_consistency_method.h" +#include "components/signin/public/base/signin_buildflags.h" +#include "components/signin/public/identity_manager/accounts_cookie_mutator.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "google_apis/gaia/gaia_auth_util.h" +#include "net/http/http_response_headers.h" + +#if defined(OS_ANDROID) +#include "chrome/browser/android/signin/signin_bridge.h" +#include "ui/android/view_android.h" +#else +#include "chrome/browser/ui/browser_commands.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/browser_window.h" +#include "extensions/browser/guest_view/web_view/web_view_renderer_state.h" +#endif // defined(OS_ANDROID) + +#if defined(OS_CHROMEOS) +#include "chrome/browser/profiles/profile_manager.h" +#include "components/account_manager_core/chromeos/account_manager_facade_factory.h" +#endif // defined(OS_CHROMEOS) + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "chrome/browser/supervised_user/supervised_user_service.h" +#include "chrome/browser/supervised_user/supervised_user_service_factory.h" +#endif // BUILDFLAG(IS_CHROMEOS_ASH) + +#if BUILDFLAG(IS_CHROMEOS_LACROS) +#include "chrome/browser/lacros/account_manager/account_manager_util.h" +#include "chrome/browser/lacros/account_manager/account_profile_mapper.h" +#endif // BUILDFLAG(IS_CHROMEOS_LACROS) + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h" +#endif + +namespace signin { + +const void* const kManageAccountsHeaderReceivedUserDataKey = + &kManageAccountsHeaderReceivedUserDataKey; + +const char kChromeMirrorHeaderSource[] = "Chrome"; + +namespace { + +// Key for RequestDestructionObserverUserData. +const void* const kRequestDestructionObserverUserDataKey = + &kRequestDestructionObserverUserDataKey; + +const char kGoogleRemoveLocalAccountResponseHeader[] = + "Google-Accounts-RemoveLocalAccount"; + +const char kRemoveLocalAccountObfuscatedIDAttrName[] = "obfuscatedid"; + +// TODO(droger): Remove this delay when the Dice implementation is finished on +// the server side. +int g_dice_account_reconcilor_blocked_delay_ms = 1000; + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + +const char kGoogleSignoutResponseHeader[] = "Google-Accounts-SignOut"; + +// Refcounted wrapper that facilitates creating and deleting a +// AccountReconcilor::Lock. +class AccountReconcilorLockWrapper + : public base::RefCountedThreadSafe { + public: + explicit AccountReconcilorLockWrapper( + const content::WebContents::Getter& web_contents_getter) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + content::WebContents* web_contents = web_contents_getter.Run(); + if (!web_contents) + return; + Profile* profile = + Profile::FromBrowserContext(web_contents->GetBrowserContext()); + AccountReconcilor* account_reconcilor = + AccountReconcilorFactory::GetForProfile(profile); + account_reconcilor_lock_ = + std::make_unique(account_reconcilor); + } + + AccountReconcilorLockWrapper(const AccountReconcilorLockWrapper&) = delete; + AccountReconcilorLockWrapper& operator=(const AccountReconcilorLockWrapper&) = + delete; + + void DestroyAfterDelay() { + // TODO(dcheng): Should ReleaseSoon() support this use case? + content::GetUIThreadTaskRunner({})->PostDelayedTask( + FROM_HERE, + base::BindOnce([](scoped_refptr) {}, + base::RetainedRef(this)), + base::Milliseconds(g_dice_account_reconcilor_blocked_delay_ms)); + } + + private: + friend class base::RefCountedThreadSafe; + ~AccountReconcilorLockWrapper() { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + } + + std::unique_ptr account_reconcilor_lock_; +}; + +// Returns true if the account reconcilor needs be be blocked while a Gaia +// sign-in request is in progress. +// +// The account reconcilor must be blocked on all request that may change the +// Gaia authentication cookies. This includes: +// * Main frame requests. +// * XHR requests having Gaia URL as referrer. +bool ShouldBlockReconcilorForRequest(ChromeRequestAdapter* request) { + if (request->GetRequestDestination() == + network::mojom::RequestDestination::kDocument) { + return true; + } + + return request->IsFetchLikeAPI() && + gaia::IsGaiaSignonRealm(request->GetReferrerOrigin()); +} + +#endif // BUILDFLAG(ENABLE_DICE_SUPPORT) + +#if BUILDFLAG(IS_CHROMEOS_LACROS) +void OnLacrosAccountsAvailableAsSecondaryFetched( + AccountProfileMapper* mapper, + const base::FilePath& profile_path, + const std::vector& accounts) { + if (!accounts.empty()) { + // Pass in the current profile to signal that the user wants to select a + // _secondary_ account for this particular profile. + ProfilePicker::Show( + ProfilePicker::EntryPoint::kLacrosSelectAvailableAccount, GURL(), + profile_path); + return; + } + mapper->ShowAddAccountDialog(profile_path, + account_manager::AccountManagerFacade:: + AccountAdditionSource::kOgbAddAccount, + AccountProfileMapper::AddAccountCallback()); +} +#endif // BUILDFLAG(IS_CHROMEOS_LACROS) + +class RequestDestructionObserverUserData : public base::SupportsUserData::Data { + public: + explicit RequestDestructionObserverUserData(base::OnceClosure closure) + : closure_(std::move(closure)) {} + + RequestDestructionObserverUserData( + const RequestDestructionObserverUserData&) = delete; + RequestDestructionObserverUserData& operator=( + const RequestDestructionObserverUserData&) = delete; + + ~RequestDestructionObserverUserData() override { std::move(closure_).Run(); } + + private: + base::OnceClosure closure_; +}; + +// This user data is used as a marker that a Mirror header was found on the +// redirect chain. It does not contain any data, its presence is enough to +// indicate that a header has already be found on the request. +class ManageAccountsHeaderReceivedUserData + : public base::SupportsUserData::Data {}; + +#if BUILDFLAG(ENABLE_MIRROR) +// Processes the mirror response header on the UI thread. Currently depending +// on the value of |header_value|, it either shows the profile avatar menu, or +// opens an incognito window/tab. +void ProcessMirrorHeader( + ManageAccountsParams manage_accounts_params, + const content::WebContents::Getter& web_contents_getter) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + GAIAServiceType service_type = manage_accounts_params.service_type; + DCHECK_NE(GAIA_SERVICE_TYPE_NONE, service_type); + + content::WebContents* web_contents = web_contents_getter.Run(); + if (!web_contents) + return; + + Profile* profile = + Profile::FromBrowserContext(web_contents->GetBrowserContext()); + DCHECK(AccountConsistencyModeManager::IsMirrorEnabledForProfile(profile)) + << "Gaia should not send the X-Chrome-Manage-Accounts header " + << "when Mirror is disabled."; + AccountReconcilor* account_reconcilor = + AccountReconcilorFactory::GetForProfile(profile); + account_reconcilor->OnReceivedManageAccountsResponse(service_type); + +#if BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(IS_CHROMEOS_LACROS) + signin_metrics::LogAccountReconcilorStateOnGaiaResponse( + account_reconcilor->GetState()); + + bool should_ignore_guest_webview = true; +#if BUILDFLAG(ENABLE_EXTENSIONS) + // The mirror headers from some guest web views need to be processed. + should_ignore_guest_webview = + HeaderModificationDelegateImpl::ShouldIgnoreGuestWebViewRequest( + web_contents); +#endif + + Browser* browser = chrome::FindBrowserWithWebContents(web_contents); + // Do not do anything if the navigation happened in the "background". + if ((!browser || !browser->window()->IsActive()) && + should_ignore_guest_webview) { + return; + } + + // Record the service type. + base::UmaHistogramEnumeration("AccountManager.ManageAccountsServiceType", + service_type); + +#if BUILDFLAG(IS_CHROMEOS_ASH) + // Ignore response to background request from another profile, so dialogs are + // not displayed in the wrong profile when using ChromeOS multiprofile mode. + if (profile != ProfileManager::GetActiveUserProfile()) + return; +#endif // BUILDFLAG(IS_CHROMEOS_ASH) + + // The only allowed operations are: + // 1. Going Incognito. + // 2. Displaying a reauthentication window: Enterprise GSuite Accounts could + // have been forced through an online in-browser sign-in for sensitive + // webpages, thereby decreasing their session validity. After their session + // expires, they will receive a "Mirror" re-authentication request for all + // Google web properties. Another case when this can be triggered is + // https://crbug.com/1012649. + // 3. Displaying an account addition window: when user clicks "Add another + // account" in One Google Bar. + // 4. Displaying the Account Manager for managing accounts. + + // 1. Going incognito. + if (service_type == GAIA_SERVICE_TYPE_INCOGNITO) { + chrome::NewIncognitoWindow(profile); + return; + } + + // 2. Displaying a reauthentication window + if (!manage_accounts_params.email.empty()) { + // TODO(https://crbug.com/1226055): enable this for lacros. +#if BUILDFLAG(IS_CHROMEOS_ASH) + // Do not display the re-authentication dialog if this event was triggered + // by supervision being enabled for an account. In this situation, a + // complete signout is required. + SupervisedUserService* service = + SupervisedUserServiceFactory::GetForProfile(profile); + if (service && service->signout_required_after_supervision_enabled()) { + return; + } +#endif // BUILDFLAG(IS_CHROMEOS_ASH) + // Child users shouldn't get the re-authentication dialog for primary + // account. Log out all accounts to re-mint the cookies. + // (See the reason below.) + signin::IdentityManager* const identity_manager = + IdentityManagerFactory::GetForProfile(profile); + CoreAccountInfo primary_account = + identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin); + if (profile->IsChild() && + gaia::AreEmailsSame(primary_account.email, + manage_accounts_params.email)) { + identity_manager->GetAccountsCookieMutator()->LogOutAllAccounts( + gaia::GaiaSource::kChromeOS, base::DoNothing()); + return; + } + + // The account's cookie is invalid but the cookie has not been removed by + // |AccountReconcilor|. Ideally, this should not happen. At this point, + // |AccountReconcilor| cannot detect this state because its source of truth + // (/ListAccounts) is giving us false positives (claiming an invalid account + // to be valid). We need to store that this account's cookie is actually + // invalid, so that if/when this account is re-authenticated, we can force a + // reconciliation for this account instead of treating it as a no-op. + // See https://crbug.com/1012649 for details. + AccountInfo maybe_account_info = + identity_manager->FindExtendedAccountInfoByEmailAddress( + manage_accounts_params.email); + if (!maybe_account_info.IsEmpty()) { + CookieReminter* const cookie_reminter = + CookieReminterFactory::GetForProfile(profile); + cookie_reminter->ForceCookieRemintingOnNextTokenUpdate( + maybe_account_info); + } + + // Display a re-authentication dialog. + ::GetAccountManagerFacade(profile->GetPath().value()) + ->ShowReauthAccountDialog(account_manager::AccountManagerFacade:: + AccountAdditionSource::kContentAreaReauth, + manage_accounts_params.email); + return; + } + + // 3. Displaying an account addition window. + if (service_type == GAIA_SERVICE_TYPE_ADDSESSION) { +#if BUILDFLAG(IS_CHROMEOS_LACROS) + AccountProfileMapper* mapper = + g_browser_process->profile_manager()->GetAccountProfileMapper(); + GetAccountsAvailableAsSecondary( + mapper, profile->GetPath(), + // It's safe to bind raw `mapper`, the callback gets called iff + // `mapper` is still valid. + base::BindOnce(&OnLacrosAccountsAvailableAsSecondaryFetched, mapper, + profile->GetPath())); +#else + ::GetAccountManagerFacade(profile->GetPath().value()) + ->ShowAddAccountDialog(account_manager::AccountManagerFacade:: + AccountAdditionSource::kOgbAddAccount); +#endif + return; + } + + // 4. Displaying the Account Manager for managing accounts. + ::GetAccountManagerFacade(profile->GetPath().value()) + ->ShowManageAccountsSettings(); + return; + +#elif defined(OS_ANDROID) + if (manage_accounts_params.show_consistency_promo) { + auto* window = web_contents->GetNativeView()->GetWindowAndroid(); + if (!window) { + // The page is prefetched in the background, ignore the header. + // See https://crbug.com/1145031#c5 for details. + return; + } + SigninBridge::OpenAccountPickerBottomSheet( + window, manage_accounts_params.continue_url.empty() + ? chrome::kChromeUINativeNewTabURL + : manage_accounts_params.continue_url); + return; + } + if (service_type == signin::GAIA_SERVICE_TYPE_INCOGNITO) { + GURL url(manage_accounts_params.continue_url.empty() + ? chrome::kChromeUINativeNewTabURL + : manage_accounts_params.continue_url); + web_contents->OpenURL(content::OpenURLParams( + url, content::Referrer(), WindowOpenDisposition::OFF_THE_RECORD, + ui::PAGE_TRANSITION_AUTO_TOPLEVEL, false)); + } else { + signin_metrics::LogAccountReconcilorStateOnGaiaResponse( + account_reconcilor->GetState()); + auto* window = web_contents->GetNativeView()->GetWindowAndroid(); + if (!window) + return; + SigninBridge::OpenAccountManagementScreen(window, service_type); + } +#endif // BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(IS_CHROMEOS_LACROS) +} +#endif // BUILDFLAG(ENABLE_MIRROR) + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + +// Creates a DiceTurnOnSyncHelper. +void CreateDiceTurnOnSyncHelper(Profile* profile, + signin_metrics::AccessPoint access_point, + signin_metrics::PromoAction promo_action, + signin_metrics::Reason reason, + content::WebContents* web_contents, + const CoreAccountId& account_id) { + DCHECK(profile); + Browser* browser = web_contents + ? chrome::FindBrowserWithWebContents(web_contents) + : chrome::FindBrowserWithProfile(profile); + // DiceTurnSyncOnHelper is suicidal (it will kill itself once it finishes + // enabling sync). + new DiceTurnSyncOnHelper( + profile, browser, access_point, promo_action, reason, account_id, + DiceTurnSyncOnHelper::SigninAbortedMode::REMOVE_ACCOUNT); +} + +// Shows UI for signin errors. +void ShowDiceSigninError(Profile* profile, + content::WebContents* web_contents, + const SigninUIError& error) { + DCHECK(profile); + Browser* browser = web_contents + ? chrome::FindBrowserWithWebContents(web_contents) + : chrome::FindBrowserWithProfile(profile); + LoginUIServiceFactory::GetForProfile(profile)->DisplayLoginResult(browser, + error); +} + +void ProcessDiceHeader( + const DiceResponseParams& dice_params, + const content::WebContents::Getter& web_contents_getter) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + content::WebContents* web_contents = web_contents_getter.Run(); + if (!web_contents) + return; + + Profile* profile = + Profile::FromBrowserContext(web_contents->GetBrowserContext()); + DCHECK(!profile->IsOffTheRecord()); + + // Ignore Dice response headers if Dice is not enabled. + if (!AccountConsistencyModeManager::IsDiceEnabledForProfile(profile)) + return; + + DiceResponseHandler* dice_response_handler = + DiceResponseHandler::GetForProfile(profile); + dice_response_handler->ProcessDiceHeader( + dice_params, + std::make_unique( + web_contents, base::BindOnce(&CreateDiceTurnOnSyncHelper), + base::BindOnce(&ShowDiceSigninError))); +} +#endif // BUILDFLAG(ENABLE_DICE_SUPPORT) + +#if BUILDFLAG(ENABLE_MIRROR) +// Looks for the X-Chrome-Manage-Accounts response header, and if found, +// tries to show the avatar bubble in the browser identified by the +// child/route id. Must be called on IO thread. +void ProcessMirrorResponseHeaderIfExists(ResponseAdapter* response, + bool is_off_the_record) { + CHECK(gaia::IsGaiaSignonRealm(response->GetOrigin())); + + if (!response->IsMainFrame()) + return; + + const net::HttpResponseHeaders* response_headers = response->GetHeaders(); + if (!response_headers) + return; + + std::string header_value; + if (!response_headers->GetNormalizedHeader(kChromeManageAccountsHeader, + &header_value)) { + return; + } + + if (is_off_the_record) { + NOTREACHED() << "Gaia should not send the X-Chrome-Manage-Accounts header " + << "in incognito."; + return; + } + + ManageAccountsParams params = BuildManageAccountsParams(header_value); + // If the request does not have a response header or if the header contains + // garbage, then |service_type| is set to |GAIA_SERVICE_TYPE_NONE|. + if (params.service_type == GAIA_SERVICE_TYPE_NONE) + return; + + // Only process one mirror header per request (multiple headers on the same + // redirect chain are ignored). + if (response->GetUserData(kManageAccountsHeaderReceivedUserDataKey)) { + LOG(ERROR) << "Multiple X-Chrome-Manage-Accounts headers on a redirect " + << "chain, ignoring"; + return; + } + + response->SetUserData( + kManageAccountsHeaderReceivedUserDataKey, + std::make_unique()); + + // Post a task even if we are already on the UI thread to avoid making any + // requests while processing a throttle event. + content::GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, base::BindOnce(ProcessMirrorHeader, params, + response->GetWebContentsGetter())); +} +#endif + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +void ProcessDiceResponseHeaderIfExists(ResponseAdapter* response, + bool is_off_the_record) { + CHECK(gaia::IsGaiaSignonRealm(response->GetOrigin())); + + if (is_off_the_record) + return; + + const net::HttpResponseHeaders* response_headers = response->GetHeaders(); + if (!response_headers) + return; + + std::string header_value; + DiceResponseParams params; + if (response_headers->GetNormalizedHeader(kDiceResponseHeader, + &header_value)) { + params = BuildDiceSigninResponseParams(header_value); + // The header must be removed for privacy reasons, so that renderers never + // have access to the authorization code. + response->RemoveHeader(kDiceResponseHeader); + } else if (response_headers->GetNormalizedHeader(kGoogleSignoutResponseHeader, + &header_value)) { + params = BuildDiceSignoutResponseParams(header_value); + } + + // If the request does not have a response header or if the header contains + // garbage, then |user_intention| is set to |NONE|. + if (params.user_intention == DiceAction::NONE) + return; + + // Post a task even if we are already on the UI thread to avoid making any + // requests while processing a throttle event. + content::GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, base::BindOnce(ProcessDiceHeader, std::move(params), + response->GetWebContentsGetter())); +} +#endif // BUILDFLAG(ENABLE_DICE_SUPPORT) + +std::string ParseGaiaIdFromRemoveLocalAccountResponseHeader( + const net::HttpResponseHeaders* response_headers) { + if (!response_headers) + return std::string(); + + std::string header_value; + if (!response_headers->GetNormalizedHeader( + kGoogleRemoveLocalAccountResponseHeader, &header_value)) { + return std::string(); + } + + const SigninHeaderHelper::ResponseHeaderDictionary header_dictionary = + SigninHeaderHelper::ParseAccountConsistencyResponseHeader(header_value); + + std::string gaia_id; + const auto it = + header_dictionary.find(kRemoveLocalAccountObfuscatedIDAttrName); + if (it != header_dictionary.end()) { + // The Gaia ID is wrapped in quotes. + base::TrimString(it->second, "\"", &gaia_id); + } + return gaia_id; +} + +void ProcessRemoveLocalAccountResponseHeaderIfExists(ResponseAdapter* response, + bool is_off_the_record) { + CHECK(gaia::IsGaiaSignonRealm(response->GetOrigin())); + + if (is_off_the_record) + return; + + const std::string gaia_id = + ParseGaiaIdFromRemoveLocalAccountResponseHeader(response->GetHeaders()); + + if (gaia_id.empty()) + return; + + content::WebContents* web_contents = response->GetWebContentsGetter().Run(); + // The tab could have just closed. Technically, it would be possible to + // refactor the code to pass around the profile by other means, but this + // should be rare enough to be worth supporting. + if (!web_contents) + return; + + Profile* profile = + Profile::FromBrowserContext(web_contents->GetBrowserContext()); + DCHECK(!profile->IsOffTheRecord()); + + IdentityManagerFactory::GetForProfile(profile) + ->GetAccountsCookieMutator() + ->RemoveLoggedOutAccountByGaiaId(gaia_id); +} + +} // namespace + +ChromeRequestAdapter::ChromeRequestAdapter( + const GURL& url, + const net::HttpRequestHeaders& original_headers, + net::HttpRequestHeaders* modified_headers, + std::vector* headers_to_remove) + : RequestAdapter(url, + original_headers, + modified_headers, + headers_to_remove) {} + +ChromeRequestAdapter::~ChromeRequestAdapter() = default; + +ResponseAdapter::ResponseAdapter() = default; + +ResponseAdapter::~ResponseAdapter() = default; + +void SetDiceAccountReconcilorBlockDelayForTesting(int delay_ms) { + g_dice_account_reconcilor_blocked_delay_ms = delay_ms; +} + +void FixAccountConsistencyRequestHeader( + ChromeRequestAdapter* request, + const GURL& redirect_url, + bool is_off_the_record, + int incognito_availibility, + AccountConsistencyMethod account_consistency, + const std::string& gaia_id, + signin::Tribool is_child_account, +#if BUILDFLAG(IS_CHROMEOS_ASH) + bool is_secondary_account_addition_allowed, +#endif +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + bool is_sync_enabled, + const std::string& signin_scoped_device_id, +#endif + content_settings::CookieSettings* cookie_settings) { + if (is_off_the_record) + return; // Account consistency is disabled in incognito. + + // If new url is eligible to have the header, add it, otherwise remove it. + +// Mirror header: +#if BUILDFLAG(ENABLE_MIRROR) + int profile_mode_mask = PROFILE_MODE_DEFAULT; + if (incognito_availibility == + static_cast(IncognitoModePrefs::Availability::kDisabled) || + IncognitoModePrefs::ArePlatformParentalControlsEnabled()) { + profile_mode_mask |= PROFILE_MODE_INCOGNITO_DISABLED; + } + +#if BUILDFLAG(IS_CHROMEOS_ASH) + if (!is_secondary_account_addition_allowed) { + account_consistency = AccountConsistencyMethod::kMirror; + // Can't add new accounts. + profile_mode_mask |= PROFILE_MODE_ADD_ACCOUNT_DISABLED; + } +#endif + + AppendOrRemoveMirrorRequestHeader( + request, redirect_url, gaia_id, is_child_account, account_consistency, + cookie_settings, profile_mode_mask, kChromeMirrorHeaderSource, + /*force_account_consistency=*/false); +#endif // BUILDFLAG(ENABLE_MIRROR) + +// Dice header: +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + bool dice_header_added = AppendOrRemoveDiceRequestHeader( + request, redirect_url, gaia_id, is_sync_enabled, account_consistency, + cookie_settings, signin_scoped_device_id); + + // Block the AccountReconcilor while the Dice requests are in flight. This + // allows the DiceReponseHandler to process the response before the reconcilor + // starts. + if (dice_header_added && ShouldBlockReconcilorForRequest(request)) { + auto lock_wrapper = base::MakeRefCounted( + request->GetWebContentsGetter()); + // On destruction of the request |lock_wrapper| will be released. + request->SetDestructionCallback(base::BindOnce( + &AccountReconcilorLockWrapper::DestroyAfterDelay, lock_wrapper)); + } +#endif +} + +void ProcessAccountConsistencyResponseHeaders(ResponseAdapter* response, + const GURL& redirect_url, + bool is_off_the_record) { + if (!gaia::IsGaiaSignonRealm(response->GetOrigin())) + return; + +#if BUILDFLAG(ENABLE_MIRROR) + // See if the response contains the X-Chrome-Manage-Accounts header. If so + // show the profile avatar bubble so that user can complete signin/out + // action the native UI. + ProcessMirrorResponseHeaderIfExists(response, is_off_the_record); +#endif + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + // Process the Dice header: on sign-in, exchange the authorization code for a + // refresh token, on sign-out just follow the sign-out URL. + ProcessDiceResponseHeaderIfExists(response, is_off_the_record); +#endif // BUILDFLAG(ENABLE_DICE_SUPPORT) + + if (base::FeatureList::IsEnabled(kProcessGaiaRemoveLocalAccountHeader)) { + ProcessRemoveLocalAccountResponseHeaderIfExists(response, + is_off_the_record); + } +} + +std::string ParseGaiaIdFromRemoveLocalAccountResponseHeaderForTesting( + const net::HttpResponseHeaders* response_headers) { + return ParseGaiaIdFromRemoveLocalAccountResponseHeader(response_headers); +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/chrome_signin_helper.h b/chromium/chrome/browser/signin/chrome_signin_helper.h new file mode 100644 index 00000000000..7d6ea870458 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_helper.h @@ -0,0 +1,129 @@ +// 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 CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_HELPER_H_ +#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_HELPER_H_ + +#include +#include + +#include "base/supports_user_data.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/prefs/incognito_mode_prefs.h" +#include "components/signin/core/browser/signin_header_helper.h" +#include "content/public/browser/web_contents.h" +#include "services/network/public/mojom/fetch_api.mojom-shared.h" + +namespace content_settings { +class CookieSettings; +} + +namespace net { +class HttpResponseHeaders; +} + +class GURL; + +// Utility functions for handling Chrome/Gaia headers during signin process. +// Chrome identity should always stay in sync with Gaia identity. Therefore +// Chrome needs to send Gaia special header for requests from a connected +// profile, so that Gaia can modify its response accordingly and let Chrome +// handle signin accordingly. +namespace signin { + +enum class Tribool; + +// Key for ManageAccountsHeaderReceivedUserData. Exposed for testing. +extern const void* const kManageAccountsHeaderReceivedUserDataKey; + +// The source to use when constructing the Mirror header. +extern const char kChromeMirrorHeaderSource[]; + +class ChromeRequestAdapter : public RequestAdapter { + public: + ChromeRequestAdapter(const GURL& url, + const net::HttpRequestHeaders& original_headers, + net::HttpRequestHeaders* modified_headers, + std::vector* headers_to_remove); + + ChromeRequestAdapter(const ChromeRequestAdapter&) = delete; + ChromeRequestAdapter& operator=(const ChromeRequestAdapter&) = delete; + + ~ChromeRequestAdapter() override; + + virtual content::WebContents::Getter GetWebContentsGetter() const = 0; + + virtual network::mojom::RequestDestination GetRequestDestination() const = 0; + + virtual bool IsFetchLikeAPI() const = 0; + + virtual GURL GetReferrerOrigin() const = 0; + + // Associate a callback with this request which will be executed when the + // request is complete (including any redirects). If a callback was already + // registered this function does nothing. + virtual void SetDestructionCallback(base::OnceClosure closure) = 0; +}; + +class ResponseAdapter { + public: + ResponseAdapter(); + + ResponseAdapter(const ResponseAdapter&) = delete; + ResponseAdapter& operator=(const ResponseAdapter&) = delete; + + virtual ~ResponseAdapter(); + + virtual content::WebContents::Getter GetWebContentsGetter() const = 0; + virtual bool IsMainFrame() const = 0; + virtual GURL GetOrigin() const = 0; + virtual const net::HttpResponseHeaders* GetHeaders() const = 0; + virtual void RemoveHeader(const std::string& name) = 0; + + virtual base::SupportsUserData::Data* GetUserData(const void* key) const = 0; + virtual void SetUserData( + const void* key, + std::unique_ptr data) = 0; +}; + +// When Dice is enabled, the AccountReconcilor is blocked for a short delay +// after sending requests to Gaia. Exposed for testing. +void SetDiceAccountReconcilorBlockDelayForTesting(int delay_ms); + +// Adds an account consistency header to Gaia requests from a connected profile, +// with the exception of requests from gaia webview. +// Removes the header if it is already in the headers but should not be there. +void FixAccountConsistencyRequestHeader( + ChromeRequestAdapter* request, + const GURL& redirect_url, + bool is_off_the_record, + int incognito_availibility, + AccountConsistencyMethod account_consistency, + const std::string& gaia_id, + signin::Tribool is_child_account, +#if BUILDFLAG(IS_CHROMEOS_ASH) + bool is_secondary_account_addition_allowed, +#endif +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + bool is_sync_enabled, + const std::string& signin_scoped_device_id, +#endif + content_settings::CookieSettings* cookie_settings); + +// Processes account consistency response headers (X-Chrome-Manage-Accounts and +// Dice). |redirect_url| is empty if the request is not a redirect. +void ProcessAccountConsistencyResponseHeaders(ResponseAdapter* response, + const GURL& redirect_url, + bool is_off_the_record); + +// Parses and returns an account ID (Gaia ID) from HTTP response header +// Google-Accounts-RemoveLocalAccount. Returns an empty string if parsing +// failed. Exposed for testing purposes. +std::string ParseGaiaIdFromRemoveLocalAccountResponseHeaderForTesting( + const net::HttpResponseHeaders* response_headers); + +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_HELPER_H_ diff --git a/chromium/chrome/browser/signin/chrome_signin_helper_unittest.cc b/chromium/chrome/browser/signin/chrome_signin_helper_unittest.cc new file mode 100644 index 00000000000..8a5757aabe3 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_helper_unittest.cc @@ -0,0 +1,182 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/chrome_signin_helper.h" + +#include +#include + +#include "base/run_loop.h" +#include "base/strings/stringprintf.h" +#include "build/chromeos_buildflags.h" +#include "components/signin/core/browser/signin_header_helper.h" +#include "components/signin/public/base/signin_buildflags.h" +#include "content/public/test/browser_task_environment.h" +#include "net/http/http_response_headers.h" +#include "net/traffic_annotation/network_traffic_annotation_test_helper.h" +#include "net/url_request/url_request.h" +#include "net/url_request/url_request_context.h" +#include "net/url_request/url_request_filter.h" +#include "net/url_request/url_request_interceptor.h" +#include "net/url_request/url_request_test_job.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace { + +#if BUILDFLAG(ENABLE_MIRROR) || BUILDFLAG(IS_CHROMEOS_LACROS) +const char kChromeManageAccountsHeader[] = "X-Chrome-Manage-Accounts"; +const char kMirrorAction[] = "action=ADDSESSION"; +#endif + +// URLRequestInterceptor adding a account consistency response header to Gaia +// responses. +class TestRequestInterceptor : public net::URLRequestInterceptor { + public: + explicit TestRequestInterceptor(const std::string& header_name, + const std::string& header_value) + : header_name_(header_name), header_value_(header_value) {} + ~TestRequestInterceptor() override = default; + + private: + std::unique_ptr MaybeInterceptRequest( + net::URLRequest* request) const override { + std::string response_headers = + base::StringPrintf("HTTP/1.1 200 OK\n\n%s: %s\n", header_name_.c_str(), + header_value_.c_str()); + return std::make_unique(request, response_headers, + "", true); + } + + const std::string header_name_; + const std::string header_value_; +}; + +class TestResponseAdapter : public signin::ResponseAdapter, + public base::SupportsUserData { + public: + TestResponseAdapter(const std::string& header_name, + const std::string& header_value, + bool is_main_frame) + : is_main_frame_(is_main_frame), + headers_(new net::HttpResponseHeaders(std::string())) { + headers_->SetHeader(header_name, header_value); + } + + TestResponseAdapter(const TestResponseAdapter&) = delete; + TestResponseAdapter& operator=(const TestResponseAdapter&) = delete; + + ~TestResponseAdapter() override {} + + content::WebContents::Getter GetWebContentsGetter() const override { + return base::BindRepeating( + []() -> content::WebContents* { return nullptr; }); + } + bool IsMainFrame() const override { return is_main_frame_; } + GURL GetOrigin() const override { + return GURL("https://accounts.google.com"); + } + const net::HttpResponseHeaders* GetHeaders() const override { + return headers_.get(); + } + + void RemoveHeader(const std::string& name) override { + headers_->RemoveHeader(name); + } + + base::SupportsUserData::Data* GetUserData(const void* key) const override { + return base::SupportsUserData::GetUserData(key); + } + + void SetUserData( + const void* key, + std::unique_ptr data) override { + return base::SupportsUserData::SetUserData(key, std::move(data)); + } + + private: + bool is_main_frame_; + scoped_refptr headers_; +}; + +} // namespace + +class ChromeSigninHelperTest : public testing::Test { + protected: + ChromeSigninHelperTest() + : task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP) {} + + ~ChromeSigninHelperTest() override = default; + + content::BrowserTaskEnvironment task_environment_; + std::unique_ptr test_request_delegate_; +}; + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +// Tests that Dice response headers are removed after being processed. +TEST_F(ChromeSigninHelperTest, RemoveDiceSigninHeader) { + // Process the header. + TestResponseAdapter adapter(signin::kDiceResponseHeader, "Foo", + /*is_main_frame=*/false); + signin::ProcessAccountConsistencyResponseHeaders(&adapter, GURL(), + false /* is_incognito */); + + // Check that the header has been removed. + EXPECT_FALSE(adapter.GetHeaders()->HasHeader(signin::kDiceResponseHeader)); +} +#endif // BUILDFLAG(ENABLE_DICE_SUPPORT) + +#if BUILDFLAG(ENABLE_MIRROR) || BUILDFLAG(IS_CHROMEOS_LACROS) +// Tests that user data is set on Mirror requests. +TEST_F(ChromeSigninHelperTest, MirrorMainFrame) { + // Process the header. + TestResponseAdapter response_adapter(kChromeManageAccountsHeader, + kMirrorAction, + /*is_main_frame=*/true); + signin::ProcessAccountConsistencyResponseHeaders(&response_adapter, GURL(), + false /* is_incognito */); + // Check that the header has not been removed. + EXPECT_TRUE( + response_adapter.GetHeaders()->HasHeader(kChromeManageAccountsHeader)); + // Request was flagged with the user data. + EXPECT_TRUE(response_adapter.GetUserData( + signin::kManageAccountsHeaderReceivedUserDataKey)); +} + +// Tests that user data is not set on Mirror requests for sub frames. +TEST_F(ChromeSigninHelperTest, MirrorSubFrame) { + // Process the header. + TestResponseAdapter response_adapter(kChromeManageAccountsHeader, + kMirrorAction, + /*is_main_frame=*/false); + signin::ProcessAccountConsistencyResponseHeaders(&response_adapter, GURL(), + false /* is_incognito */); + // Request was not flagged with the user data. + EXPECT_FALSE(response_adapter.GetUserData( + signin::kManageAccountsHeaderReceivedUserDataKey)); +} +#endif // BUILDFLAG(ENABLE_MIRROR) || BUILDFLAG(IS_CHROMEOS_LACROS) + +TEST_F(ChromeSigninHelperTest, + ParseGaiaIdFromRemoveLocalAccountResponseHeader) { + EXPECT_EQ("123456", + signin::ParseGaiaIdFromRemoveLocalAccountResponseHeaderForTesting( + TestResponseAdapter("Google-Accounts-RemoveLocalAccount", + "obfuscatedid=\"123456\"", + /*is_main_frame=*/false) + .GetHeaders())); + EXPECT_EQ("123456", + signin::ParseGaiaIdFromRemoveLocalAccountResponseHeaderForTesting( + TestResponseAdapter("Google-Accounts-RemoveLocalAccount", + "obfuscatedid=\"123456\",foo=\"bar\"", + /*is_main_frame=*/false) + .GetHeaders())); + EXPECT_EQ( + "", + signin::ParseGaiaIdFromRemoveLocalAccountResponseHeaderForTesting( + TestResponseAdapter("Google-Accounts-RemoveLocalAccount", "malformed", + /*is_main_frame=*/false) + .GetHeaders())); +} diff --git a/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.cc b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.cc new file mode 100644 index 00000000000..d4b8d911a9e --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.cc @@ -0,0 +1,551 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h" + +#include "base/barrier_closure.h" +#include "base/bind.h" +#include "base/memory/ptr_util.h" +#include "base/memory/raw_ptr.h" +#include "base/supports_user_data.h" +#include "build/build_config.h" +#include "build/buildflag.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/chrome_signin_helper.h" +#include "chrome/browser/signin/header_modification_delegate.h" +#include "chrome/browser/signin/header_modification_delegate_impl.h" +#include "components/signin/core/browser/signin_header_helper.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/render_process_host.h" +#include "extensions/browser/guest_view/web_view/web_view_renderer_state.h" +#include "extensions/buildflags/buildflags.h" +#include "google_apis/gaia/gaia_auth_util.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "net/base/net_errors.h" +#include "services/network/public/cpp/resource_request.h" +#include "services/network/public/mojom/early_hints.mojom.h" +#include "services/network/public/mojom/fetch_api.mojom-shared.h" +#include "services/network/public/mojom/url_loader.mojom.h" +#include "services/network/public/mojom/url_loader_factory.mojom.h" +#include "services/network/public/mojom/url_response_head.mojom.h" + +#if defined(OS_ANDROID) +#include "chrome/browser/android/tab_android.h" +#include "chrome/browser/android/tab_web_contents_delegate_android.h" +#endif + +namespace signin { + +namespace { + +// User data key for BrowserContextData. +const void* const kBrowserContextUserDataKey = &kBrowserContextUserDataKey; + +// Owns all of the ProxyingURLLoaderFactorys for a given Profile. +class BrowserContextData : public base::SupportsUserData::Data { + public: + BrowserContextData(const BrowserContextData&) = delete; + BrowserContextData& operator=(const BrowserContextData&) = delete; + + ~BrowserContextData() override {} + + static void StartProxying( + Profile* profile, + content::WebContents::Getter web_contents_getter, + mojo::PendingReceiver receiver, + mojo::PendingRemote target_factory) { + auto* self = static_cast( + profile->GetUserData(kBrowserContextUserDataKey)); + if (!self) { + self = new BrowserContextData(); + profile->SetUserData(kBrowserContextUserDataKey, base::WrapUnique(self)); + } + +#if defined(OS_ANDROID) + bool is_custom_tab = false; + content::WebContents* web_contents = web_contents_getter.Run(); + if (web_contents) { + auto* delegate = + TabAndroid::FromWebContents(web_contents) + ? static_cast( + web_contents->GetDelegate()) + : nullptr; + is_custom_tab = delegate && delegate->IsCustomTab(); + } + auto delegate = std::make_unique( + profile, /*incognito_enabled=*/!is_custom_tab); +#else + auto delegate = std::make_unique(profile); +#endif + auto proxy = std::make_unique( + std::move(delegate), std::move(web_contents_getter), + std::move(receiver), std::move(target_factory), + base::BindOnce(&BrowserContextData::RemoveProxy, + self->weak_factory_.GetWeakPtr())); + self->proxies_.emplace(std::move(proxy)); + } + + void RemoveProxy(ProxyingURLLoaderFactory* proxy) { + auto it = proxies_.find(proxy); + DCHECK(it != proxies_.end()); + proxies_.erase(it); + } + + private: + BrowserContextData() {} + + std::set, base::UniquePtrComparator> + proxies_; + + base::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace + +class ProxyingURLLoaderFactory::InProgressRequest + : public network::mojom::URLLoader, + public network::mojom::URLLoaderClient, + public base::SupportsUserData { + public: + InProgressRequest( + ProxyingURLLoaderFactory* factory, + mojo::PendingReceiver loader_receiver, + int32_t request_id, + uint32_t options, + const network::ResourceRequest& request, + mojo::PendingRemote client, + const net::MutableNetworkTrafficAnnotationTag& traffic_annotation); + + InProgressRequest(const InProgressRequest&) = delete; + InProgressRequest& operator=(const InProgressRequest&) = delete; + + ~InProgressRequest() override { + if (destruction_callback_) + std::move(destruction_callback_).Run(); + } + + // network::mojom::URLLoader: + void FollowRedirect( + const std::vector& removed_headers, + const net::HttpRequestHeaders& modified_headers, + const net::HttpRequestHeaders& modified_cors_exempt_headers, + const absl::optional& new_url) override; + + void SetPriority(net::RequestPriority priority, + int32_t intra_priority_value) override { + target_loader_->SetPriority(priority, intra_priority_value); + } + + void PauseReadingBodyFromNet() override { + target_loader_->PauseReadingBodyFromNet(); + } + + void ResumeReadingBodyFromNet() override { + target_loader_->ResumeReadingBodyFromNet(); + } + + // network::mojom::URLLoaderClient: + void OnReceiveEarlyHints(network::mojom::EarlyHintsPtr early_hints) override { + target_client_->OnReceiveEarlyHints(std::move(early_hints)); + } + void OnReceiveResponse(network::mojom::URLResponseHeadPtr head) override; + void OnReceiveRedirect(const net::RedirectInfo& redirect_info, + network::mojom::URLResponseHeadPtr head) override; + + void OnUploadProgress(int64_t current_position, + int64_t total_size, + OnUploadProgressCallback callback) override { + target_client_->OnUploadProgress(current_position, total_size, + std::move(callback)); + } + + void OnReceiveCachedMetadata(mojo_base::BigBuffer data) override { + target_client_->OnReceiveCachedMetadata(std::move(data)); + } + + void OnTransferSizeUpdated(int32_t transfer_size_diff) override { + target_client_->OnTransferSizeUpdated(transfer_size_diff); + } + + void OnStartLoadingResponseBody( + mojo::ScopedDataPipeConsumerHandle body) override { + target_client_->OnStartLoadingResponseBody(std::move(body)); + } + + void OnComplete(const network::URLLoaderCompletionStatus& status) override { + target_client_->OnComplete(status); + } + + private: + class ProxyRequestAdapter; + class ProxyResponseAdapter; + + void OnBindingsClosed() { + // Destroys |this|. + factory_->RemoveRequest(this); + } + + // Back pointer to the factory which owns this class. + const raw_ptr factory_; + + // Information about the current request. + GURL request_url_; + GURL response_url_; + GURL referrer_origin_; + net::HttpRequestHeaders headers_; + net::HttpRequestHeaders cors_exempt_headers_; + net::RedirectInfo redirect_info_; + const network::mojom::RequestDestination request_destination_; + const bool is_main_frame_; + const bool is_fetch_like_api_; + + base::OnceClosure destruction_callback_; + + // Messages received by |client_receiver_| are forwarded to |target_client_|. + mojo::Receiver client_receiver_{this}; + mojo::Remote target_client_; + + // Messages received by |loader_receiver_| are forwarded to |target_loader_|. + mojo::Receiver loader_receiver_; + mojo::Remote target_loader_; +}; + +class ProxyingURLLoaderFactory::InProgressRequest::ProxyRequestAdapter + : public ChromeRequestAdapter { + public: + // Does not take |modified_cors_exempt_headers| just because we don't have a + // use-case to modify it in this class now. + ProxyRequestAdapter(InProgressRequest* in_progress_request, + const net::HttpRequestHeaders& original_headers, + net::HttpRequestHeaders* modified_headers, + std::vector* removed_headers) + : ChromeRequestAdapter(in_progress_request->request_url_, + original_headers, + modified_headers, + removed_headers), + in_progress_request_(in_progress_request) { + DCHECK(in_progress_request_); + } + + ProxyRequestAdapter(const ProxyRequestAdapter&) = delete; + ProxyRequestAdapter& operator=(const ProxyRequestAdapter&) = delete; + + ~ProxyRequestAdapter() override = default; + + content::WebContents::Getter GetWebContentsGetter() const override { + return in_progress_request_->factory_->web_contents_getter_; + } + + network::mojom::RequestDestination GetRequestDestination() const override { + return in_progress_request_->request_destination_; + } + + bool IsFetchLikeAPI() const override { + return in_progress_request_->is_fetch_like_api_; + } + + GURL GetReferrerOrigin() const override { + return in_progress_request_->referrer_origin_; + } + + void SetDestructionCallback(base::OnceClosure closure) override { + if (!in_progress_request_->destruction_callback_) + in_progress_request_->destruction_callback_ = std::move(closure); + } + + private: + const raw_ptr in_progress_request_; +}; + +class ProxyingURLLoaderFactory::InProgressRequest::ProxyResponseAdapter + : public ResponseAdapter { + public: + ProxyResponseAdapter(InProgressRequest* in_progress_request, + net::HttpResponseHeaders* headers) + : in_progress_request_(in_progress_request), headers_(headers) { + DCHECK(in_progress_request_); + DCHECK(headers_); + } + + ProxyResponseAdapter(const ProxyResponseAdapter&) = delete; + ProxyResponseAdapter& operator=(const ProxyResponseAdapter&) = delete; + + ~ProxyResponseAdapter() override = default; + + // signin::ResponseAdapter + content::WebContents::Getter GetWebContentsGetter() const override { + return in_progress_request_->factory_->web_contents_getter_; + } + + bool IsMainFrame() const override { + return in_progress_request_->is_main_frame_; + } + + GURL GetOrigin() const override { + return in_progress_request_->response_url_.DeprecatedGetOriginAsURL(); + } + + const net::HttpResponseHeaders* GetHeaders() const override { + return headers_; + } + + void RemoveHeader(const std::string& name) override { + headers_->RemoveHeader(name); + } + + base::SupportsUserData::Data* GetUserData(const void* key) const override { + return in_progress_request_->GetUserData(key); + } + + void SetUserData( + const void* key, + std::unique_ptr data) override { + in_progress_request_->SetUserData(key, std::move(data)); + } + + private: + const raw_ptr in_progress_request_; + const raw_ptr headers_; +}; + +ProxyingURLLoaderFactory::InProgressRequest::InProgressRequest( + ProxyingURLLoaderFactory* factory, + mojo::PendingReceiver loader_receiver, + int32_t request_id, + uint32_t options, + const network::ResourceRequest& request, + mojo::PendingRemote client, + const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) + : factory_(factory), + request_url_(request.url), + response_url_(request.url), + referrer_origin_(request.referrer.DeprecatedGetOriginAsURL()), + request_destination_(request.destination), + is_main_frame_(request.is_main_frame), + is_fetch_like_api_(request.is_fetch_like_api), + target_client_(std::move(client)), + loader_receiver_(this, std::move(loader_receiver)) { + mojo::PendingRemote proxy_client = + client_receiver_.BindNewPipeAndPassRemote(); + + net::HttpRequestHeaders modified_headers; + std::vector removed_headers; + ProxyRequestAdapter adapter(this, request.headers, &modified_headers, + &removed_headers); + factory_->delegate_->ProcessRequest(&adapter, GURL() /* redirect_url */); + + if (modified_headers.IsEmpty() && removed_headers.empty()) { + factory_->target_factory_->CreateLoaderAndStart( + target_loader_.BindNewPipeAndPassReceiver(), request_id, options, + request, std::move(proxy_client), traffic_annotation); + + // We need to keep a full copy of the request headers in case there is a + // redirect and the request headers need to be modified again. + headers_.CopyFrom(request.headers); + cors_exempt_headers_.CopyFrom(request.cors_exempt_headers); + } else { + network::ResourceRequest request_copy = request; + request_copy.headers.MergeFrom(modified_headers); + for (const std::string& name : removed_headers) { + request_copy.headers.RemoveHeader(name); + request_copy.cors_exempt_headers.RemoveHeader(name); + } + + factory_->target_factory_->CreateLoaderAndStart( + target_loader_.BindNewPipeAndPassReceiver(), request_id, options, + request_copy, std::move(proxy_client), traffic_annotation); + + headers_.Swap(&request_copy.headers); + cors_exempt_headers_.Swap(&request_copy.cors_exempt_headers); + } + + base::RepeatingClosure closure = base::BarrierClosure( + 2, base::BindOnce(&InProgressRequest::OnBindingsClosed, + base::Unretained(this))); + loader_receiver_.set_disconnect_handler(closure); + client_receiver_.set_disconnect_handler(closure); +} + +void ProxyingURLLoaderFactory::InProgressRequest::FollowRedirect( + const std::vector& removed_headers_ext, + const net::HttpRequestHeaders& modified_headers_ext, + const net::HttpRequestHeaders& modified_cors_exempt_headers_ext, + const absl::optional& opt_new_url) { + std::vector removed_headers = removed_headers_ext; + net::HttpRequestHeaders modified_headers = modified_headers_ext; + net::HttpRequestHeaders modified_cors_exempt_headers = + modified_cors_exempt_headers_ext; + ProxyRequestAdapter adapter(this, headers_, &modified_headers, + &removed_headers); + factory_->delegate_->ProcessRequest(&adapter, redirect_info_.new_url); + + headers_.MergeFrom(modified_headers); + cors_exempt_headers_.MergeFrom(modified_cors_exempt_headers); + for (const std::string& name : removed_headers) { + headers_.RemoveHeader(name); + cors_exempt_headers_.RemoveHeader(name); + } + + target_loader_->FollowRedirect(removed_headers, modified_headers, + modified_cors_exempt_headers, opt_new_url); + + request_url_ = redirect_info_.new_url; + referrer_origin_ = + GURL(redirect_info_.new_referrer).DeprecatedGetOriginAsURL(); +} + +void ProxyingURLLoaderFactory::InProgressRequest::OnReceiveResponse( + network::mojom::URLResponseHeadPtr head) { + // Even though |head| is const we can get a non-const pointer to the headers + // and modifications we made are passed to the target client. + ProxyResponseAdapter adapter(this, head->headers.get()); + factory_->delegate_->ProcessResponse(&adapter, GURL() /* redirect_url */); + target_client_->OnReceiveResponse(std::move(head)); +} + +void ProxyingURLLoaderFactory::InProgressRequest::OnReceiveRedirect( + const net::RedirectInfo& redirect_info, + network::mojom::URLResponseHeadPtr head) { + // Even though |head| is const we can get a non-const pointer to the headers + // and modifications we made are passed to the target client. + ProxyResponseAdapter adapter(this, head->headers.get()); + factory_->delegate_->ProcessResponse(&adapter, redirect_info.new_url); + target_client_->OnReceiveRedirect(redirect_info, std::move(head)); + + // The request URL returned by ProxyResponseAdapter::GetOrigin() is updated + // immediately but the URL and referrer + redirect_info_ = redirect_info; + response_url_ = redirect_info.new_url; +} + +ProxyingURLLoaderFactory::ProxyingURLLoaderFactory( + std::unique_ptr delegate, + content::WebContents::Getter web_contents_getter, + mojo::PendingReceiver loader_receiver, + mojo::PendingRemote target_factory, + DisconnectCallback on_disconnect) { + DCHECK(proxy_receivers_.empty()); + DCHECK(!target_factory_.is_bound()); + DCHECK(!delegate_); + DCHECK(!web_contents_getter_); + DCHECK(!on_disconnect_); + + delegate_ = std::move(delegate); + web_contents_getter_ = std::move(web_contents_getter); + on_disconnect_ = std::move(on_disconnect); + + target_factory_.Bind(std::move(target_factory)); + target_factory_.set_disconnect_handler(base::BindOnce( + &ProxyingURLLoaderFactory::OnTargetFactoryError, base::Unretained(this))); + + proxy_receivers_.Add(this, std::move(loader_receiver)); + proxy_receivers_.set_disconnect_handler(base::BindRepeating( + &ProxyingURLLoaderFactory::OnProxyBindingError, base::Unretained(this))); +} + +ProxyingURLLoaderFactory::~ProxyingURLLoaderFactory() = default; + +// static +bool ProxyingURLLoaderFactory::MaybeProxyRequest( + content::RenderFrameHost* render_frame_host, + bool is_navigation, + const url::Origin& request_initiator, + mojo::PendingReceiver* factory_receiver) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + // Navigation requests are handled using signin::URLLoaderThrottle. + if (is_navigation) + return false; + + if (!render_frame_host) + return false; + + // This proxy should only be installed for subresource requests from a frame + // that is rendering the GAIA signon realm. + if (!gaia::IsGaiaSignonRealm(request_initiator.GetURL())) + return false; + + auto* web_contents = + content::WebContents::FromRenderFrameHost(render_frame_host); + auto* profile = + Profile::FromBrowserContext(web_contents->GetBrowserContext()); + if (profile->IsOffTheRecord()) + return false; + +#if BUILDFLAG(ENABLE_EXTENSIONS) + // Most requests from guest web views are ignored. + if (HeaderModificationDelegateImpl::ShouldIgnoreGuestWebViewRequest( + web_contents)) { + return false; + } +#endif + + auto proxied_receiver = std::move(*factory_receiver); + // TODO(crbug.com/955171): Replace this with PendingRemote. + mojo::PendingRemote target_factory_remote; + *factory_receiver = target_factory_remote.InitWithNewPipeAndPassReceiver(); + + auto web_contents_getter = + base::BindRepeating(&content::WebContents::FromFrameTreeNodeId, + render_frame_host->GetFrameTreeNodeId()); + + BrowserContextData::StartProxying(profile, std::move(web_contents_getter), + std::move(proxied_receiver), + std::move(target_factory_remote)); + return true; +} + +void ProxyingURLLoaderFactory::CreateLoaderAndStart( + mojo::PendingReceiver loader_receiver, + int32_t request_id, + uint32_t options, + const network::ResourceRequest& request, + mojo::PendingRemote client, + const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) { + requests_.insert(std::make_unique( + this, std::move(loader_receiver), request_id, options, request, + std::move(client), traffic_annotation)); +} + +void ProxyingURLLoaderFactory::Clone( + mojo::PendingReceiver loader_receiver) { + proxy_receivers_.Add(this, std::move(loader_receiver)); +} + +void ProxyingURLLoaderFactory::OnTargetFactoryError() { + // Stop calls to CreateLoaderAndStart() when |target_factory_| is invalid. + target_factory_.reset(); + proxy_receivers_.Clear(); + + MaybeDestroySelf(); +} + +void ProxyingURLLoaderFactory::OnProxyBindingError() { + if (proxy_receivers_.empty()) + target_factory_.reset(); + + MaybeDestroySelf(); +} + +void ProxyingURLLoaderFactory::RemoveRequest(InProgressRequest* request) { + auto it = requests_.find(request); + DCHECK(it != requests_.end()); + requests_.erase(it); + + MaybeDestroySelf(); +} + +void ProxyingURLLoaderFactory::MaybeDestroySelf() { + // Even if all URLLoaderFactory pipes connected to this object have been + // closed it has to stay alive until all active requests have completed. + if (target_factory_.is_bound() || !requests_.empty()) + return; + + // Deletes |this|. + std::move(on_disconnect_).Run(this); +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h new file mode 100644 index 00000000000..28589803472 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h @@ -0,0 +1,100 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_PROXYING_URL_LOADER_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_PROXYING_URL_LOADER_FACTORY_H_ + +#include "base/callback.h" +#include "base/containers/unique_ptr_adapters.h" +#include "base/memory/ref_counted_delete_on_sequence.h" +#include "content/public/browser/web_contents.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/receiver_set.h" +#include "mojo/public/cpp/bindings/remote.h" +#include "services/network/public/mojom/url_loader_factory.mojom.h" + +#include +#include + +namespace content { +class RenderFrameHost; +} + +namespace signin { + +class HeaderModificationDelegate; + +// This class is used to modify sub-resource requests made by the renderer +// that is displaying the GAIA signin realm, to the GAIA signin realm. When +// such a request is made a proxy is inserted between the renderer and the +// Network Service to modify request and response headers. +class ProxyingURLLoaderFactory : public network::mojom::URLLoaderFactory { + public: + using DisconnectCallback = + base::OnceCallback; + + // Constructor public for testing purposes. New instances should be created + // by calling MaybeProxyRequest(). + ProxyingURLLoaderFactory( + std::unique_ptr delegate, + content::WebContents::Getter web_contents_getter, + mojo::PendingReceiver receiver, + mojo::PendingRemote target_factory, + DisconnectCallback on_disconnect); + + ProxyingURLLoaderFactory(const ProxyingURLLoaderFactory&) = delete; + ProxyingURLLoaderFactory& operator=(const ProxyingURLLoaderFactory&) = delete; + + ~ProxyingURLLoaderFactory() override; + + // Called when a renderer needs a URLLoaderFactory to give this module the + // opportunity to install a proxy. This is only done when + // https://accounts.google.com is loaded in non-incognito mode. Returns true + // when |factory_request| has been proxied. + static bool MaybeProxyRequest( + content::RenderFrameHost* render_frame_host, + bool is_navigation, + const url::Origin& request_initiator, + mojo::PendingReceiver* + factory_receiver); + + // network::mojom::URLLoaderFactory: + void CreateLoaderAndStart( + mojo::PendingReceiver loader_receiver, + int32_t request_id, + uint32_t options, + const network::ResourceRequest& request, + mojo::PendingRemote client, + const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) + override; + void Clone(mojo::PendingReceiver + loader_receiver) override; + + private: + friend class base::DeleteHelper; + friend class base::RefCountedDeleteOnSequence; + + class InProgressRequest; + class ProxyRequestAdapter; + class ProxyResponseAdapter; + + void OnTargetFactoryError(); + void OnProxyBindingError(); + void RemoveRequest(InProgressRequest* request); + void MaybeDestroySelf(); + + std::unique_ptr delegate_; + content::WebContents::Getter web_contents_getter_; + + mojo::ReceiverSet proxy_receivers_; + std::set, base::UniquePtrComparator> + requests_; + mojo::Remote target_factory_; + DisconnectCallback on_disconnect_; +}; + +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_PROXYING_URL_LOADER_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory_unittest.cc b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory_unittest.cc new file mode 100644 index 00000000000..2d9772cc070 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory_unittest.cc @@ -0,0 +1,359 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h" + +#include +#include + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/run_loop.h" +#include "base/test/mock_callback.h" +#include "chrome/browser/signin/chrome_signin_helper.h" +#include "chrome/browser/signin/header_modification_delegate.h" +#include "content/public/test/browser_task_environment.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "net/traffic_annotation/network_traffic_annotation_test_helper.h" +#include "services/network/public/cpp/simple_url_loader.h" +#include "services/network/public/mojom/fetch_api.mojom-shared.h" +#include "services/network/public/mojom/url_response_head.mojom.h" +#include "services/network/test/test_url_loader_factory.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::Invoke; +using testing::_; + +namespace signin { + +namespace { + +class MockDelegate : public HeaderModificationDelegate { + public: + MockDelegate() = default; + + MockDelegate(const MockDelegate&) = delete; + MockDelegate& operator=(const MockDelegate&) = delete; + + ~MockDelegate() override = default; + + MOCK_METHOD1(ShouldInterceptNavigation, bool(content::WebContents* contents)); + MOCK_METHOD2(ProcessRequest, + void(ChromeRequestAdapter* request_adapter, + const GURL& redirect_url)); + MOCK_METHOD2(ProcessResponse, + void(ResponseAdapter* response_adapter, + const GURL& redirect_url)); + + base::WeakPtr GetWeakPtr() { + return weak_factory_.GetWeakPtr(); + } + + private: + base::WeakPtrFactory weak_factory_{this}; +}; + +content::WebContents::Getter NullWebContentsGetter() { + return base::BindRepeating([]() -> content::WebContents* { return nullptr; }); +} + +} // namespace + +class ChromeSigninProxyingURLLoaderFactoryTest : public testing::Test { + public: + ChromeSigninProxyingURLLoaderFactoryTest() + : test_factory_receiver_(&test_factory_) {} + + ChromeSigninProxyingURLLoaderFactoryTest( + const ChromeSigninProxyingURLLoaderFactoryTest&) = delete; + ChromeSigninProxyingURLLoaderFactoryTest& operator=( + const ChromeSigninProxyingURLLoaderFactoryTest&) = delete; + + ~ChromeSigninProxyingURLLoaderFactoryTest() override {} + + base::WeakPtr StartRequest( + std::unique_ptr request) { + loader_ = network::SimpleURLLoader::Create(std::move(request), + TRAFFIC_ANNOTATION_FOR_TESTS); + + mojo::Remote factory_remote; + auto factory_request = factory_remote.BindNewPipeAndPassReceiver(); + loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie( + factory_remote.get(), + base::BindOnce( + &ChromeSigninProxyingURLLoaderFactoryTest::OnDownloadComplete, + base::Unretained(this))); + + auto delegate = std::make_unique(); + base::WeakPtr delegate_weak = delegate->GetWeakPtr(); + + proxying_factory_ = std::make_unique( + std::move(delegate), NullWebContentsGetter(), + std::move(factory_request), + test_factory_receiver_.BindNewPipeAndPassRemote(), + base::BindOnce(&ChromeSigninProxyingURLLoaderFactoryTest::OnDisconnect, + base::Unretained(this))); + + return delegate_weak; + } + + void CloseFactoryReceiver() { test_factory_receiver_.reset(); } + + network::TestURLLoaderFactory* factory() { return &test_factory_; } + network::SimpleURLLoader* loader() { return loader_.get(); } + std::string* response_body() { return response_body_.get(); } + + void OnDownloadComplete(std::unique_ptr body) { + response_body_ = std::move(body); + } + + private: + void OnDisconnect(ProxyingURLLoaderFactory* factory) { + EXPECT_EQ(factory, proxying_factory_.get()); + proxying_factory_.reset(); + } + + content::BrowserTaskEnvironment task_environment_; + std::unique_ptr loader_; + std::unique_ptr proxying_factory_; + network::TestURLLoaderFactory test_factory_; + mojo::Receiver test_factory_receiver_; + std::unique_ptr response_body_; +}; + +TEST_F(ChromeSigninProxyingURLLoaderFactoryTest, NoModification) { + auto request = std::make_unique(); + request->url = GURL("https://google.com/"); + + factory()->AddResponse("https://google.com/", "Hello."); + base::WeakPtr delegate = StartRequest(std::move(request)); + + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(net::OK, loader()->NetError()); + ASSERT_TRUE(response_body()); + EXPECT_EQ("Hello.", *response_body()); +} + +TEST_F(ChromeSigninProxyingURLLoaderFactoryTest, ModifyHeaders) { + const GURL kTestURL("https://google.com/index.html"); + const GURL kTestReferrer("https://chrome.com/referrer.html"); + const GURL kTestRedirectURL("https://youtube.com/index.html"); + + // Set up the request. + auto request = std::make_unique(); + request->url = kTestURL; + request->referrer = kTestReferrer; + request->destination = network::mojom::RequestDestination::kDocument; + request->is_main_frame = true; + request->headers.SetHeader("X-Request-1", "Foo"); + + base::WeakPtr delegate = StartRequest(std::move(request)); + + // The first destruction callback added by ProcessRequest is expected to be + // called. The second (added after a redirect) will not be. + base::MockCallback destruction_callback; + EXPECT_CALL(destruction_callback, Run()).Times(1); + base::MockCallback ignored_destruction_callback; + EXPECT_CALL(ignored_destruction_callback, Run()).Times(0); + + // The delegate will be called twice to process a request, first when the + // request is started and again when the request is redirected. + EXPECT_CALL(*delegate, ProcessRequest(_, _)) + .WillOnce( + Invoke([&](ChromeRequestAdapter* adapter, const GURL& redirect_url) { + EXPECT_EQ(kTestURL, adapter->GetUrl()); + EXPECT_EQ(network::mojom::RequestDestination::kDocument, + adapter->GetRequestDestination()); + EXPECT_EQ(GURL("https://chrome.com"), adapter->GetReferrerOrigin()); + + EXPECT_TRUE(adapter->HasHeader("X-Request-1")); + adapter->RemoveRequestHeaderByName("X-Request-1"); + EXPECT_FALSE(adapter->HasHeader("X-Request-1")); + + adapter->SetExtraHeaderByName("X-Request-2", "Bar"); + EXPECT_TRUE(adapter->HasHeader("X-Request-2")); + + EXPECT_EQ(GURL(), redirect_url); + + adapter->SetDestructionCallback(destruction_callback.Get()); + })) + .WillOnce( + Invoke([&](ChromeRequestAdapter* adapter, const GURL& redirect_url) { + EXPECT_EQ(network::mojom::RequestDestination::kDocument, + adapter->GetRequestDestination()); + + // Changes to the URL and referrer take effect after the redirect + // is followed. + EXPECT_EQ(kTestURL, adapter->GetUrl()); + EXPECT_EQ(GURL("https://chrome.com"), adapter->GetReferrerOrigin()); + + // X-Request-1 and X-Request-2 were modified in the previous call to + // ProcessRequest(). These changes should still be present. + EXPECT_FALSE(adapter->HasHeader("X-Request-1")); + EXPECT_TRUE(adapter->HasHeader("X-Request-2")); + + adapter->RemoveRequestHeaderByName("X-Request-2"); + EXPECT_FALSE(adapter->HasHeader("X-Request-2")); + + adapter->SetExtraHeaderByName("X-Request-3", "Baz"); + EXPECT_TRUE(adapter->HasHeader("X-Request-3")); + + EXPECT_EQ(kTestRedirectURL, redirect_url); + + adapter->SetDestructionCallback(ignored_destruction_callback.Get()); + })); + + const void* const kResponseUserDataKey = &kResponseUserDataKey; + std::unique_ptr response_user_data = + std::make_unique(); + base::SupportsUserData::Data* response_user_data_ptr = + response_user_data.get(); + + // The delegate will also be called twice to process a response, first when + // the redirect is received and again for the redirect response. + EXPECT_CALL(*delegate, ProcessResponse(_, _)) + .WillOnce(Invoke([&](ResponseAdapter* adapter, const GURL& redirect_url) { + EXPECT_EQ(GURL("https://google.com"), adapter->GetOrigin()); + EXPECT_TRUE(adapter->IsMainFrame()); + + adapter->SetUserData(kResponseUserDataKey, + std::move(response_user_data)); + EXPECT_EQ(response_user_data_ptr, + adapter->GetUserData(kResponseUserDataKey)); + + const net::HttpResponseHeaders* headers = adapter->GetHeaders(); + EXPECT_TRUE(headers->HasHeader("X-Response-1")); + EXPECT_TRUE(headers->HasHeader("X-Response-2")); + adapter->RemoveHeader("X-Response-2"); + + EXPECT_EQ(kTestRedirectURL, redirect_url); + })) + .WillOnce(Invoke([&](ResponseAdapter* adapter, const GURL& redirect_url) { + EXPECT_EQ(GURL("https://youtube.com"), adapter->GetOrigin()); + EXPECT_TRUE(adapter->IsMainFrame()); + + EXPECT_EQ(response_user_data_ptr, + adapter->GetUserData(kResponseUserDataKey)); + + const net::HttpResponseHeaders* headers = adapter->GetHeaders(); + // This is a new response and so previous headers should not carry over. + EXPECT_FALSE(headers->HasHeader("X-Response-1")); + EXPECT_FALSE(headers->HasHeader("X-Response-2")); + + EXPECT_TRUE(headers->HasHeader("X-Response-3")); + EXPECT_TRUE(headers->HasHeader("X-Response-4")); + adapter->RemoveHeader("X-Response-3"); + + EXPECT_EQ(GURL(), redirect_url); + })); + + // Set up a redirect and final response. + { + net::RedirectInfo redirect_info; + redirect_info.new_url = kTestRedirectURL; + // An HTTPS to HTTPS redirect such as this wouldn't normally change the + // referrer but we do for testing purposes. + redirect_info.new_referrer = kTestURL.spec(); + + auto redirect_head = network::mojom::URLResponseHead::New(); + redirect_head->headers = base::MakeRefCounted(""); + redirect_head->headers->SetHeader("X-Response-1", "Foo"); + redirect_head->headers->SetHeader("X-Response-2", "Bar"); + + auto response_head = network::mojom::URLResponseHead::New(); + response_head->headers = base::MakeRefCounted(""); + response_head->headers->SetHeader("X-Response-3", "Foo"); + response_head->headers->SetHeader("X-Response-4", "Bar"); + std::string body("Hello."); + network::URLLoaderCompletionStatus status; + status.decoded_body_length = body.size(); + + network::TestURLLoaderFactory::Redirects redirects; + redirects.push_back({redirect_info, std::move(redirect_head)}); + + factory()->AddResponse(kTestURL, std::move(response_head), body, status, + std::move(redirects)); + } + + // Wait for the request to complete and check the response. + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(net::OK, loader()->NetError()); + const network::mojom::URLResponseHead* response_head = + loader()->ResponseInfo(); + ASSERT_TRUE(response_head && response_head->headers); + EXPECT_FALSE(response_head->headers->HasHeader("X-Response-3")); + EXPECT_TRUE(response_head->headers->HasHeader("X-Response-4")); + ASSERT_TRUE(response_body()); + EXPECT_EQ("Hello.", *response_body()); + + // NOTE: TestURLLoaderFactory currently does not expose modifications to + // request headers and so we cannot verify that the modifications have been + // passed to the target URLLoader. +} + +TEST_F(ChromeSigninProxyingURLLoaderFactoryTest, TargetFactoryFailure) { + mojo::Remote factory_remote; + mojo::PendingRemote + pending_target_factory_remote; + auto target_factory_receiver = + pending_target_factory_remote.InitWithNewPipeAndPassReceiver(); + + // Without a target factory the proxy will process no requests. + auto delegate = std::make_unique(); + EXPECT_CALL(*delegate, ProcessRequest(_, _)).Times(0); + + auto proxying_factory = std::make_unique( + std::move(delegate), NullWebContentsGetter(), + factory_remote.BindNewPipeAndPassReceiver(), + std::move(pending_target_factory_remote), base::DoNothing()); + + // Close |target_factory_receiver| instead of binding it to a + // URLLoaderFactory. Spin the message loop so that the connection error + // handler can run. + target_factory_receiver = mojo::NullReceiver(); + base::RunLoop().RunUntilIdle(); + + auto request = std::make_unique(); + request->url = GURL("https://google.com"); + auto loader = network::SimpleURLLoader::Create(std::move(request), + TRAFFIC_ANNOTATION_FOR_TESTS); + loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie( + factory_remote.get(), + base::BindOnce( + &ChromeSigninProxyingURLLoaderFactoryTest::OnDownloadComplete, + base::Unretained(this))); + base::RunLoop().RunUntilIdle(); + + EXPECT_FALSE(response_body()); + EXPECT_EQ(net::ERR_FAILED, loader->NetError()); +} + +TEST_F(ChromeSigninProxyingURLLoaderFactoryTest, RequestKeepAlive) { + // Start the request. + auto request = std::make_unique(); + request->url = GURL("https://google.com"); + base::WeakPtr delegate = StartRequest(std::move(request)); + base::RunLoop().RunUntilIdle(); + + // Close the factory receiver and spin the message loop again to allow the + // connection error handler to be called. + CloseFactoryReceiver(); + base::RunLoop().RunUntilIdle(); + + // The ProxyingURLLoaderFactory should not have been destroyed yet because + // there is still an in progress request that has not been completed. + EXPECT_TRUE(delegate); + + // Complete the request. + factory()->AddResponse("https://google.com", "Hello."); + base::RunLoop().RunUntilIdle(); + + EXPECT_FALSE(delegate); + EXPECT_EQ(net::OK, loader()->NetError()); + ASSERT_TRUE(response_body()); + EXPECT_EQ("Hello.", *response_body()); +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.cc b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.cc new file mode 100644 index 00000000000..63fe0ae0a6b --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.cc @@ -0,0 +1,141 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.h" + +#include +#include + +#include "build/build_config.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "components/signin/core/browser/signin_status_metrics_provider.h" +#include "components/signin/public/identity_manager/identity_manager.h" + +#if !defined(OS_ANDROID) +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/browser_list.h" +#endif + +ChromeSigninStatusMetricsProviderDelegate:: + ChromeSigninStatusMetricsProviderDelegate() {} + +ChromeSigninStatusMetricsProviderDelegate:: + ~ChromeSigninStatusMetricsProviderDelegate() { +#if !defined(OS_ANDROID) + BrowserList::RemoveObserver(this); +#endif + + auto* factory = IdentityManagerFactory::GetInstance(); + if (factory) + factory->RemoveObserver(this); +} + +void ChromeSigninStatusMetricsProviderDelegate::Initialize() { +#if !defined(OS_ANDROID) + // On Android, there is always only one profile in any situation, opening new + // windows (which is possible with only some Android devices) will not change + // the opened profiles signin status. + BrowserList::AddObserver(this); +#endif + + auto* factory = IdentityManagerFactory::GetInstance(); + if (factory) + factory->AddObserver(this); +} + +AccountsStatus +ChromeSigninStatusMetricsProviderDelegate::GetStatusOfAllAccounts() { + ProfileManager* profile_manager = g_browser_process->profile_manager(); + std::vector profile_list = profile_manager->GetLoadedProfiles(); + + AccountsStatus accounts_status; + accounts_status.num_accounts = profile_list.size(); + for (Profile* profile : profile_list) { +#if !defined(OS_ANDROID) + if (chrome::GetBrowserCount(profile) == 0) { + // The profile is loaded, but there's no opened browser for this profile. + continue; + } +#endif + accounts_status.num_opened_accounts++; + + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile->GetOriginalProfile()); + if (identity_manager && + identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) { + accounts_status.num_signed_in_accounts++; + } + } + + return accounts_status; +} + +std::vector +ChromeSigninStatusMetricsProviderDelegate::GetIdentityManagersForAllAccounts() { + ProfileManager* profile_manager = g_browser_process->profile_manager(); + std::vector profiles = profile_manager->GetLoadedProfiles(); + + std::vector managers; + for (Profile* profile : profiles) { + auto* identity_manager = + IdentityManagerFactory::GetForProfileIfExists(profile); + if (identity_manager) + managers.push_back(identity_manager); + } + + return managers; +} + +#if !defined(OS_ANDROID) +void ChromeSigninStatusMetricsProviderDelegate::OnBrowserAdded( + Browser* browser) { + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(browser->profile()); + + // Nothing will change if the opened browser is in incognito mode. + if (!identity_manager) + return; + + const bool signed_in = + identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync); + UpdateStatusWhenBrowserAdded(signed_in); +} +#endif + +void ChromeSigninStatusMetricsProviderDelegate::IdentityManagerCreated( + signin::IdentityManager* identity_manager) { + owner()->OnIdentityManagerCreated(identity_manager); +} + +void ChromeSigninStatusMetricsProviderDelegate::UpdateStatusWhenBrowserAdded( + bool signed_in) { +#if !defined(OS_ANDROID) + SigninStatusMetricsProviderBase::SigninStatus status = + owner()->signin_status(); + + // NOTE: If |status| is MIXED_SIGNIN_STATUS, this method + // intentionally does not update it. + if ((status == SigninStatusMetricsProviderBase::ALL_PROFILES_NOT_SIGNED_IN && + signed_in) || + (status == SigninStatusMetricsProviderBase::ALL_PROFILES_SIGNED_IN && + !signed_in)) { + owner()->UpdateSigninStatus( + SigninStatusMetricsProviderBase::MIXED_SIGNIN_STATUS); + } else if (status == SigninStatusMetricsProviderBase::UNKNOWN_SIGNIN_STATUS) { + // If when function ProvideCurrentSessionData() is called, Chrome is + // running in the background with no browser window opened, |signin_status_| + // will be reset to |UNKNOWN_SIGNIN_STATUS|. Then this newly added browser + // is the only opened browser/profile and its signin status represents + // the whole status. + owner()->UpdateSigninStatus( + signed_in + ? SigninStatusMetricsProviderBase::ALL_PROFILES_SIGNED_IN + : SigninStatusMetricsProviderBase::ALL_PROFILES_NOT_SIGNED_IN); + } +#endif +} diff --git a/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.h b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.h new file mode 100644 index 00000000000..800d5f8e3f5 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.h @@ -0,0 +1,58 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_STATUS_METRICS_PROVIDER_DELEGATE_H_ +#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_STATUS_METRICS_PROVIDER_DELEGATE_H_ + +#include + +#include "base/gtest_prod_util.h" +#include "build/build_config.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "components/signin/core/browser/signin_status_metrics_provider_delegate.h" + +#if !defined(OS_ANDROID) +#include "chrome/browser/ui/browser_list_observer.h" +#endif // !defined(OS_ANDROID) + +class ChromeSigninStatusMetricsProviderDelegate + : public SigninStatusMetricsProviderDelegate, +#if !defined(OS_ANDROID) + public BrowserListObserver, +#endif + public IdentityManagerFactory::Observer { + public: + ChromeSigninStatusMetricsProviderDelegate(); + + ChromeSigninStatusMetricsProviderDelegate( + const ChromeSigninStatusMetricsProviderDelegate&) = delete; + ChromeSigninStatusMetricsProviderDelegate& operator=( + const ChromeSigninStatusMetricsProviderDelegate&) = delete; + + ~ChromeSigninStatusMetricsProviderDelegate() override; + + private: + FRIEND_TEST_ALL_PREFIXES(ChromeSigninStatusMetricsProviderDelegateTest, + UpdateStatusWhenBrowserAdded); + + // SigninStatusMetricsProviderDelegate: + void Initialize() override; + AccountsStatus GetStatusOfAllAccounts() override; + std::vector GetIdentityManagersForAllAccounts() + override; + +#if !defined(OS_ANDROID) + // BrowserListObserver: + void OnBrowserAdded(Browser* browser) override; +#endif + + // IdentityManagerFactoryObserver: + void IdentityManagerCreated( + signin::IdentityManager* identity_manager) override; + + // Updates the sign-in status right after a new browser is opened. + void UpdateStatusWhenBrowserAdded(bool signed_in); +}; + +#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_STATUS_METRICS_PROVIDER_DELEGATE_H_ diff --git a/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate_unittest.cc b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate_unittest.cc new file mode 100644 index 00000000000..fd83b3aada5 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate_unittest.cc @@ -0,0 +1,61 @@ +// 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 "chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.h" + +#include + +#include "build/build_config.h" +#include "components/signin/core/browser/signin_status_metrics_provider.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if !defined(OS_ANDROID) +TEST(ChromeSigninStatusMetricsProviderDelegateTest, + UpdateStatusWhenBrowserAdded) { + content::BrowserTaskEnvironment task_environment; + + std::unique_ptr delegate( + new ChromeSigninStatusMetricsProviderDelegate); + ChromeSigninStatusMetricsProviderDelegate* raw_delegate = delegate.get(); + std::unique_ptr metrics_provider = + SigninStatusMetricsProvider::CreateInstance(std::move(delegate)); + + // Initial status is all signed in and then a signed-in browser is opened. + metrics_provider->UpdateInitialSigninStatusForTesting(2, 2); + raw_delegate->UpdateStatusWhenBrowserAdded(true); + EXPECT_EQ(SigninStatusMetricsProviderBase::ALL_PROFILES_SIGNED_IN, + metrics_provider->GetSigninStatusForTesting()); + + // Initial status is all signed in and then a signed-out browser is opened. + metrics_provider->UpdateInitialSigninStatusForTesting(2, 2); + raw_delegate->UpdateStatusWhenBrowserAdded(false); + EXPECT_EQ(SigninStatusMetricsProviderBase::MIXED_SIGNIN_STATUS, + metrics_provider->GetSigninStatusForTesting()); + + // Initial status is all signed out and then a signed-in browser is opened. + metrics_provider->UpdateInitialSigninStatusForTesting(2, 0); + raw_delegate->UpdateStatusWhenBrowserAdded(true); + EXPECT_EQ(SigninStatusMetricsProviderBase::MIXED_SIGNIN_STATUS, + metrics_provider->GetSigninStatusForTesting()); + + // Initial status is all signed out and then a signed-out browser is opened. + metrics_provider->UpdateInitialSigninStatusForTesting(2, 0); + raw_delegate->UpdateStatusWhenBrowserAdded(false); + EXPECT_EQ(SigninStatusMetricsProviderBase::ALL_PROFILES_NOT_SIGNED_IN, + metrics_provider->GetSigninStatusForTesting()); + + // Initial status is mixed and then a signed-in browser is opened. + metrics_provider->UpdateInitialSigninStatusForTesting(2, 1); + raw_delegate->UpdateStatusWhenBrowserAdded(true); + EXPECT_EQ(SigninStatusMetricsProviderBase::MIXED_SIGNIN_STATUS, + metrics_provider->GetSigninStatusForTesting()); + + // Initial status is mixed and then a signed-out browser is opened. + metrics_provider->UpdateInitialSigninStatusForTesting(2, 1); + raw_delegate->UpdateStatusWhenBrowserAdded(false); + EXPECT_EQ(SigninStatusMetricsProviderBase::MIXED_SIGNIN_STATUS, + metrics_provider->GetSigninStatusForTesting()); +} +#endif diff --git a/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.cc b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.cc new file mode 100644 index 00000000000..528fc7b6d44 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.cc @@ -0,0 +1,189 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/chrome_signin_url_loader_throttle.h" + +#include "base/memory/ptr_util.h" +#include "base/memory/raw_ptr.h" +#include "chrome/browser/signin/chrome_signin_helper.h" +#include "chrome/browser/signin/header_modification_delegate.h" +#include "components/signin/core/browser/signin_header_helper.h" +#include "services/network/public/cpp/resource_request.h" +#include "services/network/public/mojom/url_response_head.mojom.h" + +namespace signin { + +class URLLoaderThrottle::ThrottleRequestAdapter : public ChromeRequestAdapter { + public: + ThrottleRequestAdapter(URLLoaderThrottle* throttle, + const net::HttpRequestHeaders& original_headers, + net::HttpRequestHeaders* modified_headers, + std::vector* headers_to_remove) + : ChromeRequestAdapter(throttle->request_url_, + original_headers, + modified_headers, + headers_to_remove), + throttle_(throttle) {} + + ThrottleRequestAdapter(const ThrottleRequestAdapter&) = delete; + ThrottleRequestAdapter& operator=(const ThrottleRequestAdapter&) = delete; + + ~ThrottleRequestAdapter() override = default; + + // ChromeRequestAdapter + content::WebContents::Getter GetWebContentsGetter() const override { + return throttle_->web_contents_getter_; + } + + network::mojom::RequestDestination GetRequestDestination() const override { + return throttle_->request_destination_; + } + + bool IsFetchLikeAPI() const override { + return throttle_->request_is_fetch_like_api_; + } + + GURL GetReferrerOrigin() const override { + return throttle_->request_referrer_.DeprecatedGetOriginAsURL(); + } + + void SetDestructionCallback(base::OnceClosure closure) override { + if (!throttle_->destruction_callback_) + throttle_->destruction_callback_ = std::move(closure); + } + + private: + const raw_ptr throttle_; +}; + +class URLLoaderThrottle::ThrottleResponseAdapter : public ResponseAdapter { + public: + ThrottleResponseAdapter(URLLoaderThrottle* throttle, + net::HttpResponseHeaders* headers) + : throttle_(throttle), headers_(headers) {} + + ThrottleResponseAdapter(const ThrottleResponseAdapter&) = delete; + ThrottleResponseAdapter& operator=(const ThrottleResponseAdapter&) = delete; + + ~ThrottleResponseAdapter() override = default; + + // ResponseAdapter + content::WebContents::Getter GetWebContentsGetter() const override { + return throttle_->web_contents_getter_; + } + + bool IsMainFrame() const override { + return throttle_->request_destination_ == + network::mojom::RequestDestination::kDocument; + } + + GURL GetOrigin() const override { + return throttle_->request_url_.DeprecatedGetOriginAsURL(); + } + + const net::HttpResponseHeaders* GetHeaders() const override { + return headers_; + } + + void RemoveHeader(const std::string& name) override { + headers_->RemoveHeader(name); + } + + base::SupportsUserData::Data* GetUserData(const void* key) const override { + return throttle_->GetUserData(key); + } + + void SetUserData( + const void* key, + std::unique_ptr data) override { + throttle_->SetUserData(key, std::move(data)); + } + + private: + const raw_ptr throttle_; + raw_ptr headers_; +}; + +// static +std::unique_ptr URLLoaderThrottle::MaybeCreate( + std::unique_ptr delegate, + content::WebContents::Getter web_contents_getter) { + if (!delegate->ShouldInterceptNavigation(web_contents_getter.Run())) + return nullptr; + + return base::WrapUnique(new URLLoaderThrottle( + std::move(delegate), std::move(web_contents_getter))); +} + +URLLoaderThrottle::~URLLoaderThrottle() { + if (destruction_callback_) + std::move(destruction_callback_).Run(); +} + +void URLLoaderThrottle::WillStartRequest(network::ResourceRequest* request, + bool* defer) { + request_url_ = request->url; + request_referrer_ = request->referrer; + request_destination_ = request->destination; + request_is_fetch_like_api_ = request->is_fetch_like_api; + + net::HttpRequestHeaders modified_request_headers; + std::vector to_be_removed_request_headers; + + ThrottleRequestAdapter adapter(this, request->headers, + &modified_request_headers, + &to_be_removed_request_headers); + delegate_->ProcessRequest(&adapter, GURL() /* redirect_url */); + + request->headers.MergeFrom(modified_request_headers); + for (const std::string& name : to_be_removed_request_headers) + request->headers.RemoveHeader(name); + + // We need to keep a full copy of the request headers for later calls to + // FixAccountConsistencyRequestHeader. Perhaps this could be replaced with + // more specific per-request state. + request_headers_.CopyFrom(request->headers); + request_cors_exempt_headers_.CopyFrom(request->cors_exempt_headers); +} + +void URLLoaderThrottle::WillRedirectRequest( + net::RedirectInfo* redirect_info, + const network::mojom::URLResponseHead& response_head, + bool* /* defer */, + std::vector* to_be_removed_request_headers, + net::HttpRequestHeaders* modified_request_headers, + net::HttpRequestHeaders* modified_cors_exempt_request_headers) { + ThrottleRequestAdapter request_adapter(this, request_headers_, + modified_request_headers, + to_be_removed_request_headers); + delegate_->ProcessRequest(&request_adapter, redirect_info->new_url); + + request_headers_.MergeFrom(*modified_request_headers); + for (const std::string& name : *to_be_removed_request_headers) + request_headers_.RemoveHeader(name); + + // Modifications to |response_head.headers| will be passed to the + // URLLoaderClient even though |response_head| is const. + ThrottleResponseAdapter response_adapter(this, response_head.headers.get()); + delegate_->ProcessResponse(&response_adapter, redirect_info->new_url); + + request_url_ = redirect_info->new_url; + request_referrer_ = GURL(redirect_info->new_referrer); +} + +void URLLoaderThrottle::WillProcessResponse( + const GURL& response_url, + network::mojom::URLResponseHead* response_head, + bool* defer) { + ThrottleResponseAdapter adapter(this, response_head->headers.get()); + delegate_->ProcessResponse(&adapter, GURL() /* redirect_url */); +} + +URLLoaderThrottle::URLLoaderThrottle( + std::unique_ptr delegate, + content::WebContents::Getter web_contents_getter) + : delegate_(std::move(delegate)), + web_contents_getter_(std::move(web_contents_getter)) {} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.h b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.h new file mode 100644 index 00000000000..352a84291d7 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.h @@ -0,0 +1,72 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_URL_LOADER_THROTTLE_H_ +#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_URL_LOADER_THROTTLE_H_ + +#include "base/supports_user_data.h" +#include "content/public/browser/web_contents.h" +#include "net/http/http_request_headers.h" +#include "services/network/public/mojom/fetch_api.mojom-shared.h" +#include "third_party/blink/public/common/loader/url_loader_throttle.h" + +namespace signin { + +class HeaderModificationDelegate; + +// This class is used to modify the main frame request made when loading the +// GAIA signin realm. +class URLLoaderThrottle : public blink::URLLoaderThrottle, + public base::SupportsUserData { + public: + // Creates a new throttle if |delegate| says that this request should be + // intercepted. + static std::unique_ptr MaybeCreate( + std::unique_ptr delegate, + content::WebContents::Getter web_contents_getter); + + URLLoaderThrottle(const URLLoaderThrottle&) = delete; + URLLoaderThrottle& operator=(const URLLoaderThrottle&) = delete; + + ~URLLoaderThrottle() override; + + // blink::URLLoaderThrottle + void WillStartRequest(network::ResourceRequest* request, + bool* defer) override; + void WillRedirectRequest( + net::RedirectInfo* redirect_info, + const network::mojom::URLResponseHead& response_head, + bool* defer, + std::vector* headers_to_remove, + net::HttpRequestHeaders* modified_headers, + net::HttpRequestHeaders* modified_cors_exempt_headers) override; + void WillProcessResponse(const GURL& response_url, + network::mojom::URLResponseHead* response_head, + bool* defer) override; + + private: + class ThrottleRequestAdapter; + class ThrottleResponseAdapter; + + URLLoaderThrottle(std::unique_ptr delegate, + content::WebContents::Getter web_contents_getter); + + const std::unique_ptr delegate_; + const content::WebContents::Getter web_contents_getter_; + + // Information about the current request. + GURL request_url_; + GURL request_referrer_; + net::HttpRequestHeaders request_headers_; + net::HttpRequestHeaders request_cors_exempt_headers_; + network::mojom::RequestDestination request_destination_ = + network::mojom::RequestDestination::kEmpty; + bool request_is_fetch_like_api_ = false; + + base::OnceClosure destruction_callback_; +}; + +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_URL_LOADER_THROTTLE_H_ diff --git a/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle_unittest.cc b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle_unittest.cc new file mode 100644 index 00000000000..f28486ee18a --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle_unittest.cc @@ -0,0 +1,281 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/chrome_signin_url_loader_throttle.h" + +#include "base/bind.h" +#include "base/memory/ptr_util.h" +#include "base/test/mock_callback.h" +#include "chrome/browser/signin/chrome_signin_helper.h" +#include "chrome/browser/signin/header_modification_delegate.h" +#include "services/network/public/cpp/resource_request.h" +#include "services/network/public/mojom/url_response_head.mojom.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::ElementsAre; +using testing::Invoke; +using testing::Return; +using testing::_; + +namespace signin { + +namespace { + +class MockDelegate : public HeaderModificationDelegate { + public: + MockDelegate() = default; + + MockDelegate(const MockDelegate&) = delete; + MockDelegate& operator=(const MockDelegate&) = delete; + + ~MockDelegate() override = default; + + MOCK_METHOD1(ShouldInterceptNavigation, bool(content::WebContents* contents)); + MOCK_METHOD2(ProcessRequest, + void(ChromeRequestAdapter* request_adapter, + const GURL& redirect_url)); + MOCK_METHOD2(ProcessResponse, + void(ResponseAdapter* response_adapter, + const GURL& redirect_url)); +}; + +content::WebContents::Getter NullWebContentsGetter() { + return base::BindRepeating([]() -> content::WebContents* { return nullptr; }); +} + +} // namespace + +TEST(ChromeSigninURLLoaderThrottleTest, NoIntercept) { + auto* delegate = new MockDelegate(); + + EXPECT_CALL(*delegate, ShouldInterceptNavigation(_)).WillOnce(Return(false)); + EXPECT_FALSE(URLLoaderThrottle::MaybeCreate(base::WrapUnique(delegate), + NullWebContentsGetter())); +} + +TEST(ChromeSigninURLLoaderThrottleTest, Intercept) { + auto* delegate = new MockDelegate(); + EXPECT_CALL(*delegate, ShouldInterceptNavigation(_)).WillOnce(Return(true)); + auto throttle = URLLoaderThrottle::MaybeCreate(base::WrapUnique(delegate), + NullWebContentsGetter()); + ASSERT_TRUE(throttle); + + // Phase 1: Start the request. + + const GURL kTestURL("https://google.com/index.html"); + const GURL kTestReferrer("https://chrome.com/referrer.html"); + base::MockCallback destruction_callback; + EXPECT_CALL(*delegate, ProcessRequest(_, _)) + .WillOnce( + Invoke([&](ChromeRequestAdapter* adapter, const GURL& redirect_url) { + EXPECT_EQ(kTestURL, adapter->GetUrl()); + EXPECT_EQ(network::mojom::RequestDestination::kDocument, + adapter->GetRequestDestination()); + EXPECT_EQ(GURL("https://chrome.com"), adapter->GetReferrerOrigin()); + + EXPECT_TRUE(adapter->HasHeader("X-Request-1")); + adapter->RemoveRequestHeaderByName("X-Request-1"); + EXPECT_FALSE(adapter->HasHeader("X-Request-1")); + + adapter->SetExtraHeaderByName("X-Request-2", "Bar"); + EXPECT_TRUE(adapter->HasHeader("X-Request-2")); + + EXPECT_EQ(GURL(), redirect_url); + + adapter->SetDestructionCallback(destruction_callback.Get()); + })); + + network::ResourceRequest request; + request.url = kTestURL; + request.referrer = kTestReferrer; + request.destination = network::mojom::RequestDestination::kDocument; + request.headers.SetHeader("X-Request-1", "Foo"); + bool defer = false; + throttle->WillStartRequest(&request, &defer); + + EXPECT_FALSE(request.headers.HasHeader("X-Request-1")); + std::string value; + EXPECT_TRUE(request.headers.GetHeader("X-Request-2", &value)); + EXPECT_EQ("Bar", value); + + EXPECT_FALSE(defer); + + testing::Mock::VerifyAndClearExpectations(delegate); + + // Phase 2: Redirect the request. + + const GURL kTestRedirectURL("https://youtube.com/index.html"); + const void* const kResponseUserDataKey = &kResponseUserDataKey; + std::unique_ptr response_user_data = + std::make_unique(); + base::SupportsUserData::Data* response_user_data_ptr = + response_user_data.get(); + + EXPECT_CALL(*delegate, ProcessResponse(_, _)) + .WillOnce(Invoke([&](ResponseAdapter* adapter, const GURL& redirect_url) { + EXPECT_EQ(GURL("https://google.com"), adapter->GetOrigin()); + EXPECT_TRUE(adapter->IsMainFrame()); + + adapter->SetUserData(kResponseUserDataKey, + std::move(response_user_data)); + EXPECT_EQ(response_user_data_ptr, + adapter->GetUserData(kResponseUserDataKey)); + + const net::HttpResponseHeaders* headers = adapter->GetHeaders(); + EXPECT_TRUE(headers->HasHeader("X-Response-1")); + EXPECT_TRUE(headers->HasHeader("X-Response-2")); + adapter->RemoveHeader("X-Response-2"); + + EXPECT_EQ(kTestRedirectURL, redirect_url); + })); + + base::MockCallback ignored_destruction_callback; + EXPECT_CALL(*delegate, ProcessRequest(_, _)) + .WillOnce( + Invoke([&](ChromeRequestAdapter* adapter, const GURL& redirect_url) { + EXPECT_EQ(network::mojom::RequestDestination::kDocument, + adapter->GetRequestDestination()); + + // Changes to the URL and referrer take effect after the redirect + // is followed. + EXPECT_EQ(kTestURL, adapter->GetUrl()); + EXPECT_EQ(GURL("https://chrome.com"), adapter->GetReferrerOrigin()); + + // X-Request-1 and X-Request-2 were modified in the previous call to + // ProcessRequest(). These changes should still be present. + EXPECT_FALSE(adapter->HasHeader("X-Request-1")); + EXPECT_TRUE(adapter->HasHeader("X-Request-2")); + + adapter->RemoveRequestHeaderByName("X-Request-2"); + EXPECT_FALSE(adapter->HasHeader("X-Request-2")); + + adapter->SetExtraHeaderByName("X-Request-3", "Baz"); + EXPECT_TRUE(adapter->HasHeader("X-Request-3")); + + EXPECT_EQ(kTestRedirectURL, redirect_url); + + adapter->SetDestructionCallback(ignored_destruction_callback.Get()); + })); + + net::RedirectInfo redirect_info; + redirect_info.new_url = kTestRedirectURL; + // An HTTPS to HTTPS redirect such as this wouldn't normally change the + // referrer but we do for testing purposes. + redirect_info.new_referrer = kTestURL.spec(); + + auto response_head = network::mojom::URLResponseHead::New(); + response_head->headers = base::MakeRefCounted(""); + response_head->headers->SetHeader("X-Response-1", "Foo"); + response_head->headers->SetHeader("X-Response-2", "Bar"); + + std::vector request_headers_to_remove; + net::HttpRequestHeaders modified_request_headers; + net::HttpRequestHeaders modified_cors_exempt_request_headers; + throttle->WillRedirectRequest( + &redirect_info, *response_head, &defer, &request_headers_to_remove, + &modified_request_headers, &modified_cors_exempt_request_headers); + + EXPECT_FALSE(defer); + + EXPECT_TRUE(response_head->headers->HasHeader("X-Response-1")); + EXPECT_FALSE(response_head->headers->HasHeader("X-Response-2")); + + EXPECT_THAT(request_headers_to_remove, ElementsAre("X-Request-2")); + EXPECT_TRUE(modified_request_headers.GetHeader("X-Request-3", &value)); + EXPECT_EQ("Baz", value); + + EXPECT_TRUE(modified_cors_exempt_request_headers.IsEmpty()); + + testing::Mock::VerifyAndClearExpectations(delegate); + + // Phase 3: Complete the request. + + EXPECT_CALL(*delegate, ProcessResponse(_, _)) + .WillOnce(Invoke([&](ResponseAdapter* adapter, const GURL& redirect_url) { + EXPECT_EQ(GURL("https://youtube.com"), adapter->GetOrigin()); + EXPECT_TRUE(adapter->IsMainFrame()); + + EXPECT_EQ(response_user_data_ptr, + adapter->GetUserData(kResponseUserDataKey)); + + const net::HttpResponseHeaders* headers = adapter->GetHeaders(); + // This is a new response and so previous headers should not carry over. + EXPECT_FALSE(headers->HasHeader("X-Response-1")); + EXPECT_FALSE(headers->HasHeader("X-Response-2")); + + EXPECT_TRUE(headers->HasHeader("X-Response-3")); + EXPECT_TRUE(headers->HasHeader("X-Response-4")); + adapter->RemoveHeader("X-Response-3"); + + EXPECT_EQ(GURL(), redirect_url); + })); + + response_head = network::mojom::URLResponseHead::New(); + response_head->headers = base::MakeRefCounted(""); + response_head->headers->SetHeader("X-Response-3", "Foo"); + response_head->headers->SetHeader("X-Response-4", "Bar"); + + throttle->WillProcessResponse(kTestRedirectURL, response_head.get(), &defer); + + EXPECT_FALSE(response_head->headers->HasHeader("X-Response-3")); + EXPECT_TRUE(response_head->headers->HasHeader("X-Response-4")); + + EXPECT_FALSE(defer); + + EXPECT_CALL(destruction_callback, Run()).Times(1); + EXPECT_CALL(ignored_destruction_callback, Run()).Times(0); + throttle.reset(); +} + +TEST(ChromeSigninURLLoaderThrottleTest, InterceptSubFrame) { + auto* delegate = new MockDelegate(); + EXPECT_CALL(*delegate, ShouldInterceptNavigation(_)).WillOnce(Return(true)); + auto throttle = URLLoaderThrottle::MaybeCreate(base::WrapUnique(delegate), + NullWebContentsGetter()); + ASSERT_TRUE(throttle); + + EXPECT_CALL(*delegate, ProcessRequest(_, _)) + .Times(2) + .WillRepeatedly( + [](ChromeRequestAdapter* adapter, const GURL& redirect_url) { + EXPECT_EQ(network::mojom::RequestDestination::kIframe, + adapter->GetRequestDestination()); + }); + + network::ResourceRequest request; + request.url = GURL("https://google.com"); + request.destination = network::mojom::RequestDestination::kIframe; + + bool defer = false; + throttle->WillStartRequest(&request, &defer); + EXPECT_FALSE(defer); + + EXPECT_CALL(*delegate, ProcessResponse(_, _)) + .Times(2) + .WillRepeatedly(([](ResponseAdapter* adapter, const GURL& redirect_url) { + EXPECT_FALSE(adapter->IsMainFrame()); + })); + + net::RedirectInfo redirect_info; + redirect_info.new_url = GURL("https://youtube.com"); + auto response_head = network::mojom::URLResponseHead::New(); + + std::vector request_headers_to_remove; + net::HttpRequestHeaders modified_request_headers; + net::HttpRequestHeaders modified_cors_exempt_request_headers; + throttle->WillRedirectRequest( + &redirect_info, *response_head, &defer, &request_headers_to_remove, + &modified_request_headers, &modified_cors_exempt_request_headers); + EXPECT_FALSE(defer); + EXPECT_TRUE(request_headers_to_remove.empty()); + EXPECT_TRUE(modified_request_headers.IsEmpty()); + EXPECT_TRUE(modified_cors_exempt_request_headers.IsEmpty()); + + throttle->WillProcessResponse(GURL("https://youtube.com"), + response_head.get(), &defer); + EXPECT_FALSE(defer); +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/chromeos_mirror_account_consistency_browsertest.cc b/chromium/chrome/browser/signin/chromeos_mirror_account_consistency_browsertest.cc new file mode 100644 index 00000000000..b5b9b6a5379 --- /dev/null +++ b/chromium/chrome/browser/signin/chromeos_mirror_account_consistency_browsertest.cc @@ -0,0 +1,174 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/ash/login/login_manager_test.h" +#include "chrome/browser/ash/login/test/login_manager_mixin.h" +#include "chrome/browser/ash/profiles/profile_helper.h" +#include "chrome/browser/prefs/incognito_mode_prefs.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_key.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/supervised_user/supervised_user_constants.h" +#include "chrome/browser/supervised_user/supervised_user_settings_service.h" +#include "chrome/browser/supervised_user/supervised_user_settings_service_factory.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/common/pref_names.h" +#include "chrome/test/base/ui_test_utils.h" +#include "components/account_id/account_id.h" +#include "components/google/core/common/google_switches.h" +#include "components/network_session_configurator/common/network_switches.h" +#include "components/prefs/pref_service.h" +#include "components/signin/core/browser/signin_header_helper.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/identity_test_utils.h" +#include "components/user_manager/user.h" +#include "components/user_manager/user_manager.h" +#include "content/public/test/browser_test.h" +#include "content/public/test/browser_test_utils.h" +#include "net/test/embedded_test_server/default_handlers.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "net/traffic_annotation/network_traffic_annotation_test_helper.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace { + +constexpr char kGaiaDomain[] = "accounts.google.com"; + +// Checks whether the "X-Chrome-Connected" header of a new request to Google +// contains |expected_header_value|. +void TestMirrorRequestForProfile(net::EmbeddedTestServer* test_server, + Profile* profile, + const std::string& expected_header_value) { + GURL gaia_url(test_server->GetURL("/echoheader?X-Chrome-Connected")); + GURL::Replacements replace_host; + replace_host.SetHostStr(kGaiaDomain); + gaia_url = gaia_url.ReplaceComponents(replace_host); + + Browser* browser = Browser::Create(Browser::CreateParams(profile, true)); + ui_test_utils::NavigateToURLWithDisposition( + browser, gaia_url, WindowOpenDisposition::SINGLETON_TAB, + ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); + + std::string inner_text; + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + browser->tab_strip_model()->GetActiveWebContents(), + "domAutomationController.send(document.body.innerText);", &inner_text)); + // /echoheader returns "None" if the header isn't set. + inner_text = (inner_text == "None") ? "" : inner_text; + EXPECT_EQ(expected_header_value, inner_text); +} + +} // namespace + +// This is a Chrome OS-only test ensuring that mirror account consistency is +// enabled for child accounts, but not enabled for other account types. +class ChromeOsMirrorAccountConsistencyTest : public ash::LoginManagerTest { + public: + ChromeOsMirrorAccountConsistencyTest( + const ChromeOsMirrorAccountConsistencyTest&) = delete; + ChromeOsMirrorAccountConsistencyTest& operator=( + const ChromeOsMirrorAccountConsistencyTest&) = delete; + + protected: + ~ChromeOsMirrorAccountConsistencyTest() override {} + + ChromeOsMirrorAccountConsistencyTest() : LoginManagerTest() { + login_mixin_.AppendRegularUsers(1); + account_id_ = login_mixin_.users()[0].account_id; + } + + void SetUpCommandLine(base::CommandLine* command_line) override { + ash::LoginManagerTest::SetUpCommandLine(command_line); + + // HTTPS server only serves a valid cert for localhost, so this is needed to + // load pages from "www.google.com" without an interstitial. + command_line->AppendSwitch(switches::kIgnoreCertificateErrors); + + // The production code only allows known ports (80 for http and 443 for + // https), but the test server runs on a random port. + command_line->AppendSwitch(switches::kIgnoreGooglePortNumbers); + } + + void SetUpOnMainThread() override { + // We can't use BrowserTestBase's EmbeddedTestServer because google.com + // URL's have to be https. + test_server_ = std::make_unique( + net::EmbeddedTestServer::TYPE_HTTPS); + net::test_server::RegisterDefaultHandlers(test_server_.get()); + ASSERT_TRUE(test_server_->Start()); + + ash::LoginManagerTest::SetUpOnMainThread(); + } + + AccountId account_id_; + ash::LoginManagerMixin login_mixin_{&mixin_host_}; + + protected: + std::unique_ptr test_server_; +}; + +// Mirror is enabled for child accounts. +IN_PROC_BROWSER_TEST_F(ChromeOsMirrorAccountConsistencyTest, + TestMirrorRequestChromeOsChildAccount) { + // Child user. + LoginUser(account_id_); + + user_manager::User* user = user_manager::UserManager::Get()->GetActiveUser(); + ASSERT_EQ(user, user_manager::UserManager::Get()->GetPrimaryUser()); + ASSERT_EQ(user, user_manager::UserManager::Get()->FindUser(account_id_)); + Profile* profile = chromeos::ProfileHelper::Get()->GetProfileByUser(user); + + // Supervised flag uses `FindExtendedAccountInfoForAccountWithRefreshToken`, + // so wait for tokens to be loaded. + signin::WaitForRefreshTokensLoaded( + IdentityManagerFactory::GetForProfile(profile)); + + SupervisedUserSettingsService* supervised_user_settings_service = + SupervisedUserSettingsServiceFactory::GetForKey(profile->GetProfileKey()); + supervised_user_settings_service->SetActive(true); + + // Incognito is always disabled for child accounts. + PrefService* prefs = profile->GetPrefs(); + prefs->SetInteger( + prefs::kIncognitoModeAvailability, + static_cast(IncognitoModePrefs::Availability::kDisabled)); + ASSERT_EQ(1, signin::PROFILE_MODE_INCOGNITO_DISABLED); + + // TODO(http://crbug.com/1134144): This test seems to test supervised profiles + // instead of child accounts. With the current implementation, + // X-Chrome-Connected header gets a supervised=true argument only for child + // profiles. Verify if these tests needs to be updated to use child accounts + // or whether supervised profiles need to be supported as well. + TestMirrorRequestForProfile( + test_server_.get(), profile, + "source=Chrome,mode=1,enable_account_consistency=true,supervised=false," + "consistency_enabled_by_default=false"); +} + +// Mirror is enabled for non-child accounts. +IN_PROC_BROWSER_TEST_F(ChromeOsMirrorAccountConsistencyTest, + TestMirrorRequestChromeOsNotChildAccount) { + // Not a child user. + LoginUser(account_id_); + + user_manager::User* user = user_manager::UserManager::Get()->GetActiveUser(); + ASSERT_EQ(user, user_manager::UserManager::Get()->GetPrimaryUser()); + ASSERT_EQ(user, user_manager::UserManager::Get()->FindUser(account_id_)); + Profile* profile = chromeos::ProfileHelper::Get()->GetProfileByUser(user); + + // Supervised flag uses `FindExtendedAccountInfoForAccountWithRefreshToken`, + // so wait for tokens to be loaded. + signin::WaitForRefreshTokensLoaded( + IdentityManagerFactory::GetForProfile(profile)); + + // With Chrome OS Account Manager enabled, this should be true. + EXPECT_TRUE( + AccountConsistencyModeManager::IsMirrorEnabledForProfile(profile)); + TestMirrorRequestForProfile( + test_server_.get(), profile, + "source=Chrome,mode=0,enable_account_consistency=true,supervised=false," + "consistency_enabled_by_default=false"); +} diff --git a/chromium/chrome/browser/signin/cookie_reminter_factory.cc b/chromium/chrome/browser/signin/cookie_reminter_factory.cc new file mode 100644 index 00000000000..f53de57d249 --- /dev/null +++ b/chromium/chrome/browser/signin/cookie_reminter_factory.cc @@ -0,0 +1,37 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/cookie_reminter_factory.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/signin/core/browser/cookie_reminter.h" + +CookieReminterFactory::CookieReminterFactory() + : BrowserContextKeyedServiceFactory( + "CookieReminter", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(IdentityManagerFactory::GetInstance()); +} + +CookieReminterFactory::~CookieReminterFactory() {} + +// static +CookieReminter* CookieReminterFactory::GetForProfile(Profile* profile) { + return static_cast( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +// static +CookieReminterFactory* CookieReminterFactory::GetInstance() { + return base::Singleton::get(); +} + +KeyedService* CookieReminterFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile); + return new CookieReminter(identity_manager); +} diff --git a/chromium/chrome/browser/signin/cookie_reminter_factory.h b/chromium/chrome/browser/signin/cookie_reminter_factory.h new file mode 100644 index 00000000000..8ce4768bebd --- /dev/null +++ b/chromium/chrome/browser/signin/cookie_reminter_factory.h @@ -0,0 +1,30 @@ +// Copyright 2019 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 CHROME_BROWSER_SIGNIN_COOKIE_REMINTER_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_COOKIE_REMINTER_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +class CookieReminter; +class Profile; + +class CookieReminterFactory : public BrowserContextKeyedServiceFactory { + public: + static CookieReminter* GetForProfile(Profile* profile); + static CookieReminterFactory* GetInstance(); + + private: + friend struct base::DefaultSingletonTraits; + + CookieReminterFactory(); + ~CookieReminterFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_COOKIE_REMINTER_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/dice_browsertest.cc b/chromium/chrome/browser/signin/dice_browsertest.cc new file mode 100644 index 00000000000..150647b8b0b --- /dev/null +++ b/chromium/chrome/browser/signin/dice_browsertest.cc @@ -0,0 +1,1236 @@ +// Copyright 2017 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 +#include +#include +#include + +#include "base/auto_reset.h" +#include "base/base_switches.h" +#include "base/bind.h" +#include "base/callback.h" +#include "base/callback_helpers.h" +#include "base/check.h" +#include "base/command_line.h" +#include "base/location.h" +#include "base/run_loop.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/stringprintf.h" +#include "base/test/scoped_feature_list.h" +#include "base/test/test_mock_time_task_runner.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "chrome/browser/apps/platform_apps/shortcut_manager.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/extensions/api/identity/web_auth_flow.h" +#include "chrome/browser/policy/cloud/user_policy_signin_service.h" +#include "chrome/browser/policy/cloud/user_policy_signin_service_internal.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/signin/account_reconcilor_factory.h" +#include "chrome/browser/signin/chrome_device_id_helper.h" +#include "chrome/browser/signin/chrome_signin_client.h" +#include "chrome/browser/signin/chrome_signin_client_factory.h" +#include "chrome/browser/signin/chrome_signin_helper.h" +#include "chrome/browser/signin/dice_response_handler.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/signin_util.h" +#include "chrome/browser/sync/user_event_service_factory.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/profile_chooser_constants.h" +#include "chrome/browser/ui/simple_message_box_internal.h" +#include "chrome/browser/ui/webui/signin/login_ui_service.h" +#include "chrome/browser/ui/webui/signin/login_ui_service_factory.h" +#include "chrome/browser/ui/webui/signin/login_ui_test_utils.h" +#include "chrome/common/pref_names.h" +#include "chrome/common/url_constants.h" +#include "chrome/common/webui_url_constants.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/ui_test_utils.h" +#include "components/prefs/pref_service.h" +#include "components/search/ntp_features.h" +#include "components/signin/core/browser/account_reconcilor.h" +#include "components/signin/core/browser/dice_header_helper.h" +#include "components/signin/core/browser/signin_header_helper.h" +#include "components/signin/public/base/account_consistency_method.h" +#include "components/signin/public/base/signin_client.h" +#include "components/signin/public/base/signin_metrics.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_test_utils.h" +#include "components/signin/public/identity_manager/primary_account_mutator.h" +#include "components/sync/base/sync_prefs.h" +#include "components/sync_user_events/user_event_service.h" +#include "components/variations/variations_switches.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/load_notification_details.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/notification_types.h" +#include "content/public/common/content_switches.h" +#include "content/public/test/browser_test.h" +#include "google_apis/gaia/gaia_switches.h" +#include "google_apis/gaia/gaia_urls.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "net/test/embedded_test_server/http_request.h" +#include "net/test/embedded_test_server/http_response.h" +#include "net/test/embedded_test_server/request_handler_util.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +using net::test_server::BasicHttpResponse; +using net::test_server::HttpRequest; +using net::test_server::HttpResponse; +using signin::AccountConsistencyMethod; + +namespace { + +constexpr int kAccountReconcilorDelayMs = 10; + +enum SignoutType { + kSignoutTypeFirst = 0, + + kAllAccounts = 0, // Sign out from all accounts. + kMainAccount = 1, // Sign out from main account only. + kSecondaryAccount = 2, // Sign out from secondary account only. + + kSignoutTypeLast +}; + +const char kAuthorizationCode[] = "authorization_code"; +const char kDiceResponseHeader[] = "X-Chrome-ID-Consistency-Response"; +const char kChromeSyncEndpointURL[] = "/signin/chrome/sync"; +const char kEnableSyncURL[] = "/enable_sync"; +const char kGoogleSignoutResponseHeader[] = "Google-Accounts-SignOut"; +const char kMainGmailEmail[] = "main_email@gmail.com"; +const char kMainManagedEmail[] = "main_email@managed.com"; +const char kNoDiceRequestHeader[] = "NoDiceHeader"; +const char kOAuth2TokenExchangeURL[] = "/oauth2/v4/token"; +const char kOAuth2TokenRevokeURL[] = "/o/oauth2/revoke"; +const char kSecondaryEmail[] = "secondary_email@example.com"; +const char kSigninURL[] = "/signin"; +const char kSigninWithOutageInDiceURL[] = "/signin/outage"; +const char kSignoutURL[] = "/signout"; + +// Test response that does not complete synchronously. It must be unblocked by +// calling the completion closure. +class BlockedHttpResponse : public net::test_server::BasicHttpResponse { + public: + explicit BlockedHttpResponse( + base::OnceCallback callback) + : callback_(std::move(callback)) {} + + void SendResponse( + base::WeakPtr delegate) override { + // Called on the IO thread to unblock the response. + base::OnceClosure unblock_io_thread = + base::BindOnce(&BlockedHttpResponse::SendResponseInternal, + weak_factory_.GetWeakPtr(), delegate); + // Unblock the response from any thread by posting a task to the IO thread. + base::OnceClosure unblock_any_thread = + base::BindOnce(base::IgnoreResult(&base::TaskRunner::PostTask), + base::ThreadTaskRunnerHandle::Get(), FROM_HERE, + std::move(unblock_io_thread)); + // Pass |unblock_any_thread| to the caller on the UI thread. + content::GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, + base::BindOnce(std::move(callback_), std::move(unblock_any_thread))); + } + + private: + void SendResponseInternal( + base::WeakPtr delegate) { + if (delegate) + BasicHttpResponse::SendResponse(delegate); + } + base::OnceCallback callback_; + + base::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace + +namespace FakeGaia { + +// Handler for the signin page on the embedded test server. +// The response has the content of the Dice request header in its body, and has +// the Dice response header. +// Handles both the "Chrome Sync" endpoint and the old endpoint. +std::unique_ptr HandleSigninURL( + const std::string& main_email, + const base::RepeatingCallback& callback, + const HttpRequest& request) { + if (!net::test_server::ShouldHandle(request, kSigninURL) && + !net::test_server::ShouldHandle(request, kChromeSyncEndpointURL) && + !net::test_server::ShouldHandle(request, kSigninWithOutageInDiceURL)) + return nullptr; + + // Extract Dice request header. + std::string header_value = kNoDiceRequestHeader; + auto it = request.headers.find(signin::kDiceRequestHeader); + if (it != request.headers.end()) + header_value = it->second; + + content::GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, base::BindOnce(callback, header_value)); + + // Add the SIGNIN dice header. + std::unique_ptr http_response(new BasicHttpResponse); + if (header_value != kNoDiceRequestHeader) { + if (net::test_server::ShouldHandle(request, kSigninWithOutageInDiceURL)) { + http_response->AddCustomHeader( + kDiceResponseHeader, + base::StringPrintf("action=SIGNIN,authuser=1,id=%s,email=%s," + "no_authorization_code=true", + signin::GetTestGaiaIdForEmail(main_email).c_str(), + main_email.c_str())); + } else { + http_response->AddCustomHeader( + kDiceResponseHeader, + base::StringPrintf( + "action=SIGNIN,authuser=1,id=%s,email=%s,authorization_code=%s", + signin::GetTestGaiaIdForEmail(main_email).c_str(), + main_email.c_str(), kAuthorizationCode)); + } + } + + // When hitting the Chrome Sync endpoint, redirect to kEnableSyncURL, which + // adds the ENABLE_SYNC dice header. + if (net::test_server::ShouldHandle(request, kChromeSyncEndpointURL)) { + http_response->set_code(net::HTTP_FOUND); // 302 redirect. + http_response->AddCustomHeader("location", kEnableSyncURL); + } + + http_response->AddCustomHeader("Cache-Control", "no-store"); + return std::move(http_response); +} + +// Handler for the Gaia endpoint adding the ENABLE_SYNC dice header. +std::unique_ptr HandleEnableSyncURL( + const std::string& main_email, + const base::RepeatingCallback& callback, + const HttpRequest& request) { + if (!net::test_server::ShouldHandle(request, kEnableSyncURL)) + return nullptr; + + std::unique_ptr http_response = + std::make_unique(callback); + http_response->AddCustomHeader( + kDiceResponseHeader, + base::StringPrintf("action=ENABLE_SYNC,authuser=1,id=%s,email=%s", + signin::GetTestGaiaIdForEmail(main_email).c_str(), + main_email.c_str())); + http_response->AddCustomHeader("Cache-Control", "no-store"); + return std::move(http_response); +} + +// Handler for the signout page on the embedded test server. +// Responds with a Google-Accounts-SignOut header for the main account, the +// secondary account, or both (depending on the SignoutType, which is encoded in +// the query string). +std::unique_ptr HandleSignoutURL(const std::string& main_email, + const HttpRequest& request) { + if (!net::test_server::ShouldHandle(request, kSignoutURL)) + return nullptr; + + // Build signout header. + int query_value; + EXPECT_TRUE(base::StringToInt(request.GetURL().query(), &query_value)); + SignoutType signout_type = static_cast(query_value); + EXPECT_GE(signout_type, kSignoutTypeFirst); + EXPECT_LT(signout_type, kSignoutTypeLast); + std::string signout_header_value; + if (signout_type == kAllAccounts || signout_type == kMainAccount) { + std::string main_gaia_id = signin::GetTestGaiaIdForEmail(main_email); + signout_header_value = + base::StringPrintf("email=\"%s\", obfuscatedid=\"%s\", sessionindex=1", + main_email.c_str(), main_gaia_id.c_str()); + } + if (signout_type == kAllAccounts || signout_type == kSecondaryAccount) { + if (!signout_header_value.empty()) + signout_header_value += ", "; + std::string secondary_gaia_id = + signin::GetTestGaiaIdForEmail(kSecondaryEmail); + signout_header_value += + base::StringPrintf("email=\"%s\", obfuscatedid=\"%s\", sessionindex=2", + kSecondaryEmail, secondary_gaia_id.c_str()); + } + + std::unique_ptr http_response(new BasicHttpResponse); + http_response->AddCustomHeader(kGoogleSignoutResponseHeader, + signout_header_value); + http_response->AddCustomHeader("Cache-Control", "no-store"); + return std::move(http_response); +} + +// Handler for OAuth2 token exchange. +// Checks that the request is well formatted and returns a refresh token in a +// JSON dictionary. +std::unique_ptr HandleOAuth2TokenExchangeURL( + const base::RepeatingCallback& callback, + const HttpRequest& request) { + if (!net::test_server::ShouldHandle(request, kOAuth2TokenExchangeURL)) + return nullptr; + + // Check that the authorization code is somewhere in the request body. + if (!request.has_content) + return nullptr; + if (request.content.find(kAuthorizationCode) == std::string::npos) + return nullptr; + + std::unique_ptr http_response = + std::make_unique(callback); + + std::string content = + "{" + " \"access_token\":\"access_token\"," + " \"refresh_token\":\"new_refresh_token\"," + " \"expires_in\":9999" + "}"; + + http_response->set_content(content); + http_response->set_content_type("text/plain"); + http_response->AddCustomHeader("Cache-Control", "no-store"); + return std::move(http_response); +} + +// Handler for OAuth2 token revocation. +std::unique_ptr HandleOAuth2TokenRevokeURL( + const base::RepeatingClosure& callback, + const HttpRequest& request) { + if (!net::test_server::ShouldHandle(request, kOAuth2TokenRevokeURL)) + return nullptr; + + content::GetUIThreadTaskRunner({})->PostTask(FROM_HERE, callback); + + std::unique_ptr http_response(new BasicHttpResponse); + http_response->AddCustomHeader("Cache-Control", "no-store"); + return std::move(http_response); +} + +// Handler for ServiceLogin on the embedded test server. +// Calls the callback with the dice request header, or kNoDiceRequestHeader if +// there is no Dice header. +std::unique_ptr HandleChromeSigninEmbeddedURL( + const base::RepeatingCallback& callback, + const HttpRequest& request) { + if (!net::test_server::ShouldHandle(request, + "/embedded/setup/chrome/usermenu")) + return nullptr; + + std::string dice_request_header(kNoDiceRequestHeader); + auto it = request.headers.find(signin::kDiceRequestHeader); + if (it != request.headers.end()) + dice_request_header = it->second; + content::GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, base::BindOnce(callback, dice_request_header)); + + std::unique_ptr http_response(new BasicHttpResponse); + http_response->AddCustomHeader("Cache-Control", "no-store"); + return std::move(http_response); +} + +} // namespace FakeGaia + +class DiceBrowserTest : public InProcessBrowserTest, + public AccountReconcilor::Observer, + public signin::IdentityManager::Observer { + public: + DiceBrowserTest(const DiceBrowserTest&) = delete; + DiceBrowserTest& operator=(const DiceBrowserTest&) = delete; + + protected: + ~DiceBrowserTest() override {} + + explicit DiceBrowserTest(const std::string& main_email = kMainGmailEmail) + : main_email_(main_email), + https_server_(net::EmbeddedTestServer::TYPE_HTTPS), + enable_sync_requested_(false), + token_requested_(false), + refresh_token_available_(false), + token_revoked_notification_count_(0), + token_revoked_count_(0), + reconcilor_blocked_count_(0), + reconcilor_unblocked_count_(0), + reconcilor_started_count_(0) { + feature_list_.InitAndEnableFeature(kSupportOAuthOutageInDice); + https_server_.RegisterDefaultHandler(base::BindRepeating( + &FakeGaia::HandleSigninURL, main_email_, + base::BindRepeating(&DiceBrowserTest::OnSigninRequest, + base::Unretained(this)))); + https_server_.RegisterDefaultHandler(base::BindRepeating( + &FakeGaia::HandleEnableSyncURL, main_email_, + base::BindRepeating(&DiceBrowserTest::OnEnableSyncRequest, + base::Unretained(this)))); + https_server_.RegisterDefaultHandler( + base::BindRepeating(&FakeGaia::HandleSignoutURL, main_email_)); + https_server_.RegisterDefaultHandler(base::BindRepeating( + &FakeGaia::HandleOAuth2TokenExchangeURL, + base::BindRepeating(&DiceBrowserTest::OnTokenExchangeRequest, + base::Unretained(this)))); + https_server_.RegisterDefaultHandler(base::BindRepeating( + &FakeGaia::HandleOAuth2TokenRevokeURL, + base::BindRepeating(&DiceBrowserTest::OnTokenRevocationRequest, + base::Unretained(this)))); + https_server_.RegisterDefaultHandler(base::BindRepeating( + &FakeGaia::HandleChromeSigninEmbeddedURL, + base::BindRepeating(&DiceBrowserTest::OnChromeSigninEmbeddedRequest, + base::Unretained(this)))); + signin::SetDiceAccountReconcilorBlockDelayForTesting( + kAccountReconcilorDelayMs); + } + + // Navigates to the given path on the test server. + void NavigateToURL(const std::string& path) { + ASSERT_TRUE( + ui_test_utils::NavigateToURL(browser(), https_server_.GetURL(path))); + } + + // Returns the identity manager. + signin::IdentityManager* GetIdentityManager() { + return IdentityManagerFactory::GetForProfile(browser()->profile()); + } + + // Returns the account ID associated with |main_email_| and its associated + // gaia ID. + CoreAccountId GetMainAccountID() { + return GetIdentityManager()->PickAccountIdForAccount( + signin::GetTestGaiaIdForEmail(main_email_), main_email_); + } + + // Returns the account ID associated with kSecondaryEmail and its associated + // gaia ID. + CoreAccountId GetSecondaryAccountID() { + return GetIdentityManager()->PickAccountIdForAccount( + signin::GetTestGaiaIdForEmail(kSecondaryEmail), kSecondaryEmail); + } + + std::string GetDeviceId() { + return GetSigninScopedDeviceIdForProfile(browser()->profile()); + } + + // Signin with a main account and add token for a secondary account. + void SetupSignedInAccounts( + signin::ConsentLevel primary_account_consent_level) { + // Signin main account. + AccountInfo primary_account_info = signin::MakePrimaryAccountAvailable( + GetIdentityManager(), main_email_, primary_account_consent_level); + ASSERT_TRUE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + ASSERT_FALSE( + GetIdentityManager()->HasAccountWithRefreshTokenInPersistentErrorState( + GetMainAccountID())); + ASSERT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId( + primary_account_consent_level)); + + // Add a token for a secondary account. + AccountInfo secondary_account_info = + signin::MakeAccountAvailable(GetIdentityManager(), kSecondaryEmail); + ASSERT_TRUE(GetIdentityManager()->HasAccountWithRefreshToken( + secondary_account_info.account_id)); + ASSERT_FALSE( + GetIdentityManager()->HasAccountWithRefreshTokenInPersistentErrorState( + secondary_account_info.account_id)); + } + + // Navigate to a Gaia URL setting the Google-Accounts-SignOut header. + void SignOutWithDice(SignoutType signout_type) { + NavigateToURL(base::StringPrintf("%s?%i", kSignoutURL, signout_type)); + EXPECT_EQ(1, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(1); + + base::RunLoop().RunUntilIdle(); + } + + // InProcessBrowserTest: + void SetUp() override { + ASSERT_TRUE(https_server_.InitializeAndListen()); + InProcessBrowserTest::SetUp(); + } + + void SetUpCommandLine(base::CommandLine* command_line) override { + const GURL& base_url = https_server_.base_url(); + command_line->AppendSwitchASCII(switches::kGaiaUrl, base_url.spec()); + command_line->AppendSwitchASCII(switches::kGoogleApisUrl, base_url.spec()); + command_line->AppendSwitchASCII(switches::kLsoUrl, base_url.spec()); + } + + void SetUpOnMainThread() override { + InProcessBrowserTest::SetUpOnMainThread(); + https_server_.StartAcceptingConnections(); + + GetIdentityManager()->AddObserver(this); + // Wait for the token service to be ready. + if (!GetIdentityManager()->AreRefreshTokensLoaded()) { + WaitForClosure(&tokens_loaded_quit_closure_); + } + ASSERT_TRUE(GetIdentityManager()->AreRefreshTokensLoaded()); + + AccountReconcilor* reconcilor = + AccountReconcilorFactory::GetForProfile(browser()->profile()); + + // Reconcilor starts as soon as the token service finishes loading its + // credentials. Abort the reconcilor here to make sure tests start in a + // stable state. + reconcilor->AbortReconcile(); + reconcilor->SetState( + signin_metrics::AccountReconcilorState::ACCOUNT_RECONCILOR_OK); + reconcilor->AddObserver(this); + } + + void TearDownOnMainThread() override { + GetIdentityManager()->RemoveObserver(this); + AccountReconcilorFactory::GetForProfile(browser()->profile()) + ->RemoveObserver(this); + } + + // Calls |closure| if it is not null and resets it after. + void RunClosureIfValid(base::OnceClosure closure) { + if (closure) + std::move(closure).Run(); + } + + // Creates and runs a RunLoop until |closure| is called. + void WaitForClosure(base::OnceClosure* closure) { + base::RunLoop run_loop; + *closure = run_loop.QuitClosure(); + run_loop.Run(); + } + + // FakeGaia callbacks: + void OnSigninRequest(const std::string& dice_request_header) { + EXPECT_EQ(dice_request_header != kNoDiceRequestHeader, + IsReconcilorBlocked()); + dice_request_header_ = dice_request_header; + RunClosureIfValid(std::move(signin_requested_quit_closure_)); + } + + void OnChromeSigninEmbeddedRequest(const std::string& dice_request_header) { + dice_request_header_ = dice_request_header; + RunClosureIfValid(std::move(chrome_signin_embedded_quit_closure_)); + } + + void OnEnableSyncRequest(base::OnceClosure unblock_response_closure) { + EXPECT_TRUE(IsReconcilorBlocked()); + enable_sync_requested_ = true; + RunClosureIfValid(std::move(enable_sync_requested_quit_closure_)); + unblock_enable_sync_response_closure_ = std::move(unblock_response_closure); + } + + void OnTokenExchangeRequest(base::OnceClosure unblock_response_closure) { + // The token must be exchanged only once. + EXPECT_FALSE(token_requested_); + EXPECT_TRUE(IsReconcilorBlocked()); + token_requested_ = true; + RunClosureIfValid(std::move(token_requested_quit_closure_)); + unblock_token_exchange_response_closure_ = + std::move(unblock_response_closure); + } + + void OnTokenRevocationRequest() { + ++token_revoked_count_; + RunClosureIfValid(std::move(token_revoked_quit_closure_)); + } + + // AccountReconcilor::Observer: + void OnBlockReconcile() override { ++reconcilor_blocked_count_; } + void OnUnblockReconcile() override { + ++reconcilor_unblocked_count_; + RunClosureIfValid(std::move(unblock_count_quit_closure_)); + } + void OnStateChanged(signin_metrics::AccountReconcilorState state) override { + if (state == + signin_metrics::AccountReconcilorState::ACCOUNT_RECONCILOR_RUNNING) { + ++reconcilor_started_count_; + } + } + + // signin::IdentityManager::Observer + void OnPrimaryAccountChanged( + const signin::PrimaryAccountChangeEvent& event) override { + if (event.GetEventTypeFor(signin::ConsentLevel::kSync) == + signin::PrimaryAccountChangeEvent::Type::kSet) { + RunClosureIfValid(std::move(on_primary_account_set_quit_closure_)); + } + } + + void OnRefreshTokenUpdatedForAccount( + const CoreAccountInfo& account_info) override { + if (account_info.account_id == GetMainAccountID()) { + refresh_token_available_ = true; + RunClosureIfValid(std::move(refresh_token_available_quit_closure_)); + } + } + + void OnRefreshTokenRemovedForAccount( + const CoreAccountId& account_id) override { + ++token_revoked_notification_count_; + } + + void OnRefreshTokensLoaded() override { + RunClosureIfValid(std::move(tokens_loaded_quit_closure_)); + } + + // Returns true if the account reconcilor is currently blocked. + bool IsReconcilorBlocked() { + EXPECT_GE(reconcilor_blocked_count_, reconcilor_unblocked_count_); + EXPECT_LE(reconcilor_blocked_count_, reconcilor_unblocked_count_ + 1); + return (reconcilor_unblocked_count_ + 1) == reconcilor_blocked_count_; + } + + // Waits until |reconcilor_unblocked_count_| reaches |count|. + void WaitForReconcilorUnblockedCount(int count) { + if (reconcilor_unblocked_count_ == count) + return; + + ASSERT_EQ(count - 1, reconcilor_unblocked_count_); + // Wait for the timeout after the request is complete. + WaitForClosure(&unblock_count_quit_closure_); + EXPECT_EQ(count, reconcilor_unblocked_count_); + } + + // Waits until the user consented for sync. + void WaitForSigninSucceeded() { + if (GetIdentityManager() + ->GetPrimaryAccountId(signin::ConsentLevel::kSync) + .empty()) { + WaitForClosure(&on_primary_account_set_quit_closure_); + } + } + + // Waits for the ENABLE_SYNC request to hit the server, and unblocks the + // response. If this is not called, ENABLE_SYNC will not be sent by the + // server. + // Note: this does not wait for the response to reach Chrome. + void SendEnableSyncResponse() { + if (!enable_sync_requested_) + WaitForClosure(&enable_sync_requested_quit_closure_); + DCHECK(unblock_enable_sync_response_closure_); + std::move(unblock_enable_sync_response_closure_).Run(); + } + + // Waits until the token request is sent to the server, the response is + // received and the refresh token is available. If this is not called, the + // refresh token will not be sent by the server. + void SendRefreshTokenResponse() { + // Wait for the request hitting the server. + if (!token_requested_) + WaitForClosure(&token_requested_quit_closure_); + EXPECT_TRUE(token_requested_); + // Unblock the server response. + DCHECK(unblock_token_exchange_response_closure_); + std::move(unblock_token_exchange_response_closure_).Run(); + // Wait for the response coming back. + if (!refresh_token_available_) + WaitForClosure(&refresh_token_available_quit_closure_); + EXPECT_TRUE(refresh_token_available_); + } + + void WaitForTokenRevokedCount(int count) { + EXPECT_LE(token_revoked_count_, count); + while (token_revoked_count_ < count) + WaitForClosure(&token_revoked_quit_closure_); + EXPECT_EQ(count, token_revoked_count_); + } + + DiceResponseHandler* GetDiceResponseHandler() { + return DiceResponseHandler::GetForProfile(browser()->profile()); + } + + const std::string main_email_; + net::EmbeddedTestServer https_server_; + bool enable_sync_requested_; + bool token_requested_; + bool refresh_token_available_; + int token_revoked_notification_count_; + int token_revoked_count_; + int reconcilor_blocked_count_; + int reconcilor_unblocked_count_; + int reconcilor_started_count_; + std::string dice_request_header_; + base::test::ScopedFeatureList feature_list_; + + // Unblocks the server responses. + base::OnceClosure unblock_token_exchange_response_closure_; + base::OnceClosure unblock_enable_sync_response_closure_; + + // Used for waiting asynchronous events. + base::OnceClosure enable_sync_requested_quit_closure_; + base::OnceClosure token_requested_quit_closure_; + base::OnceClosure token_revoked_quit_closure_; + base::OnceClosure refresh_token_available_quit_closure_; + base::OnceClosure chrome_signin_embedded_quit_closure_; + base::OnceClosure unblock_count_quit_closure_; + base::OnceClosure tokens_loaded_quit_closure_; + base::OnceClosure on_primary_account_set_quit_closure_; + base::OnceClosure signin_requested_quit_closure_; +}; + +// Checks that signin on Gaia triggers the fetch for a refresh token. +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, Signin) { + EXPECT_EQ(0, reconcilor_started_count_); + + // Navigate to Gaia and sign in. + NavigateToURL(kSigninURL); + + // Check that the Dice request header was sent. + std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id(); + EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s," + "signin_mode=all_accounts," + "signout_mode=show_confirmation", + signin::kDiceProtocolVersion, client_id.c_str(), + GetDeviceId().c_str()), + dice_request_header_); + + // Check that the token was requested and added to the token service. + SendRefreshTokenResponse(); + EXPECT_TRUE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + // Sync should not be enabled. + EXPECT_TRUE(GetIdentityManager() + ->GetPrimaryAccountId(signin::ConsentLevel::kSync) + .empty()); + + EXPECT_EQ(1, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(1); + EXPECT_EQ(1, reconcilor_started_count_); +} + +// Checks that the account reconcilor is blocked when where was OAuth +// outage in Dice, and unblocked after the timeout. +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, SupportOAuthOutageInDice) { + DiceResponseHandler* dice_response_handler = GetDiceResponseHandler(); + scoped_refptr task_runner = + new base::TestMockTimeTaskRunner(); + dice_response_handler->SetTaskRunner(task_runner); + NavigateToURL(kSigninWithOutageInDiceURL); + // Check that the Dice request header was sent. + std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id(); + EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s," + "signin_mode=all_accounts," + "signout_mode=show_confirmation", + signin::kDiceProtocolVersion, client_id.c_str(), + GetDeviceId().c_str()), + dice_request_header_); + // Check that the reconcilor was blocked and not unblocked before timeout. + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(0, reconcilor_unblocked_count_); + task_runner->FastForwardBy( + base::Hours(kLockAccountReconcilorTimeoutHours / 2)); + EXPECT_EQ(0, reconcilor_unblocked_count_); + task_runner->FastForwardBy( + base::Hours((kLockAccountReconcilorTimeoutHours + 1) / 2)); + // Wait until reconcilor is unblocked. + WaitForReconcilorUnblockedCount(1); +} + +// Checks that re-auth on Gaia triggers the fetch for a refresh token. +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, Reauth) { + EXPECT_EQ(0, reconcilor_started_count_); + + // Start from a signed-in state. + SetupSignedInAccounts(signin::ConsentLevel::kSync); + EXPECT_EQ(1, reconcilor_started_count_); + + // Navigate to Gaia and sign in again with the main account. + NavigateToURL(kSigninURL); + + // Check that the Dice request header was sent. + std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id(); + EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s," + "signin_mode=all_accounts," + "signout_mode=show_confirmation", + signin::kDiceProtocolVersion, client_id.c_str(), + GetDeviceId().c_str()), + dice_request_header_); + + // Check that the token was requested and added to the token service. + SendRefreshTokenResponse(); + EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId( + signin::ConsentLevel::kSync)); + + // Old token must not be revoked (see http://crbug.com/865189). + EXPECT_EQ(0, token_revoked_notification_count_); + + EXPECT_EQ(1, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(1); + EXPECT_EQ(2, reconcilor_started_count_); +} + +// Checks that the Dice signout flow works and deletes all tokens. +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, SignoutMainAccount) { + // Start from a signed-in state. + SetupSignedInAccounts(signin::ConsentLevel::kSync); + + // Signout from main account. + SignOutWithDice(kMainAccount); + + // Check that the user is in error state. + EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId( + signin::ConsentLevel::kSync)); + EXPECT_TRUE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + EXPECT_TRUE( + GetIdentityManager()->HasAccountWithRefreshTokenInPersistentErrorState( + GetMainAccountID())); + EXPECT_TRUE(GetIdentityManager()->HasAccountWithRefreshToken( + GetSecondaryAccountID())); + + // Token for main account is revoked on server but not notified in the client. + EXPECT_EQ(0, token_revoked_notification_count_); + WaitForTokenRevokedCount(1); + + EXPECT_EQ(1, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(1); +} + +// Checks that signing out from a secondary account does not delete the main +// token. +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, SignoutSecondaryAccount) { + // Start from a signed-in state. + SetupSignedInAccounts(signin::ConsentLevel::kSync); + + // Signout from secondary account. + SignOutWithDice(kSecondaryAccount); + + // Check that the user is still signed in from main account, but secondary + // token is deleted. + EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId( + signin::ConsentLevel::kSync)); + EXPECT_TRUE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + EXPECT_FALSE(GetIdentityManager()->HasAccountWithRefreshToken( + GetSecondaryAccountID())); + EXPECT_EQ(1, token_revoked_notification_count_); + WaitForTokenRevokedCount(1); + EXPECT_EQ(1, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(1); +} + +// Checks that the Dice signout flow works and deletes all tokens. +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, SignoutAllAccounts) { + // Start from a signed-in state. + SetupSignedInAccounts(signin::ConsentLevel::kSync); + + // Signout from all accounts. + SignOutWithDice(kAllAccounts); + + // Check that the user is in error state. + EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId( + signin::ConsentLevel::kSync)); + EXPECT_TRUE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + EXPECT_TRUE( + GetIdentityManager()->HasAccountWithRefreshTokenInPersistentErrorState( + GetMainAccountID())); + EXPECT_FALSE(GetIdentityManager()->HasAccountWithRefreshToken( + GetSecondaryAccountID())); + + // Token for main account is revoked on server but not notified in the client. + EXPECT_EQ(1, token_revoked_notification_count_); + WaitForTokenRevokedCount(2); + + EXPECT_EQ(1, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(1); +} + +// Checks that Dice request header is not set from request from WebUI. +// See https://crbug.com/428396 +#if defined(OS_WIN) +#define MAYBE_NoDiceFromWebUI DISABLED_NoDiceFromWebUI +#else +#define MAYBE_NoDiceFromWebUI NoDiceFromWebUI +#endif +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, MAYBE_NoDiceFromWebUI) { + // Navigate to Gaia and from the native tab, which uses an extension. + ASSERT_TRUE(ui_test_utils::NavigateToURL( + browser(), GURL("chrome:chrome-signin?reason=5"))); + + // Check that the request had no Dice request header. + if (dice_request_header_.empty()) + WaitForClosure(&chrome_signin_embedded_quit_closure_); + EXPECT_EQ(kNoDiceRequestHeader, dice_request_header_); + EXPECT_EQ(0, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(0); +} + +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, + NoDiceExtensionConsent_LaunchWebAuthFlow) { + auto web_auth_flow = std::make_unique( + nullptr, browser()->profile(), https_server_.GetURL(kSigninURL), + extensions::WebAuthFlow::INTERACTIVE, + extensions::WebAuthFlow::LAUNCH_WEB_AUTH_FLOW); + web_auth_flow->Start(); + + if (dice_request_header_.empty()) + WaitForClosure(&signin_requested_quit_closure_); + + EXPECT_EQ(kNoDiceRequestHeader, dice_request_header_); + EXPECT_EQ(0, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(0); + + // Delete the web auth flow (uses DeleteSoon). + web_auth_flow.release()->DetachDelegateAndDelete(); + base::RunLoop().RunUntilIdle(); +} + +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, DiceExtensionConsent_GetAuthToken) { + // Signin from extension consent flow. + class DummyDelegate : public extensions::WebAuthFlow::Delegate { + public: + void OnAuthFlowFailure(extensions::WebAuthFlow::Failure failure) override {} + ~DummyDelegate() override = default; + }; + + DummyDelegate delegate; + auto web_auth_flow = std::make_unique( + &delegate, browser()->profile(), https_server_.GetURL(kSigninURL), + extensions::WebAuthFlow::INTERACTIVE, + extensions::WebAuthFlow::GET_AUTH_TOKEN); + web_auth_flow->Start(); + + // Check that the token was requested and added to the token service. + SendRefreshTokenResponse(); + EXPECT_TRUE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + + // Check that the Dice request header was sent. + std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id(); + EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s," + "signin_mode=all_accounts," + "signout_mode=show_confirmation", + signin::kDiceProtocolVersion, client_id.c_str(), + GetDeviceId().c_str()), + dice_request_header_); + + // Sync should not be enabled. + EXPECT_TRUE(GetIdentityManager() + ->GetPrimaryAccountId(signin::ConsentLevel::kSync) + .empty()); + + EXPECT_EQ(1, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(1); + EXPECT_EQ(1, reconcilor_started_count_); + + // Delete the web auth flow (uses DeleteSoon). + web_auth_flow.release()->DetachDelegateAndDelete(); + base::RunLoop().RunUntilIdle(); +} + +// Tests that Sync is enabled if the ENABLE_SYNC response is received after the +// refresh token. +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, EnableSyncAfterToken) { + EXPECT_EQ(0, reconcilor_started_count_); + + // Signin using the Chrome Sync endpoint. + browser()->signin_view_controller()->ShowSignin( + profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN, + signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS); + + // Receive token. + EXPECT_FALSE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + SendRefreshTokenResponse(); + EXPECT_TRUE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + + // Receive ENABLE_SYNC. + SendEnableSyncResponse(); + + // Check that the Dice request header was sent, with signout confirmation. + std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id(); + EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s," + "signin_mode=all_accounts," + "signout_mode=show_confirmation", + signin::kDiceProtocolVersion, client_id.c_str(), + GetDeviceId().c_str()), + dice_request_header_); + + content::WindowedNotificationObserver ntp_url_observer( + content::NOTIFICATION_LOAD_STOP, + base::BindRepeating([](const content::NotificationSource&, + const content::NotificationDetails& details) { + auto url = + content::Details(details)->url; + // Some test flags (e.g. ForceWebRequestProxyForTest) can change whether + // the reported NTP URL is chrome://newtab or chrome://new-tab-page. + return url == GURL(chrome::kChromeUINewTabPageURL) || + url == GURL(chrome::kChromeUINewTabURL); + })); + + WaitForSigninSucceeded(); + EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId( + signin::ConsentLevel::kSync)); + + EXPECT_EQ(1, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(1); + EXPECT_EQ(1, reconcilor_started_count_); + + // Check that the tab was navigated to the NTP. + ntp_url_observer.Wait(); + + // Dismiss the Sync confirmation UI. + EXPECT_TRUE(login_ui_test_utils::ConfirmSyncConfirmationDialog(browser())); +} + +// Tests that Sync is enabled if the ENABLE_SYNC response is received before the +// refresh token. + +// https://crbug.com/1082858 +#if (defined(OS_LINUX) || defined(OS_CHROMEOS)) && !defined(NDEBUG) +#define MAYBE_EnableSyncBeforeToken DISABLED_EnableSyncBeforeToken +#else +#define MAYBE_EnableSyncBeforeToken EnableSyncBeforeToken +#endif +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, MAYBE_EnableSyncBeforeToken) { + EXPECT_EQ(0, reconcilor_started_count_); + + ui_test_utils::UrlLoadObserver enable_sync_url_observer( + https_server_.GetURL(kEnableSyncURL), + content::NotificationService::AllSources()); + + // Signin using the Chrome Sync endpoint. + browser()->signin_view_controller()->ShowSignin( + profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN, + signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS); + + // Receive ENABLE_SYNC. + SendEnableSyncResponse(); + // Wait for the page to be fully loaded. + enable_sync_url_observer.Wait(); + + // Receive token. + EXPECT_FALSE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + SendRefreshTokenResponse(); + EXPECT_TRUE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + + // Check that the Dice request header was sent, with signout confirmation. + std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id(); + EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s," + "signin_mode=all_accounts," + "signout_mode=show_confirmation", + signin::kDiceProtocolVersion, client_id.c_str(), + GetDeviceId().c_str()), + dice_request_header_); + + ui_test_utils::UrlLoadObserver ntp_url_observer( + GURL(chrome::kChromeUINewTabURL), + content::NotificationService::AllSources()); + + WaitForSigninSucceeded(); + EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId( + signin::ConsentLevel::kSync)); + + EXPECT_EQ(1, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(1); + EXPECT_EQ(1, reconcilor_started_count_); + + // Check that the tab was navigated to the NTP. + ntp_url_observer.Wait(); + + // Dismiss the Sync confirmation UI. + EXPECT_TRUE(login_ui_test_utils::ConfirmSyncConfirmationDialog(browser())); +} + +// Tests that turning off Dice via preferences works when singed out. +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, PRE_TurnOffDice_SignedOut) { + ASSERT_FALSE( + GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSignin)); + ASSERT_TRUE(AccountConsistencyModeManager::IsDiceEnabledForProfile( + browser()->profile())); + + // Turn off Dice for this profile. + browser()->profile()->GetPrefs()->SetBoolean( + prefs::kSigninAllowedOnNextStartup, false); +} + +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, TurnOffDice_SignedOut) { + // Check that Dice is disabled. + EXPECT_FALSE( + browser()->profile()->GetPrefs()->GetBoolean(prefs::kSigninAllowed)); + EXPECT_FALSE(browser()->profile()->GetPrefs()->GetBoolean( + prefs::kSigninAllowedOnNextStartup)); + EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile( + browser()->profile())); + + EXPECT_FALSE( + GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSignin)); + + // Navigate to Gaia and sign in. + NavigateToURL(kSigninURL); + // Check that the Dice request header was not sent. + EXPECT_EQ(kNoDiceRequestHeader, dice_request_header_); + EXPECT_EQ(0, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(0); +} + +// Tests that turning off Dice via preferences works when signed in without sync +// consent. +// +// Regression test for crbug/1254325 +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, PRE_TurnOffDice_NotOptedIntoSync) { + SetupSignedInAccounts(signin::ConsentLevel::kSignin); + + ASSERT_TRUE( + GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSignin)); + ASSERT_FALSE( + GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); + ASSERT_TRUE(AccountConsistencyModeManager::IsDiceEnabledForProfile( + browser()->profile())); + + // Turn off Dice for this profile. + browser()->profile()->GetPrefs()->SetBoolean( + prefs::kSigninAllowedOnNextStartup, false); +} + +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, TurnOffDice_NotOptedIntoSync) { + // Check that Dice is disabled. + EXPECT_FALSE( + browser()->profile()->GetPrefs()->GetBoolean(prefs::kSigninAllowed)); + EXPECT_FALSE(browser()->profile()->GetPrefs()->GetBoolean( + prefs::kSigninAllowedOnNextStartup)); + EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile( + browser()->profile())); + + EXPECT_FALSE( + GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSignin)); + EXPECT_FALSE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + EXPECT_TRUE(GetIdentityManager()->GetAccountsWithRefreshTokens().empty()); + + // Navigate to Gaia and sign in. + NavigateToURL(kSigninURL); + // Check that the Dice request header was not sent. + EXPECT_EQ(kNoDiceRequestHeader, dice_request_header_); + EXPECT_EQ(0, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(0); +} + +// Tests that turning off Dice via preferences works when signed in with sync +// consent +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, PRE_TurnOffDice_OptedIntoSync) { + // Sign the profile in and turn sync on. + SetupSignedInAccounts(signin::ConsentLevel::kSync); + syncer::SyncPrefs(browser()->profile()->GetPrefs()).SetFirstSetupComplete(); + + ASSERT_TRUE( + GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); + ASSERT_TRUE(AccountConsistencyModeManager::IsDiceEnabledForProfile( + browser()->profile())); + + // Turn off Dice for this profile. + browser()->profile()->GetPrefs()->SetBoolean( + prefs::kSigninAllowedOnNextStartup, false); +} + +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, TurnOffDice_OptedIntoSync) { + EXPECT_FALSE( + browser()->profile()->GetPrefs()->GetBoolean(prefs::kSigninAllowed)); + EXPECT_FALSE(browser()->profile()->GetPrefs()->GetBoolean( + prefs::kSigninAllowedOnNextStartup)); + EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile( + browser()->profile())); + + EXPECT_FALSE( + GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); + EXPECT_FALSE( + GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSignin)); + EXPECT_FALSE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + EXPECT_TRUE(GetIdentityManager()->GetAccountsWithRefreshTokens().empty()); + + // Navigate to Gaia and sign in. + NavigateToURL(kSigninURL); + // Check that the Dice request header was not sent. + EXPECT_EQ(kNoDiceRequestHeader, dice_request_header_); + EXPECT_EQ(0, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(0); +} + +// Checks that Dice is disabled in incognito mode. +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, Incognito) { + Browser* incognito_browser = Browser::Create(Browser::CreateParams( + browser()->profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true), + true)); + + // Check that Dice is disabled. + EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile( + incognito_browser->profile())); +} + +// This test is not specifically related to DICE, but it extends +// |DiceBrowserTest| for convenience. +class DiceManageAccountBrowserTest : public DiceBrowserTest { + public: + DiceManageAccountBrowserTest() + : DiceBrowserTest(kMainManagedEmail), + // Skip showing the error message box to avoid freezing the main thread. + skip_message_box_auto_reset_( + &chrome::internal::g_should_skip_message_box_for_test, + true), + // Force the policy component to prohibit clearing the primary account + // even when the policy core component is not initialized. + prohibit_sigout_auto_reset_( + &policy::internal::g_force_prohibit_signout_for_tests, + true) {} + + void SetUp() override { +#if defined(OS_WIN) + // Shortcut deletion delays tests shutdown on Win-7 and results in time out. + // See crbug.com/1073451. + AppShortcutManager::SuppressShortcutsForTesting(); +#endif + DiceBrowserTest::SetUp(); + } + + protected: + base::AutoReset skip_message_box_auto_reset_; + base::AutoReset prohibit_sigout_auto_reset_; + unsigned int number_of_profiles_added_ = 0; +}; + +// Tests that prohiting sign-in on startup for a managed profile clears the +// profile directory on next start-up. +IN_PROC_BROWSER_TEST_F(DiceManageAccountBrowserTest, + PRE_ClearManagedProfileOnStartup) { + // Ensure that there are not deleted profiles before running this test. + PrefService* local_state = g_browser_process->local_state(); + DCHECK(local_state); + const base::ListValue* deleted_profiles = + local_state->GetList(prefs::kProfilesDeleted); + ASSERT_TRUE(deleted_profiles); + ASSERT_TRUE(deleted_profiles->GetList().empty()); + + // Sign the profile in. + SetupSignedInAccounts(signin::ConsentLevel::kSync); + + // Prohibit sign-in on next start-up. + browser()->profile()->GetPrefs()->SetBoolean( + prefs::kSigninAllowedOnNextStartup, false); +} + +IN_PROC_BROWSER_TEST_F(DiceManageAccountBrowserTest, + ClearManagedProfileOnStartup) { + // Initial profile should have been deleted as sign-in and sign out were no + // longer allowed. + PrefService* local_state = g_browser_process->local_state(); + DCHECK(local_state); + const base::ListValue* deleted_profiles = + local_state->GetList(prefs::kProfilesDeleted); + EXPECT_TRUE(deleted_profiles); + EXPECT_EQ(1U, deleted_profiles->GetList().size()); + + content::RunAllTasksUntilIdle(); + + // Verify that there is an active profile. + Profile* initial_profile = browser()->profile(); + EXPECT_EQ(1U, g_browser_process->profile_manager()->GetNumberOfProfiles()); + EXPECT_EQ(g_browser_process->profile_manager()->GetLastUsedProfile(), + initial_profile); +} diff --git a/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.cc b/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.cc new file mode 100644 index 00000000000..ecd8893f92b --- /dev/null +++ b/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.cc @@ -0,0 +1,179 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/dice_intercepted_session_startup_helper.h" + +#include +#include + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "chrome/browser/signin/account_reconcilor_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/ui/browser_navigator.h" +#include "chrome/browser/ui/browser_navigator_params.h" +#include "chrome/common/webui_url_constants.h" +#include "components/signin/public/base/multilogin_parameters.h" +#include "components/signin/public/identity_manager/accounts_cookie_mutator.h" +#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h" +#include "components/signin/public/identity_manager/set_accounts_in_cookie_result.h" +#include "content/public/browser/web_contents.h" +#include "google_apis/gaia/gaia_auth_fetcher.h" +#include "google_apis/gaia/gaia_auth_util.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "url/gurl.h" + +namespace { + +// Returns true if |account_id| is signed in the cookies. +bool CookieInfoContains(const signin::AccountsInCookieJarInfo& cookie_info, + const CoreAccountId& account_id) { + const std::vector& accounts = + cookie_info.signed_in_accounts; + return std::find_if(accounts.begin(), accounts.end(), + [&account_id](const gaia::ListedAccount& account) { + return account.id == account_id; + }) != accounts.end(); +} + +} // namespace + +DiceInterceptedSessionStartupHelper::DiceInterceptedSessionStartupHelper( + Profile* profile, + bool is_new_profile, + CoreAccountId account_id, + content::WebContents* tab_to_move) + : profile_(profile), + use_multilogin_(is_new_profile), + account_id_(account_id) { + if (tab_to_move) + web_contents_ = tab_to_move->GetWeakPtr(); +} + +DiceInterceptedSessionStartupHelper::~DiceInterceptedSessionStartupHelper() = + default; + +void DiceInterceptedSessionStartupHelper::Startup(base::OnceClosure callback) { + callback_ = std::move(callback); + + // Wait until the account is set in cookies of the newly created profile + // before opening the URL, so that the user is signed-in in content area. If + // the account is still not in the cookie after some timeout, proceed without + // cookies, so that the user can at least take some action in the new profile. + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile_); + signin::AccountsInCookieJarInfo cookie_info = + identity_manager->GetAccountsInCookieJar(); + if (cookie_info.accounts_are_fresh && + CookieInfoContains(cookie_info, account_id_)) { + MoveTab(); + } else { + // Set the timeout. + on_cookie_update_timeout_.Reset(base::BindOnce( + &DiceInterceptedSessionStartupHelper::MoveTab, base::Unretained(this))); + // Adding accounts to the cookies can be an expensive operation. In + // particular the ExternalCCResult fetch may time out after multiple seconds + // (see kExternalCCResultTimeoutSeconds and https://crbug.com/750316#c37). + base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, on_cookie_update_timeout_.callback(), base::Seconds(12)); + + accounts_in_cookie_observer_.Observe(identity_manager); + if (use_multilogin_) + StartupMultilogin(identity_manager); + else + StartupReconcilor(identity_manager); + } +} + +void DiceInterceptedSessionStartupHelper::OnAccountsInCookieUpdated( + const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info, + const GoogleServiceAuthError& error) { + if (error != GoogleServiceAuthError::AuthErrorNone()) + return; + if (!accounts_in_cookie_jar_info.accounts_are_fresh) + return; + if (!CookieInfoContains(accounts_in_cookie_jar_info, account_id_)) + return; + + MoveTab(); +} + +void DiceInterceptedSessionStartupHelper::OnStateChanged( + signin_metrics::AccountReconcilorState state) { + DCHECK(!use_multilogin_); + if (state == signin_metrics::ACCOUNT_RECONCILOR_ERROR) { + reconcile_error_encountered_ = true; + return; + } + + // TODO(https://crbug.com/1051864): remove this when the cookie updates are + // correctly sent after reconciliation. + if (state == signin_metrics::ACCOUNT_RECONCILOR_OK) { + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile_); + // GetAccountsInCookieJar() automatically re-schedules a /ListAccounts call + // if the cookie is not fresh. + signin::AccountsInCookieJarInfo cookie_info = + identity_manager->GetAccountsInCookieJar(); + OnAccountsInCookieUpdated(cookie_info, + GoogleServiceAuthError::AuthErrorNone()); + } +} + +void DiceInterceptedSessionStartupHelper::StartupMultilogin( + signin::IdentityManager* identity_manager) { + // Lock the reconcilor to avoid making multiple multilogin calls. + reconcilor_lock_ = std::make_unique( + AccountReconcilorFactory::GetForProfile(profile_)); + + // Start the multilogin call. + signin::MultiloginParameters params = { + /*mode=*/gaia::MultiloginMode::MULTILOGIN_UPDATE_COOKIE_ACCOUNTS_ORDER, + /*accounts_to_send=*/{account_id_}}; + identity_manager->GetAccountsCookieMutator()->SetAccountsInCookie( + params, gaia::GaiaSource::kChrome, + base::BindOnce( + &DiceInterceptedSessionStartupHelper::OnSetAccountInCookieCompleted, + weak_factory_.GetWeakPtr())); +} + +void DiceInterceptedSessionStartupHelper::StartupReconcilor( + signin::IdentityManager* identity_manager) { + // TODO(https://crbug.com/1051864): cookie notifications are not triggered + // when the account is added by the reconcilor. Observe the reconcilor and + // re-trigger the cookie update when it completes. + reconcilor_observer_.Observe( + AccountReconcilorFactory::GetForProfile(profile_)); + identity_manager->GetAccountsCookieMutator()->TriggerCookieJarUpdate(); +} + +void DiceInterceptedSessionStartupHelper::OnSetAccountInCookieCompleted( + signin::SetAccountsInCookieResult result) { + DCHECK(use_multilogin_); + MoveTab(); +} + +void DiceInterceptedSessionStartupHelper::MoveTab() { + accounts_in_cookie_observer_.Reset(); + reconcilor_observer_.Reset(); + on_cookie_update_timeout_.Cancel(); + reconcilor_lock_.reset(); + + GURL url_to_open = GURL(chrome::kChromeUINewTabURL); + // If the intercepted web contents is still alive, close it now. + if (web_contents_) { + url_to_open = web_contents_->GetURL(); + web_contents_->Close(); + } + + // Open a new browser. + NavigateParams params(profile_, url_to_open, + ui::PAGE_TRANSITION_AUTO_BOOKMARK); + Navigate(¶ms); + + if (callback_) + std::move(callback_).Run(); +} diff --git a/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.h b/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.h new file mode 100644 index 00000000000..2895a90d66b --- /dev/null +++ b/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.h @@ -0,0 +1,102 @@ +// Copyright 2020 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 CHROME_BROWSER_SIGNIN_DICE_INTERCEPTED_SESSION_STARTUP_HELPER_H_ +#define CHROME_BROWSER_SIGNIN_DICE_INTERCEPTED_SESSION_STARTUP_HELPER_H_ + +#include "base/callback_forward.h" +#include "base/cancelable_callback.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/scoped_observation.h" +#include "components/signin/core/browser/account_reconcilor.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "content/public/browser/web_contents_observer.h" +#include "google_apis/gaia/core_account_id.h" + +namespace content { +class WebContents; +} + +namespace signin { +struct AccountsInCookieJarInfo; +class IdentityManager; +enum class SetAccountsInCookieResult; +} + +class GoogleServiceAuthError; +class Profile; + +// Called when the user accepted the dice signin interception and the new +// profile has been created. Creates a new browser and moves the intercepted tab +// to the new browser. +// It is assumed that the account is already in the profile, but not necessarily +// in the content area (cookies). +class DiceInterceptedSessionStartupHelper + : public signin::IdentityManager::Observer, + public AccountReconcilor::Observer { + public: + // |profile| is the new profile that was created after signin interception. + // |account_id| is the main account for the profile, it's already in the + // profile. + // |tab_to_move| is the tab where the interception happened, in the source + // profile. + DiceInterceptedSessionStartupHelper(Profile* profile, + bool is_new_profile, + CoreAccountId account_id, + content::WebContents* tab_to_move); + + ~DiceInterceptedSessionStartupHelper() override; + + DiceInterceptedSessionStartupHelper( + const DiceInterceptedSessionStartupHelper&) = delete; + DiceInterceptedSessionStartupHelper& operator=( + const DiceInterceptedSessionStartupHelper&) = delete; + + // Start up the session. Can only be called once. + void Startup(base::OnceClosure callback); + + // signin::IdentityManager::Observer: + void OnAccountsInCookieUpdated( + const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info, + const GoogleServiceAuthError& error) override; + + // AccountReconcilor::Observer: + void OnStateChanged(signin_metrics::AccountReconcilorState state) override; + + private: + // For new profiles, the account is added directly using multilogin. + void StartupMultilogin(signin::IdentityManager* identity_manager); + + // For existing profiles, simply wait for the reconcilor to update the + // accounts. + void StartupReconcilor(signin::IdentityManager* identity_manager); + + // Called when multilogin completes. + void OnSetAccountInCookieCompleted(signin::SetAccountsInCookieResult result); + + // Creates a browser with a new tab, and closes the intercepted tab if it's + // still open. + void MoveTab(); + + const raw_ptr profile_; + base::WeakPtr web_contents_; + bool use_multilogin_; + CoreAccountId account_id_; + base::OnceClosure callback_; + bool reconcile_error_encountered_ = false; + base::ScopedObservation + accounts_in_cookie_observer_{this}; + base::ScopedObservation + reconcilor_observer_{this}; + std::unique_ptr reconcilor_lock_; + // Timeout while waiting for the account to be added to the cookies in the new + // profile. + base::CancelableOnceCallback on_cookie_update_timeout_; + + base::WeakPtrFactory weak_factory_{this}; +}; + +#endif // CHROME_BROWSER_SIGNIN_DICE_INTERCEPTED_SESSION_STARTUP_HELPER_H_ diff --git a/chromium/chrome/browser/signin/dice_response_handler.cc b/chromium/chrome/browser/signin/dice_response_handler.cc new file mode 100644 index 00000000000..88d7563846f --- /dev/null +++ b/chromium/chrome/browser/signin/dice_response_handler.cc @@ -0,0 +1,426 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/dice_response_handler.h" + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/location.h" +#include "base/logging.h" +#include "base/memory/singleton.h" +#include "base/metrics/histogram_macros.h" +#include "base/strings/stringprintf.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profiles_state.h" +#include "chrome/browser/signin/about_signin_internals_factory.h" +#include "chrome/browser/signin/account_reconcilor_factory.h" +#include "chrome/browser/signin/chrome_signin_client_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/ui/webui/profile_helper.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" +#include "components/signin/core/browser/about_signin_internals.h" +#include "components/signin/core/browser/signin_header_helper.h" +#include "components/signin/public/base/signin_client.h" +#include "components/signin/public/base/signin_metrics.h" +#include "components/signin/public/identity_manager/accounts_mutator.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "google_apis/gaia/gaia_auth_fetcher.h" +#include "google_apis/gaia/gaia_auth_util.h" +#include "google_apis/gaia/google_service_auth_error.h" + +const int kDiceTokenFetchTimeoutSeconds = 10; +// Timeout for locking the account reconcilor when +// there was OAuth outage in Dice. +const int kLockAccountReconcilorTimeoutHours = 12; + +const base::Feature kSupportOAuthOutageInDice{"SupportOAuthOutageInDice", + base::FEATURE_ENABLED_BY_DEFAULT}; + +namespace { + +// The UMA histograms that logs events related to Dice responses. +const char kDiceResponseHeaderHistogram[] = "Signin.DiceResponseHeader"; +const char kDiceTokenFetchResultHistogram[] = "Signin.DiceTokenFetchResult"; + +// Used for UMA. Do not reorder, append new values at the end. +enum DiceResponseHeader { + // Received a signin header. + kSignin = 0, + // Received a signout header including the Chrome primary account. + kSignoutPrimary = 1, + // Received a signout header for other account(s). + kSignoutSecondary = 2, + // Received a "EnableSync" header. + kEnableSync = 3, + + kDiceResponseHeaderCount +}; + +// Used for UMA. Do not reorder, append new values at the end. +enum DiceTokenFetchResult { + // The token fetch succeeded. + kFetchSuccess = 0, + // The token fetch was aborted. For example, if another request for the same + // account is already in flight. + kFetchAbort = 1, + // The token fetch failed because Gaia responsed with an error. + kFetchFailure = 2, + // The token fetch failed because no response was received from Gaia. + kFetchTimeout = 3, + + kDiceTokenFetchResultCount +}; + +class DiceResponseHandlerFactory : public BrowserContextKeyedServiceFactory { + public: + // Returns an instance of the factory singleton. + static DiceResponseHandlerFactory* GetInstance() { + return base::Singleton::get(); + } + + static DiceResponseHandler* GetForProfile(Profile* profile) { + return static_cast( + GetInstance()->GetServiceForBrowserContext(profile, true)); + } + + private: + friend struct base::DefaultSingletonTraits; + + DiceResponseHandlerFactory() + : BrowserContextKeyedServiceFactory( + "DiceResponseHandler", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(AboutSigninInternalsFactory::GetInstance()); + DependsOn(AccountReconcilorFactory::GetInstance()); + DependsOn(ChromeSigninClientFactory::GetInstance()); + DependsOn(IdentityManagerFactory::GetInstance()); + } + + ~DiceResponseHandlerFactory() override {} + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* context) const override { + if (context->IsOffTheRecord()) + return nullptr; + + Profile* profile = static_cast(context); + return new DiceResponseHandler( + ChromeSigninClientFactory::GetForProfile(profile), + IdentityManagerFactory::GetForProfile(profile), + AccountReconcilorFactory::GetForProfile(profile), + AboutSigninInternalsFactory::GetForProfile(profile), + profile->GetPath()); + } +}; + +// Histogram macros expand to a lot of code, so it is better to wrap them in +// functions. + +void RecordDiceResponseHeader(DiceResponseHeader header) { + UMA_HISTOGRAM_ENUMERATION(kDiceResponseHeaderHistogram, header, + kDiceResponseHeaderCount); +} + +void RecordDiceFetchTokenResult(DiceTokenFetchResult result) { + UMA_HISTOGRAM_ENUMERATION(kDiceTokenFetchResultHistogram, result, + kDiceTokenFetchResultCount); +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +// DiceTokenFetcher +//////////////////////////////////////////////////////////////////////////////// + +DiceResponseHandler::DiceTokenFetcher::DiceTokenFetcher( + const std::string& gaia_id, + const std::string& email, + const std::string& authorization_code, + SigninClient* signin_client, + AccountReconcilor* account_reconcilor, + std::unique_ptr delegate, + DiceResponseHandler* dice_response_handler) + : gaia_id_(gaia_id), + email_(email), + authorization_code_(authorization_code), + delegate_(std::move(delegate)), + dice_response_handler_(dice_response_handler), + timeout_closure_( + base::BindOnce(&DiceResponseHandler::DiceTokenFetcher::OnTimeout, + base::Unretained(this))), + should_enable_sync_(false) { + DCHECK(dice_response_handler_); + account_reconcilor_lock_ = + std::make_unique(account_reconcilor); + gaia_auth_fetcher_ = + signin_client->CreateGaiaAuthFetcher(this, gaia::GaiaSource::kChrome); + VLOG(1) << "Start fetching token for account: " << email; + gaia_auth_fetcher_->StartAuthCodeForOAuth2TokenExchange(authorization_code_); + base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, timeout_closure_.callback(), + base::Seconds(kDiceTokenFetchTimeoutSeconds)); +} + +DiceResponseHandler::DiceTokenFetcher::~DiceTokenFetcher() {} + +void DiceResponseHandler::DiceTokenFetcher::OnTimeout() { + RecordDiceFetchTokenResult(kFetchTimeout); + gaia_auth_fetcher_.reset(); + timeout_closure_.Cancel(); + dice_response_handler_->OnTokenExchangeFailure( + this, GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED)); + // |this| may be deleted at this point. +} + +void DiceResponseHandler::DiceTokenFetcher::OnClientOAuthSuccess( + const GaiaAuthConsumer::ClientOAuthResult& result) { + RecordDiceFetchTokenResult(kFetchSuccess); + gaia_auth_fetcher_.reset(); + timeout_closure_.Cancel(); + dice_response_handler_->OnTokenExchangeSuccess( + this, result.refresh_token, result.is_under_advanced_protection); + // |this| may be deleted at this point. +} + +void DiceResponseHandler::DiceTokenFetcher::OnClientOAuthFailure( + const GoogleServiceAuthError& error) { + RecordDiceFetchTokenResult(kFetchFailure); + gaia_auth_fetcher_.reset(); + timeout_closure_.Cancel(); + dice_response_handler_->OnTokenExchangeFailure(this, error); + // |this| may be deleted at this point. +} + +//////////////////////////////////////////////////////////////////////////////// +// DiceResponseHandler +//////////////////////////////////////////////////////////////////////////////// + +// static +DiceResponseHandler* DiceResponseHandler::GetForProfile(Profile* profile) { + return DiceResponseHandlerFactory::GetForProfile(profile); +} + +DiceResponseHandler::DiceResponseHandler( + SigninClient* signin_client, + signin::IdentityManager* identity_manager, + AccountReconcilor* account_reconcilor, + AboutSigninInternals* about_signin_internals, + const base::FilePath& profile_path) + : signin_client_(signin_client), + identity_manager_(identity_manager), + account_reconcilor_(account_reconcilor), + about_signin_internals_(about_signin_internals), + profile_path_(profile_path) { + DCHECK(signin_client_); + DCHECK(identity_manager_); + DCHECK(account_reconcilor_); + DCHECK(about_signin_internals_); +} + +DiceResponseHandler::~DiceResponseHandler() {} + +void DiceResponseHandler::ProcessDiceHeader( + const signin::DiceResponseParams& dice_params, + std::unique_ptr delegate) { + DCHECK(delegate); + switch (dice_params.user_intention) { + case signin::DiceAction::SIGNIN: { + const signin::DiceResponseParams::AccountInfo& info = + dice_params.signin_info->account_info; + ProcessDiceSigninHeader( + info.gaia_id, info.email, dice_params.signin_info->authorization_code, + dice_params.signin_info->no_authorization_code, std::move(delegate)); + return; + } + case signin::DiceAction::ENABLE_SYNC: { + const signin::DiceResponseParams::AccountInfo& info = + dice_params.enable_sync_info->account_info; + ProcessEnableSyncHeader(info.gaia_id, info.email, std::move(delegate)); + return; + } + case signin::DiceAction::SIGNOUT: + DCHECK_GT(dice_params.signout_info->account_infos.size(), 0u); + ProcessDiceSignoutHeader(dice_params.signout_info->account_infos); + return; + case signin::DiceAction::NONE: + NOTREACHED() << "Invalid Dice response parameters."; + return; + } + NOTREACHED(); +} + +size_t DiceResponseHandler::GetPendingDiceTokenFetchersCountForTesting() const { + return token_fetchers_.size(); +} + +void DiceResponseHandler::OnTimeoutUnlockReconcilor() { + lock_.reset(); +} + +void DiceResponseHandler::SetTaskRunner( + scoped_refptr task_runner) { + task_runner_ = std::move(task_runner); +} + +void DiceResponseHandler::ProcessDiceSigninHeader( + const std::string& gaia_id, + const std::string& email, + const std::string& authorization_code, + bool no_authorization_code, + std::unique_ptr delegate) { + if (no_authorization_code) { + if (base::FeatureList::IsEnabled(kSupportOAuthOutageInDice)) { + lock_ = std::make_unique(account_reconcilor_); + about_signin_internals_->OnRefreshTokenReceived( + "Missing authorization code due to OAuth outage in Dice."); + if (!timer_) { + timer_ = std::make_unique(); + if (task_runner_) + timer_->SetTaskRunner(task_runner_); + } + // If there is already another lock, the timer will be reset and + // we'll wait another full timeout. + timer_->Start( + FROM_HERE, base::Hours(kLockAccountReconcilorTimeoutHours), + base::BindOnce(&DiceResponseHandler::OnTimeoutUnlockReconcilor, + base::Unretained(this))); + } + return; + } + + DCHECK(!gaia_id.empty()); + DCHECK(!email.empty()); + DCHECK(!authorization_code.empty()); + VLOG(1) << "Start processing Dice signin response"; + RecordDiceResponseHeader(kSignin); + + for (auto it = token_fetchers_.begin(); it != token_fetchers_.end(); ++it) { + if ((it->get()->gaia_id() == gaia_id) && (it->get()->email() == email) && + (it->get()->authorization_code() == authorization_code)) { + RecordDiceFetchTokenResult(kFetchAbort); + return; // There is already a request in flight with the same parameters. + } + } + token_fetchers_.push_back(std::make_unique( + gaia_id, email, authorization_code, signin_client_, account_reconcilor_, + std::move(delegate), this)); +} + +void DiceResponseHandler::ProcessEnableSyncHeader( + const std::string& gaia_id, + const std::string& email, + std::unique_ptr delegate) { + VLOG(1) << "Start processing Dice enable sync response"; + RecordDiceResponseHeader(kEnableSync); + for (auto it = token_fetchers_.begin(); it != token_fetchers_.end(); ++it) { + DiceTokenFetcher* fetcher = it->get(); + if (fetcher->gaia_id() == gaia_id) { + DCHECK(gaia::AreEmailsSame(fetcher->email(), email)); + // If there is a fetch in progress for a resfresh token for the given + // account, then simply mark it to enable sync after the refresh token is + // available. + fetcher->set_should_enable_sync(true); + return; // There is already a request in flight with the same parameters. + } + } + CoreAccountId account_id = + identity_manager_->PickAccountIdForAccount(gaia_id, email); + delegate->EnableSync(account_id); +} + +void DiceResponseHandler::ProcessDiceSignoutHeader( + const std::vector& account_infos) { + VLOG(1) << "Start processing Dice signout response"; + + CoreAccountId primary_account = + identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSync); + bool primary_account_signed_out = false; + auto* accounts_mutator = identity_manager_->GetAccountsMutator(); + for (const auto& account_info : account_infos) { + CoreAccountId signed_out_account = + identity_manager_->PickAccountIdForAccount(account_info.gaia_id, + account_info.email); + if (signed_out_account == primary_account) { + primary_account_signed_out = true; + RecordDiceResponseHeader(kSignoutPrimary); + + // Put the account in error state. + accounts_mutator->InvalidateRefreshTokenForPrimaryAccount( + signin_metrics::SourceForRefreshTokenOperation:: + kDiceResponseHandler_Signout); + } else { + accounts_mutator->RemoveAccount( + signed_out_account, signin_metrics::SourceForRefreshTokenOperation:: + kDiceResponseHandler_Signout); + } + + // If a token fetch is in flight for the same account, cancel it. + for (auto it = token_fetchers_.begin(); it != token_fetchers_.end(); ++it) { + CoreAccountId token_fetcher_account_id = + identity_manager_->PickAccountIdForAccount(it->get()->gaia_id(), + it->get()->email()); + if (token_fetcher_account_id == signed_out_account) { + token_fetchers_.erase(it); + break; + } + } + } + + if (!primary_account_signed_out) + RecordDiceResponseHeader(kSignoutSecondary); +} + +void DiceResponseHandler::DeleteTokenFetcher(DiceTokenFetcher* token_fetcher) { + for (auto it = token_fetchers_.begin(); it != token_fetchers_.end(); ++it) { + if (it->get() == token_fetcher) { + token_fetchers_.erase(it); + return; + } + } + NOTREACHED(); +} + +void DiceResponseHandler::OnTokenExchangeSuccess( + DiceTokenFetcher* token_fetcher, + const std::string& refresh_token, + bool is_under_advanced_protection) { + const std::string& email = token_fetcher->email(); + const std::string& gaia_id = token_fetcher->gaia_id(); + VLOG(1) << "[Dice] OAuth success for email " << email; + bool should_enable_sync = token_fetcher->should_enable_sync(); + CoreAccountId account_id = + identity_manager_->PickAccountIdForAccount(gaia_id, email); + bool is_new_account = + !identity_manager_->HasAccountWithRefreshToken(account_id); + identity_manager_->GetAccountsMutator()->AddOrUpdateAccount( + gaia_id, email, refresh_token, is_under_advanced_protection, + signin_metrics::SourceForRefreshTokenOperation:: + kDiceResponseHandler_Signin); + about_signin_internals_->OnRefreshTokenReceived( + base::StringPrintf("Successful (%s)", account_id.ToString().c_str())); + token_fetcher->delegate()->HandleTokenExchangeSuccess(account_id, + is_new_account); + if (should_enable_sync) + token_fetcher->delegate()->EnableSync(account_id); + + DeleteTokenFetcher(token_fetcher); +} + +void DiceResponseHandler::OnTokenExchangeFailure( + DiceTokenFetcher* token_fetcher, + const GoogleServiceAuthError& error) { + const std::string& email = token_fetcher->email(); + const std::string& gaia_id = token_fetcher->gaia_id(); + CoreAccountId account_id = + identity_manager_->PickAccountIdForAccount(gaia_id, email); + about_signin_internals_->OnRefreshTokenReceived( + base::StringPrintf("Failure (%s)", account_id.ToString().c_str())); + token_fetcher->delegate()->HandleTokenExchangeFailure(email, error); + + DeleteTokenFetcher(token_fetcher); +} diff --git a/chromium/chrome/browser/signin/dice_response_handler.h b/chromium/chrome/browser/signin/dice_response_handler.h new file mode 100644 index 00000000000..2e90c9ddc11 --- /dev/null +++ b/chromium/chrome/browser/signin/dice_response_handler.h @@ -0,0 +1,187 @@ +// Copyright 2017 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 CHROME_BROWSER_SIGNIN_DICE_RESPONSE_HANDLER_H_ +#define CHROME_BROWSER_SIGNIN_DICE_RESPONSE_HANDLER_H_ + +#include +#include +#include + +#include "base/cancelable_callback.h" +#include "base/files/file_path.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/scoped_refptr.h" +#include "base/task/sequenced_task_runner.h" +#include "base/timer/timer.h" +#include "components/keyed_service/core/keyed_service.h" +#include "components/signin/core/browser/account_reconcilor.h" +#include "components/signin/core/browser/signin_header_helper.h" +#include "components/signin/public/base/account_consistency_method.h" +#include "google_apis/gaia/gaia_auth_consumer.h" + +class AboutSigninInternals; +class GaiaAuthFetcher; +class GoogleServiceAuthError; +class SigninClient; +class Profile; + +namespace signin { +class IdentityManager; +} + +// Exposed for testing. +extern const int kDiceTokenFetchTimeoutSeconds; +// Exposed for testing. +extern const int kLockAccountReconcilorTimeoutHours; +extern const base::Feature kSupportOAuthOutageInDice; + +// Delegate interface for processing a dice request. +class ProcessDiceHeaderDelegate { + public: + virtual ~ProcessDiceHeaderDelegate() = default; + + // Called when a token was successfully exchanged. + // Called after the account was seeded in the account tracker service and + // after the refresh token was fetched and updated in the token service. + // |is_new_account| is true if the account was added to Chrome (it is not a + // re-auth). + virtual void HandleTokenExchangeSuccess(CoreAccountId account_id, + bool is_new_account) = 0; + + // Asks the delegate to enable sync for the |account_id|. + // Called after the account was seeded in the account tracker service and + // after the refresh token was fetched and updated in the token service. + virtual void EnableSync(const CoreAccountId& account_id) = 0; + + // Handles a failure in the token exchange (i.e. shows the error to the user). + virtual void HandleTokenExchangeFailure( + const std::string& email, + const GoogleServiceAuthError& error) = 0; +}; + +// Processes the Dice responses from Gaia. +class DiceResponseHandler : public KeyedService { + public: + // Returns the DiceResponseHandler associated with this profile. + // May return nullptr if there is none (e.g. in incognito). + static DiceResponseHandler* GetForProfile(Profile* profile); + + DiceResponseHandler(SigninClient* signin_client, + signin::IdentityManager* identity_manager, + AccountReconcilor* account_reconcilor, + AboutSigninInternals* about_signin_internals, + const base::FilePath& profile_path_); + + DiceResponseHandler(const DiceResponseHandler&) = delete; + DiceResponseHandler& operator=(const DiceResponseHandler&) = delete; + + ~DiceResponseHandler() override; + + // Must be called when receiving a Dice response header. + void ProcessDiceHeader(const signin::DiceResponseParams& dice_params, + std::unique_ptr delegate); + + // Returns the number of pending DiceTokenFetchers. Exposed for testing. + size_t GetPendingDiceTokenFetchersCountForTesting() const; + + // Sets |task_runner_| for testing. + void SetTaskRunner(scoped_refptr task_runner); + + private: + // Helper class to fetch a refresh token from an authorization code. + class DiceTokenFetcher : public GaiaAuthConsumer { + public: + DiceTokenFetcher(const std::string& gaia_id, + const std::string& email, + const std::string& authorization_code, + SigninClient* signin_client, + AccountReconcilor* account_reconcilor, + std::unique_ptr delegate, + DiceResponseHandler* dice_response_handler); + + DiceTokenFetcher(const DiceTokenFetcher&) = delete; + DiceTokenFetcher& operator=(const DiceTokenFetcher&) = delete; + + ~DiceTokenFetcher() override; + + const std::string& gaia_id() const { return gaia_id_; } + const std::string& email() const { return email_; } + const std::string& authorization_code() const { + return authorization_code_; + } + bool should_enable_sync() const { return should_enable_sync_; } + void set_should_enable_sync(bool should_enable_sync) { + should_enable_sync_ = should_enable_sync; + } + ProcessDiceHeaderDelegate* delegate() { return delegate_.get(); } + + private: + // Called by |timeout_closure_| when the request times out. + void OnTimeout(); + + // GaiaAuthConsumer implementation: + void OnClientOAuthSuccess( + const GaiaAuthConsumer::ClientOAuthResult& result) override; + void OnClientOAuthFailure(const GoogleServiceAuthError& error) override; + + // Lock the account reconcilor while tokens are being fetched. + std::unique_ptr account_reconcilor_lock_; + + std::string gaia_id_; + std::string email_; + std::string authorization_code_; + std::unique_ptr delegate_; + raw_ptr dice_response_handler_; + base::CancelableOnceClosure timeout_closure_; + bool should_enable_sync_; + std::unique_ptr gaia_auth_fetcher_; + }; + + // Deletes the token fetcher. + void DeleteTokenFetcher(DiceTokenFetcher* token_fetcher); + + // Process the Dice signin action. + void ProcessDiceSigninHeader( + const std::string& gaia_id, + const std::string& email, + const std::string& authorization_code, + bool no_authorization_code, + std::unique_ptr delegate); + + // Process the Dice enable sync action. + void ProcessEnableSyncHeader( + const std::string& gaia_id, + const std::string& email, + std::unique_ptr delegate); + + // Process the Dice signout action. + void ProcessDiceSignoutHeader( + const std::vector& + account_infos); + + // Called after exchanging an OAuth 2.0 authorization code for a refresh token + // after DiceAction::SIGNIN. + void OnTokenExchangeSuccess(DiceTokenFetcher* token_fetcher, + const std::string& refresh_token, + bool is_under_advanced_protection); + void OnTokenExchangeFailure(DiceTokenFetcher* token_fetcher, + const GoogleServiceAuthError& error); + // Called to unlock the reconcilor after a SLO outage. + void OnTimeoutUnlockReconcilor(); + + raw_ptr signin_client_; + raw_ptr identity_manager_; + raw_ptr account_reconcilor_; + raw_ptr about_signin_internals_; + base::FilePath profile_path_; + std::vector> token_fetchers_; + // Lock the account reconcilor for kLockAccountReconcilorTimeoutHours + // when there was OAuth outage in Dice. + std::unique_ptr lock_; + std::unique_ptr timer_; + scoped_refptr task_runner_; +}; + +#endif // CHROME_BROWSER_SIGNIN_DICE_RESPONSE_HANDLER_H_ diff --git a/chromium/chrome/browser/signin/dice_response_handler_unittest.cc b/chromium/chrome/browser/signin/dice_response_handler_unittest.cc new file mode 100644 index 00000000000..437f4a4fb20 --- /dev/null +++ b/chromium/chrome/browser/signin/dice_response_handler_unittest.cc @@ -0,0 +1,820 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/dice_response_handler.h" + +#include +#include + +#include "base/bind.h" +#include "base/check.h" +#include "base/command_line.h" +#include "base/files/scoped_temp_dir.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/ref_counted.h" +#include "base/notreached.h" +#include "base/test/scoped_feature_list.h" +#include "base/test/task_environment.h" +#include "base/time/time.h" +#include "chrome/test/base/testing_profile.h" +#include "components/signin/core/browser/about_signin_internals.h" +#include "components/signin/core/browser/account_reconcilor.h" +#include "components/signin/core/browser/dice_account_reconcilor_delegate.h" +#include "components/signin/core/browser/signin_error_controller.h" +#include "components/signin/core/browser/signin_header_helper.h" +#include "components/signin/public/base/test_signin_client.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "components/signin/public/identity_manager/identity_test_utils.h" +#include "components/sync_preferences/testing_pref_service_syncable.h" +#include "content/public/test/browser_task_environment.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using signin::DiceAction; +using signin::DiceResponseParams; + +namespace { + +const char kAuthorizationCode[] = "authorization_code"; +const char kEmail[] = "test@email.com"; +const int kSessionIndex = 42; + +// TestSigninClient implementation that intercepts the GaiaAuthConsumer and +// replaces it by a dummy one. +class DiceTestSigninClient : public TestSigninClient, public GaiaAuthConsumer { + public: + explicit DiceTestSigninClient(PrefService* pref_service) + : TestSigninClient(pref_service), consumer_(nullptr) {} + + DiceTestSigninClient(const DiceTestSigninClient&) = delete; + DiceTestSigninClient& operator=(const DiceTestSigninClient&) = delete; + + ~DiceTestSigninClient() override {} + + std::unique_ptr CreateGaiaAuthFetcher( + GaiaAuthConsumer* consumer, + gaia::GaiaSource source) override { + DCHECK(!consumer_ || (consumer_ == consumer)); + consumer_ = consumer; + + // Pass |this| as a dummy consumer to CreateGaiaAuthFetcher(). + // Since DiceTestSigninClient does not overrides any consumer method, + // everything will be dropped on the floor. + return TestSigninClient::CreateGaiaAuthFetcher(this, source); + } + + // We want to reset |consumer_| here before the test interacts with the last + // consumer. Interacting with the last consumer (simulating success of the + // fetcher) namely sometimes immediately triggers another fetch with another + // consumer. If |consumer_| is non-null, we would hit the DCHECK. + GaiaAuthConsumer* GetAndClearConsumer() { + GaiaAuthConsumer* last_consumer = consumer_; + consumer_ = nullptr; + return last_consumer; + } + + private: + raw_ptr consumer_; +}; + +class DiceResponseHandlerTest : public testing::Test, + public AccountReconcilor::Observer { + public: + // Called after the refresh token was fetched and added in the token service. + void HandleTokenExchangeSuccess(CoreAccountId account_id, + bool is_new_account) { + token_exchange_account_id_ = account_id; + token_exchange_is_new_account_ = is_new_account; + } + + // Called after the refresh token was fetched and added in the token service. + void EnableSync(const CoreAccountId& account_id) { + enable_sync_account_id_ = account_id; + } + + void HandleTokenExchangeFailure(const std::string& email, + const GoogleServiceAuthError& error) { + auth_error_email_ = email; + auth_error_ = error; + } + + protected: + DiceResponseHandlerTest() + : task_environment_( + base::test::SingleThreadTaskEnvironment::MainThreadType::IO, + base::test::SingleThreadTaskEnvironment::TimeSource:: + MOCK_TIME), // URLRequestContext requires IO. + signin_client_(&pref_service_), + identity_test_env_(/*test_url_loader_factory=*/nullptr, + &pref_service_, + signin::AccountConsistencyMethod::kDice, + &signin_client_), + signin_error_controller_( + SigninErrorController::AccountMode::PRIMARY_ACCOUNT, + identity_test_env_.identity_manager()) { + EXPECT_TRUE(temp_dir_.CreateUniqueTempDir()); + AboutSigninInternals::RegisterPrefs(pref_service_.registry()); + auto account_reconcilor_delegate = + std::make_unique(); + account_reconcilor_ = std::make_unique( + identity_test_env_.identity_manager(), &signin_client_, + std::move(account_reconcilor_delegate)); + account_reconcilor_->AddObserver(this); + + about_signin_internals_ = std::make_unique( + identity_test_env_.identity_manager(), &signin_error_controller_, + signin::AccountConsistencyMethod::kDice, &signin_client_, + account_reconcilor_.get()); + + dice_response_handler_ = std::make_unique( + &signin_client_, identity_test_env_.identity_manager(), + account_reconcilor_.get(), about_signin_internals_.get(), + temp_dir_.GetPath()); + } + + ~DiceResponseHandlerTest() override { + account_reconcilor_->RemoveObserver(this); + account_reconcilor_->Shutdown(); + about_signin_internals_->Shutdown(); + signin_error_controller_.Shutdown(); + } + + DiceResponseParams MakeDiceParams(DiceAction action) { + DiceResponseParams dice_params; + dice_params.user_intention = action; + DiceResponseParams::AccountInfo account_info; + account_info.gaia_id = signin::GetTestGaiaIdForEmail(kEmail); + account_info.email = kEmail; + account_info.session_index = kSessionIndex; + switch (action) { + case DiceAction::SIGNIN: + dice_params.signin_info = + std::make_unique(); + dice_params.signin_info->account_info = account_info; + dice_params.signin_info->authorization_code = kAuthorizationCode; + break; + case DiceAction::ENABLE_SYNC: + dice_params.enable_sync_info = + std::make_unique(); + dice_params.enable_sync_info->account_info = account_info; + break; + case DiceAction::SIGNOUT: + dice_params.signout_info = + std::make_unique(); + dice_params.signout_info->account_infos.push_back(account_info); + break; + case DiceAction::NONE: + NOTREACHED(); + break; + } + return dice_params; + } + + // AccountReconcilor::Observer: + void OnBlockReconcile() override { ++reconcilor_blocked_count_; } + void OnUnblockReconcile() override { ++reconcilor_unblocked_count_; } + + signin::IdentityManager* identity_manager() { + return identity_test_env_.identity_manager(); + } + + base::test::SingleThreadTaskEnvironment task_environment_; + base::ScopedTempDir temp_dir_; + sync_preferences::TestingPrefServiceSyncable pref_service_; + DiceTestSigninClient signin_client_; + signin::IdentityTestEnvironment identity_test_env_; + SigninErrorController signin_error_controller_; + std::unique_ptr about_signin_internals_; + std::unique_ptr account_reconcilor_; + std::unique_ptr dice_response_handler_; + int reconcilor_blocked_count_ = 0; + int reconcilor_unblocked_count_ = 0; + CoreAccountId token_exchange_account_id_; + bool token_exchange_is_new_account_ = false; + CoreAccountId enable_sync_account_id_; + GoogleServiceAuthError auth_error_; + std::string auth_error_email_; +}; + +class TestProcessDiceHeaderDelegate : public ProcessDiceHeaderDelegate { + public: + explicit TestProcessDiceHeaderDelegate(DiceResponseHandlerTest* owner) + : owner_(owner) {} + + ~TestProcessDiceHeaderDelegate() override = default; + + // Called after the refresh token was fetched and added in the token service. + void HandleTokenExchangeSuccess(CoreAccountId account_id, + bool is_new_account) override { + owner_->HandleTokenExchangeSuccess(account_id, is_new_account); + } + + // Called after the refresh token was fetched and added in the token service. + void EnableSync(const CoreAccountId& account_id) override { + owner_->EnableSync(account_id); + } + + void HandleTokenExchangeFailure( + const std::string& email, + const GoogleServiceAuthError& error) override { + owner_->HandleTokenExchangeFailure(email, error); + } + + private: + raw_ptr owner_; +}; + +// Checks that a SIGNIN action triggers a token exchange request. +TEST_F(DiceResponseHandlerTest, Signin) { + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN); + const auto& account_info = dice_params.signin_info->account_info; + CoreAccountId account_id = identity_manager()->PickAccountIdForAccount( + account_info.gaia_id, account_info.email); + EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id)); + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique(this)); + // Check that a GaiaAuthFetcher has been created. + GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer, testing::NotNull()); + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(0, reconcilor_unblocked_count_); + // Simulate GaiaAuthFetcher success. + consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult( + "refresh_token", "access_token", 10, false /* is_child_account */, + true /* is_advanced_protection*/)); + // Check that the token has been inserted in the token service. + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id)); + EXPECT_TRUE(auth_error_email_.empty()); + EXPECT_EQ(GoogleServiceAuthError::NONE, auth_error_.state()); + // Check HandleTokenExchangeSuccess parameters. + EXPECT_EQ(token_exchange_account_id_, account_id); + EXPECT_TRUE(token_exchange_is_new_account_); + // Check that the reconcilor was blocked and unblocked exactly once. + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(1, reconcilor_unblocked_count_); + // Check that the AccountInfo::is_under_advanced_protection is set. + EXPECT_TRUE(identity_manager() + ->FindExtendedAccountInfoByAccountId(account_id) + .is_under_advanced_protection); +} + +// Checks that the account reconcilor is blocked when where was OAuth +// outage in Dice, and unblocked after the timeout. +TEST_F(DiceResponseHandlerTest, SupportOAuthOutageInDice) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndEnableFeature(kSupportOAuthOutageInDice); + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN); + dice_params.signin_info->authorization_code.clear(); + dice_params.signin_info->no_authorization_code = true; + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique(this)); + // Check that the reconcilor was blocked and not unblocked before timeout. + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(0, reconcilor_unblocked_count_); + task_environment_.FastForwardBy( + base::Hours(kLockAccountReconcilorTimeoutHours + 1)); + // Check that the reconcilor was unblocked. + EXPECT_EQ(1, reconcilor_unblocked_count_); + EXPECT_EQ(1, reconcilor_blocked_count_); +} + +// Check that after receiving two headers with no authorization code, +// timeout still restarts. +TEST_F(DiceResponseHandlerTest, CheckTimersDuringOutageinDice) { + ASSERT_GT(kLockAccountReconcilorTimeoutHours, 3); + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndEnableFeature(kSupportOAuthOutageInDice); + // Create params for the first header with no authorization code. + DiceResponseParams dice_params_1 = MakeDiceParams(DiceAction::SIGNIN); + dice_params_1.signin_info->authorization_code.clear(); + dice_params_1.signin_info->no_authorization_code = true; + dice_response_handler_->ProcessDiceHeader( + dice_params_1, std::make_unique(this)); + // Check that the reconcilor was blocked and not unblocked before timeout. + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(0, reconcilor_unblocked_count_); + // Wait half of the timeout. + task_environment_.FastForwardBy( + base::Hours(kLockAccountReconcilorTimeoutHours / 2)); + // Create params for the second header with no authorization code. + DiceResponseParams dice_params_2 = MakeDiceParams(DiceAction::SIGNIN); + dice_params_2.signin_info->authorization_code.clear(); + dice_params_2.signin_info->no_authorization_code = true; + dice_response_handler_->ProcessDiceHeader( + dice_params_2, std::make_unique(this)); + task_environment_.FastForwardBy( + base::Hours((kLockAccountReconcilorTimeoutHours + 1) / 2 + 1)); + // Check that the reconcilor was not unblocked after the first timeout + // passed, timer should be restarted after getting the second header. + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(0, reconcilor_unblocked_count_); + task_environment_.FastForwardBy( + base::Hours((kLockAccountReconcilorTimeoutHours + 1) / 2)); + // Check that the reconcilor was unblocked. + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(1, reconcilor_unblocked_count_); +} + +// Check that signin works normally (the token is fetched and added to chrome) +// on valid headers after getting a no_authorization_code header. +TEST_F(DiceResponseHandlerTest, CheckSigninAfterOutageInDice) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndEnableFeature(kSupportOAuthOutageInDice); + // Create params for the header with no authorization code. + DiceResponseParams dice_params_1 = MakeDiceParams(DiceAction::SIGNIN); + dice_params_1.signin_info->authorization_code.clear(); + dice_params_1.signin_info->no_authorization_code = true; + dice_response_handler_->ProcessDiceHeader( + dice_params_1, std::make_unique(this)); + // Create params for the valid header with an authorization code. + DiceResponseParams dice_params_2 = MakeDiceParams(DiceAction::SIGNIN); + const auto& account_info_2 = dice_params_2.signin_info->account_info; + CoreAccountId account_id_2 = identity_manager()->PickAccountIdForAccount( + account_info_2.gaia_id, account_info_2.email); + EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_2)); + dice_response_handler_->ProcessDiceHeader( + dice_params_2, std::make_unique(this)); + // Check that the reconcilor was blocked and not unblocked before timeout. + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(0, reconcilor_unblocked_count_); + // Check that a GaiaAuthFetcher has been created. + GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer, testing::NotNull()); + // Simulate GaiaAuthFetcher success. + consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult( + "refresh_token", "access_token", 10, false /* is_child_account */, + true /* is_advanced_protection*/)); + // Check that the token has been inserted in the token service. + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id_2)); + EXPECT_TRUE(auth_error_email_.empty()); + EXPECT_EQ(GoogleServiceAuthError::NONE, auth_error_.state()); + // Check HandleTokenExchangeSuccess parameters. + EXPECT_EQ(token_exchange_account_id_, account_id_2); + EXPECT_TRUE(token_exchange_is_new_account_); + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(0, reconcilor_unblocked_count_); + // Check that the AccountInfo::is_under_advanced_protection is set. + EXPECT_TRUE(identity_manager() + ->FindExtendedAccountInfoByAccountId(account_id_2) + .is_under_advanced_protection); + task_environment_.FastForwardBy( + base::Hours(kLockAccountReconcilorTimeoutHours + 1)); + // Check that the reconcilor was unblocked. + EXPECT_EQ(1, reconcilor_unblocked_count_); + EXPECT_EQ(1, reconcilor_blocked_count_); +} + +// Checks that a SIGNIN action triggers a token exchange request when the +// account is in authentication error. +TEST_F(DiceResponseHandlerTest, Reauth) { + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN); + AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable( + dice_params.signin_info->account_info.email, signin::ConsentLevel::kSync); + dice_params.signin_info->account_info.gaia_id = account_info.gaia; + CoreAccountId account_id = account_info.account_id; + identity_test_env_.UpdatePersistentErrorOfRefreshTokenForAccount( + account_id, + GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id)); + EXPECT_TRUE( + identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState( + account_id)); + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique(this)); + // Check that a GaiaAuthFetcher has been created. + GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer, testing::NotNull()); + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(0, reconcilor_unblocked_count_); + // Simulate GaiaAuthFetcher success. + consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult( + "refresh_token", "access_token", 10, false /* is_child_account */, + true /* is_advanced_protection*/)); + // Check that the token has been inserted in the token service. + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id)); + EXPECT_FALSE( + identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState( + account_id)); + // Check HandleTokenExchangeSuccess parameters. + EXPECT_EQ(token_exchange_account_id_, account_id); + EXPECT_FALSE(token_exchange_is_new_account_); +} + +// Checks that a GaiaAuthFetcher failure is handled correctly. +TEST_F(DiceResponseHandlerTest, SigninFailure) { + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN); + const auto& account_info = dice_params.signin_info->account_info; + CoreAccountId account_id = identity_manager()->PickAccountIdForAccount( + account_info.gaia_id, account_info.email); + EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id)); + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique(this)); + // Check that a GaiaAuthFetcher has been created. + GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer, testing::NotNull()); + EXPECT_EQ( + 1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting()); + // Simulate GaiaAuthFetcher failure. + GoogleServiceAuthError::State error_state = + GoogleServiceAuthError::SERVICE_UNAVAILABLE; + consumer->OnClientOAuthFailure(GoogleServiceAuthError(error_state)); + EXPECT_EQ( + 0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting()); + // Check that the token has not been inserted in the token service. + EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id)); + EXPECT_EQ(account_info.email, auth_error_email_); + EXPECT_EQ(error_state, auth_error_.state()); +} + +// Checks that a second token for the same account is not requested when a +// request is already in flight. +TEST_F(DiceResponseHandlerTest, SigninRepeatedWithSameAccount) { + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN); + const auto& account_info = dice_params.signin_info->account_info; + CoreAccountId account_id = identity_manager()->PickAccountIdForAccount( + account_info.gaia_id, account_info.email); + ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id)); + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique(this)); + // Check that a GaiaAuthFetcher has been created. + GaiaAuthConsumer* consumer_1 = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer_1, testing::NotNull()); + // Start a second request for the same account. + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique(this)); + // Check that there is no new request. + GaiaAuthConsumer* consumer_2 = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer_2, testing::IsNull()); + // Simulate GaiaAuthFetcher success for the first request. + consumer_1->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult( + "refresh_token", "access_token", 10, false /* is_child_account */, + false /* is_advanced_protection*/)); + // Check that the token has been inserted in the token service. + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id)); + EXPECT_FALSE(identity_manager() + ->FindExtendedAccountInfoByAccountId(account_id) + .is_under_advanced_protection); +} + +// Checks that two SIGNIN requests can happen concurrently. +TEST_F(DiceResponseHandlerTest, SigninWithTwoAccounts) { + DiceResponseParams dice_params_1 = MakeDiceParams(DiceAction::SIGNIN); + const auto& account_info_1 = dice_params_1.signin_info->account_info; + DiceResponseParams dice_params_2 = MakeDiceParams(DiceAction::SIGNIN); + dice_params_2.signin_info->account_info.email = "other_email"; + dice_params_2.signin_info->account_info.gaia_id = "other_gaia_id"; + const auto& account_info_2 = dice_params_2.signin_info->account_info; + CoreAccountId account_id_1 = identity_manager()->PickAccountIdForAccount( + account_info_1.gaia_id, account_info_1.email); + CoreAccountId account_id_2 = identity_manager()->PickAccountIdForAccount( + account_info_2.gaia_id, account_info_2.email); + ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_1)); + ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_2)); + // Start first request. + dice_response_handler_->ProcessDiceHeader( + dice_params_1, std::make_unique(this)); + // Check that a GaiaAuthFetcher has been created. + GaiaAuthConsumer* consumer_1 = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer_1, testing::NotNull()); + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(0, reconcilor_unblocked_count_); + // Start second request. + dice_response_handler_->ProcessDiceHeader( + dice_params_2, std::make_unique(this)); + GaiaAuthConsumer* consumer_2 = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer_2, testing::NotNull()); + // Simulate GaiaAuthFetcher success for the first request. + consumer_1->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult( + "refresh_token", "access_token", 10, false /* is_child_account */, + true /* is_advanced_protection*/)); + // Check that the token has been inserted in the token service. + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id_1)); + EXPECT_TRUE(identity_manager() + ->FindExtendedAccountInfoByAccountId(account_id_1) + .is_under_advanced_protection); + // Simulate GaiaAuthFetcher success for the second request. + consumer_2->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult( + "refresh_token", "access_token", 10, false /* is_child_account */, + false /* is_advanced_protection*/)); + // Check that the token has been inserted in the token service. + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id_2)); + EXPECT_FALSE(identity_manager() + ->FindExtendedAccountInfoByAccountId(account_id_2) + .is_under_advanced_protection); + // Check that the reconcilor was blocked and unblocked exactly once. + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(1, reconcilor_unblocked_count_); +} + +// Checks that a ENABLE_SYNC action received after the refresh token is added +// to the token service, triggers a call to enable sync on the delegate. +TEST_F(DiceResponseHandlerTest, SigninEnableSyncAfterRefreshTokenFetched) { + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN); + const auto& account_info = dice_params.signin_info->account_info; + CoreAccountId account_id = identity_manager()->PickAccountIdForAccount( + account_info.gaia_id, account_info.email); + ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id)); + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique(this)); + // Check that a GaiaAuthFetcher has been created. + GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer, testing::NotNull()); + // Simulate GaiaAuthFetcher success. + consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult( + "refresh_token", "access_token", 10, false /* is_child_account */, + false /* is_advanced_protection*/)); + // Check that the token has been inserted in the token service. + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id)); + // Check HandleTokenExchangeSuccess parameters. + EXPECT_EQ(token_exchange_account_id_, account_id); + EXPECT_TRUE(token_exchange_is_new_account_); + // Check that delegate was not called to enable sync. + EXPECT_TRUE(enable_sync_account_id_.empty()); + + // Enable sync. + dice_response_handler_->ProcessDiceHeader( + MakeDiceParams(DiceAction::ENABLE_SYNC), + std::make_unique(this)); + // Check that delegate was called to enable sync. + EXPECT_EQ(account_id, enable_sync_account_id_); +} + +// Checks that a ENABLE_SYNC action received before the refresh token is added +// to the token service, is schedules a call to enable sync on the delegate +// once the refresh token is received. +TEST_F(DiceResponseHandlerTest, SigninEnableSyncBeforeRefreshTokenFetched) { + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN); + const auto& account_info = dice_params.signin_info->account_info; + CoreAccountId account_id = identity_manager()->PickAccountIdForAccount( + account_info.gaia_id, account_info.email); + ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id)); + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique(this)); + // Check that a GaiaAuthFetcher has been created. + GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer, testing::NotNull()); + + // Enable sync. + dice_response_handler_->ProcessDiceHeader( + MakeDiceParams(DiceAction::ENABLE_SYNC), + std::make_unique(this)); + // Check that delegate was not called to enable sync. + EXPECT_TRUE(enable_sync_account_id_.empty()); + + // Simulate GaiaAuthFetcher success. + consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult( + "refresh_token", "access_token", 10, false /* is_child_account */, + false /* is_advanced_protection*/)); + // Check that the token has been inserted in the token service. + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id)); + // Check HandleTokenExchangeSuccess parameters. + EXPECT_EQ(token_exchange_account_id_, account_id); + EXPECT_TRUE(token_exchange_is_new_account_); + // Check that delegate was called to enable sync. + EXPECT_EQ(account_id, enable_sync_account_id_); +} + +TEST_F(DiceResponseHandlerTest, Timeout) { + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN); + const auto& account_info = dice_params.signin_info->account_info; + CoreAccountId account_id = identity_manager()->PickAccountIdForAccount( + account_info.gaia_id, account_info.email); + ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id)); + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique(this)); + // Check that a GaiaAuthFetcher has been created. + GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer, testing::NotNull()); + EXPECT_EQ( + 1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting()); + // Force a timeout. + task_environment_.FastForwardBy( + base::Seconds(kDiceTokenFetchTimeoutSeconds + 1)); + EXPECT_EQ( + 0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting()); + // Check that the token has not been inserted in the token service. + EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id)); + // Check that the reconcilor was blocked and unblocked exactly once. + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(1, reconcilor_unblocked_count_); +} + +TEST_F(DiceResponseHandlerTest, SignoutMainAccount) { + const char kSecondaryEmail[] = "other@gmail.com"; + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNOUT); + const auto& dice_account_info = dice_params.signout_info->account_infos[0]; + // User is signed in to Chrome, and has some refresh token for a secondary + // account. + AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable( + dice_account_info.email, signin::ConsentLevel::kSync); + AccountInfo secondary_account_info = + identity_test_env_.MakeAccountAvailable(kSecondaryEmail); + EXPECT_TRUE( + identity_manager()->HasAccountWithRefreshToken(account_info.account_id)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken( + secondary_account_info.account_id)); + EXPECT_TRUE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); + // Receive signout response for the main account. + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique(this)); + + // User is not signed out, token for the main account is now invalid, + // secondary account is untouched. + EXPECT_TRUE( + identity_manager()->HasAccountWithRefreshToken(account_info.account_id)); + EXPECT_TRUE( + identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState( + account_info.account_id)); + auto error = identity_manager()->GetErrorStateOfRefreshTokenForAccount( + account_info.account_id); + EXPECT_EQ(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS, error.state()); + EXPECT_EQ(GoogleServiceAuthError::InvalidGaiaCredentialsReason:: + CREDENTIALS_REJECTED_BY_CLIENT, + error.GetInvalidGaiaCredentialsReason()); + + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken( + secondary_account_info.account_id)); + EXPECT_FALSE( + identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState( + secondary_account_info.account_id)); + + EXPECT_TRUE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); + // Check that the reconcilor was not blocked. + EXPECT_EQ(0, reconcilor_blocked_count_); + EXPECT_EQ(0, reconcilor_unblocked_count_); +} + +TEST_F(DiceResponseHandlerTest, SignoutSecondaryAccount) { + const char kMainEmail[] = "main@gmail.com"; + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNOUT); + const auto& secondary_dice_account_info = + dice_params.signout_info->account_infos[0]; + // User is signed in to Chrome, and has some refresh token for a secondary + // account. + AccountInfo main_account_info = + identity_test_env_.MakePrimaryAccountAvailable( + kMainEmail, signin::ConsentLevel::kSync); + AccountInfo secondary_account_info = identity_test_env_.MakeAccountAvailable( + secondary_dice_account_info.email); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken( + secondary_account_info.account_id)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken( + main_account_info.account_id)); + EXPECT_TRUE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); + // Receive signout response for the secondary account. + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique(this)); + + // Only the token corresponding the the Dice parameter has been removed, and + // the user is still signed in. + EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken( + secondary_account_info.account_id)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken( + main_account_info.account_id)); + EXPECT_TRUE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); +} + +TEST_F(DiceResponseHandlerTest, SignoutWebOnly) { + const char kSecondaryEmail[] = "other@gmail.com"; + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNOUT); + const auto& dice_account_info = dice_params.signout_info->account_infos[0]; + // User is NOT signed in to Chrome, and has some refresh tokens for two + // accounts. + AccountInfo account_info = + identity_test_env_.MakeAccountAvailable(dice_account_info.email); + AccountInfo secondary_account_info = + identity_test_env_.MakeAccountAvailable(kSecondaryEmail); + EXPECT_TRUE( + identity_manager()->HasAccountWithRefreshToken(account_info.account_id)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken( + secondary_account_info.account_id)); + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); + // Receive signout response. + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique(this)); + // Only the token corresponding the the Dice parameter has been removed. + EXPECT_FALSE( + identity_manager()->HasAccountWithRefreshToken(account_info.account_id)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken( + secondary_account_info.account_id)); + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); +} + +// Checks that signin in progress is canceled by a signout. +TEST_F(DiceResponseHandlerTest, SigninSignoutSameAccount) { + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNOUT); + const auto& dice_account_info = dice_params.signout_info->account_infos[0]; + + // User is signed in to Chrome. + AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable( + dice_account_info.email, signin::ConsentLevel::kSync); + EXPECT_TRUE( + identity_manager()->HasAccountWithRefreshToken(account_info.account_id)); + EXPECT_FALSE( + identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState( + account_info.account_id)); + // Start Dice signin (reauth). + DiceResponseParams dice_params_2 = MakeDiceParams(DiceAction::SIGNIN); + dice_response_handler_->ProcessDiceHeader( + dice_params_2, std::make_unique(this)); + // Check that a GaiaAuthFetcher has been created and is pending. + ASSERT_THAT(signin_client_.GetAndClearConsumer(), testing::NotNull()); + EXPECT_EQ( + 1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting()); + // Signout while signin is in flight. + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique(this)); + // Check that the token fetcher has been canceled and the token is invalid. + EXPECT_EQ( + 0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting()); + EXPECT_TRUE( + identity_manager()->HasAccountWithRefreshToken(account_info.account_id)); + EXPECT_TRUE( + identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState( + account_info.account_id)); + auto error = identity_manager()->GetErrorStateOfRefreshTokenForAccount( + account_info.account_id); + EXPECT_EQ(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS, error.state()); + EXPECT_EQ(GoogleServiceAuthError::InvalidGaiaCredentialsReason:: + CREDENTIALS_REJECTED_BY_CLIENT, + error.GetInvalidGaiaCredentialsReason()); +} + +// Checks that signin in progress is not canceled by a signout for a different +// account. +TEST_F(DiceResponseHandlerTest, SigninSignoutDifferentAccount) { + // User starts signin in the web with two accounts. + DiceResponseParams signout_params_1 = MakeDiceParams(DiceAction::SIGNOUT); + DiceResponseParams signin_params_1 = MakeDiceParams(DiceAction::SIGNIN); + DiceResponseParams signin_params_2 = MakeDiceParams(DiceAction::SIGNIN); + signin_params_2.signin_info->account_info.email = "other_email"; + signin_params_2.signin_info->account_info.gaia_id = "other_gaia_id"; + const auto& signin_account_info_1 = signin_params_1.signin_info->account_info; + const auto& signin_account_info_2 = signin_params_2.signin_info->account_info; + CoreAccountId account_id_1 = identity_manager()->PickAccountIdForAccount( + signin_account_info_1.gaia_id, signin_account_info_1.email); + CoreAccountId account_id_2 = identity_manager()->PickAccountIdForAccount( + signin_account_info_2.gaia_id, signin_account_info_2.email); + dice_response_handler_->ProcessDiceHeader( + signin_params_1, std::make_unique(this)); + + GaiaAuthConsumer* consumer_1 = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer_1, testing::NotNull()); + dice_response_handler_->ProcessDiceHeader( + signin_params_2, std::make_unique(this)); + GaiaAuthConsumer* consumer_2 = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer_2, testing::NotNull()); + EXPECT_EQ( + 2u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting()); + ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_1)); + ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_2)); + ASSERT_FALSE( + identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState( + account_id_1)); + ASSERT_FALSE( + identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState( + account_id_2)); + // Signout from one of the accounts while signin is in flight. + dice_response_handler_->ProcessDiceHeader( + signout_params_1, std::make_unique(this)); + // Check that one of the fetchers is cancelled. + EXPECT_EQ( + 1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting()); + // Allow the remaining fetcher to complete. + consumer_2->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult( + "refresh_token", "access_token", 10, false /* is_child_account */, + false /* is_advanced_protection*/)); + EXPECT_EQ( + 0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting()); + // Check that the right token is available. + EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_1)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id_2)); + EXPECT_FALSE( + identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState( + account_id_2)); +} + +// Tests that the DiceResponseHandler is created for a normal profile but not +// for off-the-record profiles. +TEST(DiceResponseHandlerFactoryTest, NotInOffTheRecord) { + content::BrowserTaskEnvironment task_environment; + TestingProfile profile; + EXPECT_THAT(DiceResponseHandler::GetForProfile(&profile), testing::NotNull()); + EXPECT_THAT(DiceResponseHandler::GetForProfile( + profile.GetPrimaryOTRProfile(/*create_if_needed=*/true)), + testing::IsNull()); + EXPECT_THAT(DiceResponseHandler::GetForProfile(profile.GetOffTheRecordProfile( + Profile::OTRProfileID::CreateUniqueForTesting(), + /*create_if_needed=*/true)), + testing::IsNull()); +} + +} // namespace diff --git a/chromium/chrome/browser/signin/dice_signed_in_profile_creator.cc b/chromium/chrome/browser/signin/dice_signed_in_profile_creator.cc new file mode 100644 index 00000000000..494a17b4695 --- /dev/null +++ b/chromium/chrome/browser/signin/dice_signed_in_profile_creator.cc @@ -0,0 +1,211 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/dice_signed_in_profile_creator.h" + +#include + +#include "base/check.h" +#include "base/location.h" +#include "base/memory/ptr_util.h" +#include "base/memory/raw_ptr.h" +#include "base/scoped_observation.h" +#include "base/threading/thread_task_runner_handle.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/profiles/profile_avatar_icon_util.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "components/signin/public/identity_manager/accounts_mutator.h" +#include "components/signin/public/identity_manager/identity_manager.h" + +// Waits until the tokens are loaded and calls the callback. The callback is +// called immediately if the tokens are already loaded, and called with nullptr +// if the profile is destroyed before the tokens are loaded. +class TokensLoadedCallbackRunner : public signin::IdentityManager::Observer { + public: + ~TokensLoadedCallbackRunner() override = default; + TokensLoadedCallbackRunner(const TokensLoadedCallbackRunner&) = delete; + TokensLoadedCallbackRunner& operator=(const TokensLoadedCallbackRunner&) = + delete; + + // Runs the callback when the tokens are loaded. If tokens are already loaded + // the callback is called synchronously and this returns nullptr. + static std::unique_ptr RunWhenLoaded( + Profile* profile, + base::OnceCallback callback); + + private: + TokensLoadedCallbackRunner(Profile* profile, + base::OnceCallback callback); + + // signin::IdentityManager::Observer implementation: + void OnRefreshTokensLoaded() override { + scoped_identity_manager_observer_.Reset(); + std::move(callback_).Run(profile_.get()); + } + + void OnIdentityManagerShutdown(signin::IdentityManager* manager) override { + scoped_identity_manager_observer_.Reset(); + std::move(callback_).Run(nullptr); + } + + raw_ptr profile_; + raw_ptr identity_manager_; + base::ScopedObservation + scoped_identity_manager_observer_{this}; + base::OnceCallback callback_; +}; + +// static +std::unique_ptr +TokensLoadedCallbackRunner::RunWhenLoaded( + Profile* profile, + base::OnceCallback callback) { + if (IdentityManagerFactory::GetForProfile(profile) + ->AreRefreshTokensLoaded()) { + std::move(callback).Run(profile); + return nullptr; + } + + return base::WrapUnique( + new TokensLoadedCallbackRunner(profile, std::move(callback))); +} + +TokensLoadedCallbackRunner::TokensLoadedCallbackRunner( + Profile* profile, + base::OnceCallback callback) + : profile_(profile), + identity_manager_(IdentityManagerFactory::GetForProfile(profile)), + callback_(std::move(callback)) { + DCHECK(profile_); + DCHECK(identity_manager_); + DCHECK(callback_); + DCHECK(!identity_manager_->AreRefreshTokensLoaded()); + scoped_identity_manager_observer_.Observe(identity_manager_.get()); +} + +DiceSignedInProfileCreator::DiceSignedInProfileCreator( + Profile* source_profile, + CoreAccountId account_id, + const std::u16string& local_profile_name, + absl::optional icon_index, + bool use_guest_profile, + base::OnceCallback callback) + : source_profile_(source_profile), + account_id_(account_id), + callback_(std::move(callback)) { + // Passing the sign-in token to an ephemeral Guest profile is part of the + // experiment to surface a Guest mode link in the DiceWebSigninIntercept + // and is only used to sign in to the web through account consistency and + // does NOT enable sync or any other browser level functionality. + // TODO(https://crbug.com/1225171): Revise the comment after Guest mode plans + // are finalized. + if (use_guest_profile) { + // TODO(https://crbug.com/1225171): Re-enabled if ephemeral based Guest mode + // is added. Remove the code otherwise. + NOTREACHED(); + + // Make sure the callback is not called synchronously. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(&ProfileManager::CreateProfileAsync, + base::Unretained(g_browser_process->profile_manager()), + ProfileManager::GetGuestProfilePath(), + base::BindRepeating( + &DiceSignedInProfileCreator::OnNewProfileCreated, + weak_pointer_factory_.GetWeakPtr()))); + } else { + ProfileAttributesStorage& storage = + g_browser_process->profile_manager()->GetProfileAttributesStorage(); + if (!icon_index.has_value()) + icon_index = storage.ChooseAvatarIconIndexForNewProfile(); + std::u16string name = local_profile_name.empty() + ? storage.ChooseNameForNewProfile(*icon_index) + : local_profile_name; + ProfileManager::CreateMultiProfileAsync( + name, *icon_index, /*is_hidden=*/false, + base::BindRepeating(&DiceSignedInProfileCreator::OnNewProfileCreated, + weak_pointer_factory_.GetWeakPtr())); + } +} + +DiceSignedInProfileCreator::DiceSignedInProfileCreator( + Profile* source_profile, + CoreAccountId account_id, + const base::FilePath& target_profile_path, + base::OnceCallback callback) + : source_profile_(source_profile), + account_id_(account_id), + callback_(std::move(callback)) { + // Make sure the callback is not called synchronously. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce( + base::IgnoreResult(&ProfileManager::LoadProfileByPath), + base::Unretained(g_browser_process->profile_manager()), + target_profile_path, /*incognito=*/false, + base::BindOnce(&DiceSignedInProfileCreator::OnNewProfileInitialized, + weak_pointer_factory_.GetWeakPtr()))); +} + +DiceSignedInProfileCreator::~DiceSignedInProfileCreator() = default; + +void DiceSignedInProfileCreator::OnNewProfileCreated( + Profile* new_profile, + Profile::CreateStatus status) { + switch (status) { + case Profile::CREATE_STATUS_CREATED: + // Ignore this, wait for profile to be initialized. + return; + case Profile::CREATE_STATUS_INITIALIZED: + OnNewProfileInitialized(new_profile); + return; + case Profile::CREATE_STATUS_LOCAL_FAIL: + NOTREACHED() << "Error creating new profile"; + if (callback_) + std::move(callback_).Run(nullptr); + return; + } +} + +void DiceSignedInProfileCreator::OnNewProfileInitialized(Profile* new_profile) { + if (!new_profile) { + if (callback_) + std::move(callback_).Run(nullptr); + return; + } + + DCHECK(!tokens_loaded_callback_runner_); + // base::Unretained is fine because the runner is owned by this. + auto tokens_loaded_callback_runner = + TokensLoadedCallbackRunner::RunWhenLoaded( + new_profile, + base::BindOnce(&DiceSignedInProfileCreator::OnNewProfileTokensLoaded, + base::Unretained(this))); + // If the callback was called synchronously, |this| may have been deleted. + if (tokens_loaded_callback_runner) { + tokens_loaded_callback_runner_ = std::move(tokens_loaded_callback_runner); + } +} + +void DiceSignedInProfileCreator::OnNewProfileTokensLoaded( + Profile* new_profile) { + tokens_loaded_callback_runner_.reset(); + if (!new_profile) { + if (callback_) + std::move(callback_).Run(nullptr); + return; + } + + auto* accounts_mutator = + IdentityManagerFactory::GetForProfile(source_profile_) + ->GetAccountsMutator(); + auto* new_profile_accounts_mutator = + IdentityManagerFactory::GetForProfile(new_profile)->GetAccountsMutator(); + accounts_mutator->MoveAccount(new_profile_accounts_mutator, account_id_); + if (callback_) + std::move(callback_).Run(new_profile); +} diff --git a/chromium/chrome/browser/signin/dice_signed_in_profile_creator.h b/chromium/chrome/browser/signin/dice_signed_in_profile_creator.h new file mode 100644 index 00000000000..97bb462da96 --- /dev/null +++ b/chromium/chrome/browser/signin/dice_signed_in_profile_creator.h @@ -0,0 +1,70 @@ +// Copyright 2020 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 CHROME_BROWSER_SIGNIN_DICE_SIGNED_IN_PROFILE_CREATOR_H_ +#define CHROME_BROWSER_SIGNIN_DICE_SIGNED_IN_PROFILE_CREATOR_H_ + +#include + +#include "base/callback.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "chrome/browser/profiles/profile.h" +#include "google_apis/gaia/core_account_id.h" +#include "third_party/abseil-cpp/absl/types/optional.h" + +class TokensLoadedCallbackRunner; + +// Extracts an account from an existing profile and moves it to a new profile. +class DiceSignedInProfileCreator { + public: + // Creates a new profile or uses Guest profile if |use_guest_profile|, and + // moves the account from source_profile to it. + // The callback is called with the new profile or nullptr in case of failure. + // The callback is never called synchronously. + // If |local_profile_name| is not empty, it will be set as local name for the + // new profile. + // If |icon_index| is nullopt, a random icon will be selected. + DiceSignedInProfileCreator(Profile* source_profile, + CoreAccountId account_id, + const std::u16string& local_profile_name, + absl::optional icon_index, + bool use_guest_profile, + base::OnceCallback callback); + + // Uses this version when the profile already exists at `target_profile_path` + // but may not be loaded in memory. The profile is loaded if necessary, and + // the account is moved. + DiceSignedInProfileCreator(Profile* source_profile, + CoreAccountId account_id, + const base::FilePath& target_profile_path, + base::OnceCallback callback); + + ~DiceSignedInProfileCreator(); + + DiceSignedInProfileCreator(const DiceSignedInProfileCreator&) = delete; + DiceSignedInProfileCreator& operator=(const DiceSignedInProfileCreator&) = + delete; + + private: + // Callback invoked once a profile is created, so we can transfer the + // credentials. + void OnNewProfileCreated(Profile* new_profile, Profile::CreateStatus status); + + // Called when the profile is initialized. + void OnNewProfileInitialized(Profile* new_profile); + + // Callback invoked once the token service is ready for the new profile. + void OnNewProfileTokensLoaded(Profile* new_profile); + + const raw_ptr source_profile_; + const CoreAccountId account_id_; + + base::OnceCallback callback_; + std::unique_ptr tokens_loaded_callback_runner_; + + base::WeakPtrFactory weak_pointer_factory_{this}; +}; + +#endif // CHROME_BROWSER_SIGNIN_DICE_SIGNED_IN_PROFILE_CREATOR_H_ diff --git a/chromium/chrome/browser/signin/dice_signed_in_profile_creator_unittest.cc b/chromium/chrome/browser/signin/dice_signed_in_profile_creator_unittest.cc new file mode 100644 index 00000000000..b0d813a17ec --- /dev/null +++ b/chromium/chrome/browser/signin/dice_signed_in_profile_creator_unittest.cc @@ -0,0 +1,285 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/dice_signed_in_profile_creator.h" + +#include "base/bind.h" +#include "base/callback.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/memory/raw_ptr.h" +#include "base/run_loop.h" +#include "base/strings/utf_string_conversions.h" +#include "base/threading/thread_task_runner_handle.h" +#include "chrome/browser/profiles/profile_attributes_entry.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/profiles/profile_avatar_icon_util.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/profiles/profile_manager_observer.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h" +#include "chrome/test/base/fake_profile_manager.h" +#include "chrome/test/base/scoped_testing_local_state.h" +#include "chrome/test/base/testing_browser_process.h" +#include "chrome/test/base/testing_profile.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +const char16_t kProfileTestName[] = u"profile_test_name"; + +std::unique_ptr BuildTestingProfile(const base::FilePath& path, + Profile::Delegate* delegate, + bool tokens_loaded) { + TestingProfile::Builder profile_builder; + profile_builder.SetDelegate(delegate); + profile_builder.SetPath(path); + std::unique_ptr profile = + IdentityTestEnvironmentProfileAdaptor:: + CreateProfileForIdentityTestEnvironment(profile_builder); + if (!tokens_loaded) { + IdentityTestEnvironmentProfileAdaptor adaptor(profile.get()); + adaptor.identity_test_env()->ResetToAccountsNotYetLoadedFromDiskState(); + } + if (profile->GetPath() == ProfileManager::GetGuestProfilePath()) + profile->SetGuestSession(true); + return profile; +} + +class UnittestProfileManager : public FakeProfileManager { + public: + explicit UnittestProfileManager(const base::FilePath& user_data_dir) + : FakeProfileManager(user_data_dir) {} + + void set_tokens_loaded_at_creation(bool loaded) { + tokens_loaded_at_creation_ = loaded; + } + + std::unique_ptr BuildTestingProfile( + const base::FilePath& path, + Profile::Delegate* delegate) override { + return ::BuildTestingProfile(path, delegate, tokens_loaded_at_creation_); + } + + bool tokens_loaded_at_creation_ = true; +}; + +} // namespace + +class DiceSignedInProfileCreatorTest : public testing::Test, + public ProfileManagerObserver { + public: + DiceSignedInProfileCreatorTest() + : local_state_(TestingBrowserProcess::GetGlobal()) { + EXPECT_TRUE(temp_dir_.CreateUniqueTempDir()); + auto profile_manager_unique = + std::make_unique(temp_dir_.GetPath()); + profile_manager_ = profile_manager_unique.get(); + TestingBrowserProcess::GetGlobal()->SetProfileManager( + std::move(profile_manager_unique)); + profile_ = BuildTestingProfile(base::FilePath(), /*delegate=*/nullptr, + /*tokens_loaded=*/true); + identity_test_env_profile_adaptor_ = + std::make_unique(profile()); + profile_manager()->AddObserver(this); + } + + ~DiceSignedInProfileCreatorTest() override { DeleteProfiles(); } + + UnittestProfileManager* profile_manager() { return profile_manager_; } + + // Test environment attached to profile(). + signin::IdentityTestEnvironment* identity_test_env() { + return identity_test_env_profile_adaptor_->identity_test_env(); + } + + // Source profile (the one which we are extracting credentials from). + Profile* profile() { return profile_.get(); } + + // Profile created by the DiceSignedInProfileCreator. + Profile* signed_in_profile() { return signed_in_profile_; } + + // Profile added to the ProfileManager. In general this should be the same as + // signed_in_profile() except in error cases. + Profile* added_profile() { return added_profile_; } + + bool creator_callback_called() { return creator_callback_called_; } + + void set_profile_added_closure(base::OnceClosure closure) { + profile_added_closure_ = std::move(closure); + } + + bool use_guest_profile() const { return use_guest_profile_; } + + void DeleteProfiles() { + identity_test_env_profile_adaptor_.reset(); + if (profile_manager_) { + profile_manager()->RemoveObserver(this); + TestingBrowserProcess::GetGlobal()->SetProfileManager(nullptr); + profile_manager_ = nullptr; + } + } + + // Callback for the DiceSignedInProfileCreator. + void OnProfileCreated(base::OnceClosure quit_closure, Profile* profile) { + creator_callback_called_ = true; + signed_in_profile_ = profile; + if (quit_closure) + std::move(quit_closure).Run(); + } + + // ProfileManagerObserver: + void OnProfileAdded(Profile* profile) override { + added_profile_ = profile; + if (profile_added_closure_) + std::move(profile_added_closure_).Run(); + } + + private: + content::BrowserTaskEnvironment task_environment_; + base::ScopedTempDir temp_dir_; + ScopedTestingLocalState local_state_; + raw_ptr profile_manager_ = nullptr; + std::unique_ptr + identity_test_env_profile_adaptor_; + std::unique_ptr profile_; + raw_ptr signed_in_profile_ = nullptr; + raw_ptr added_profile_ = nullptr; + base::OnceClosure profile_added_closure_; + bool creator_callback_called_ = false; + base::test::ScopedFeatureList scoped_feature_list_; + bool use_guest_profile_ = false; +}; + +TEST_F(DiceSignedInProfileCreatorTest, CreateWithTokensLoaded) { + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + size_t kTestIcon = profiles::GetModernAvatarIconStartIndex(); + + base::RunLoop loop; + std::unique_ptr creator = + std::make_unique( + profile(), account_info.account_id, kProfileTestName, kTestIcon, + use_guest_profile(), + base::BindOnce(&DiceSignedInProfileCreatorTest::OnProfileCreated, + base::Unretained(this), loop.QuitClosure())); + loop.Run(); + + // Check that the account was moved. + EXPECT_TRUE(creator_callback_called()); + EXPECT_TRUE(signed_in_profile()); + EXPECT_NE(profile(), signed_in_profile()); + EXPECT_EQ(signed_in_profile(), added_profile()); + EXPECT_FALSE(IdentityManagerFactory::GetForProfile(profile()) + ->HasAccountWithRefreshToken(account_info.account_id)); + EXPECT_EQ(1u, IdentityManagerFactory::GetForProfile(signed_in_profile()) + ->GetAccountsWithRefreshTokens() + .size()); + EXPECT_TRUE(IdentityManagerFactory::GetForProfile(signed_in_profile()) + ->HasAccountWithRefreshToken(account_info.account_id)); + + // Check profile type + ASSERT_EQ(use_guest_profile(), signed_in_profile()->IsGuestSession()); + + // Check the profile name and icon. + ProfileAttributesStorage& storage = + profile_manager()->GetProfileAttributesStorage(); + ProfileAttributesEntry* entry = + storage.GetProfileAttributesWithPath(signed_in_profile()->GetPath()); + ASSERT_TRUE(entry); + if (!use_guest_profile()) { + EXPECT_EQ(kProfileTestName, entry->GetLocalProfileName()); + EXPECT_EQ(kTestIcon, entry->GetAvatarIconIndex()); + } +} + +TEST_F(DiceSignedInProfileCreatorTest, CreateWithTokensNotLoaded) { + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + profile_manager()->set_tokens_loaded_at_creation(false); + + base::RunLoop creator_loop; + base::RunLoop profile_added_loop; + set_profile_added_closure(profile_added_loop.QuitClosure()); + std::unique_ptr creator = + std::make_unique( + profile(), account_info.account_id, std::u16string(), absl::nullopt, + use_guest_profile(), + base::BindOnce(&DiceSignedInProfileCreatorTest::OnProfileCreated, + base::Unretained(this), creator_loop.QuitClosure())); + profile_added_loop.Run(); + base::RunLoop().RunUntilIdle(); + + // The profile was created, but tokens not loaded. The callback has not been + // called yet. + EXPECT_FALSE(creator_callback_called()); + EXPECT_TRUE(added_profile()); + EXPECT_NE(profile(), added_profile()); + + // Load the tokens. + IdentityTestEnvironmentProfileAdaptor adaptor(added_profile()); + adaptor.identity_test_env()->ReloadAccountsFromDisk(); + creator_loop.Run(); + + // Check that the account was moved. + EXPECT_EQ(signed_in_profile(), added_profile()); + EXPECT_TRUE(creator_callback_called()); + EXPECT_FALSE(IdentityManagerFactory::GetForProfile(profile()) + ->HasAccountWithRefreshToken(account_info.account_id)); + EXPECT_EQ(1u, IdentityManagerFactory::GetForProfile(signed_in_profile()) + ->GetAccountsWithRefreshTokens() + .size()); + EXPECT_TRUE(IdentityManagerFactory::GetForProfile(signed_in_profile()) + ->HasAccountWithRefreshToken(account_info.account_id)); +} + +// Deleting the creator while it is running does not crash. +TEST_F(DiceSignedInProfileCreatorTest, DeleteWhileCreating) { + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + std::unique_ptr creator = + std::make_unique( + profile(), account_info.account_id, std::u16string(), absl::nullopt, + use_guest_profile(), + base::BindOnce(&DiceSignedInProfileCreatorTest::OnProfileCreated, + base::Unretained(this), base::OnceClosure())); + EXPECT_FALSE(creator_callback_called()); + creator.reset(); + base::RunLoop().RunUntilIdle(); +} + +// Deleting the profile while waiting for the tokens. +TEST_F(DiceSignedInProfileCreatorTest, DeleteProfile) { + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + profile_manager()->set_tokens_loaded_at_creation(false); + + base::RunLoop creator_loop; + base::RunLoop profile_added_loop; + set_profile_added_closure(profile_added_loop.QuitClosure()); + std::unique_ptr creator = + std::make_unique( + profile(), account_info.account_id, std::u16string(), absl::nullopt, + use_guest_profile(), + base::BindOnce(&DiceSignedInProfileCreatorTest::OnProfileCreated, + base::Unretained(this), creator_loop.QuitClosure())); + profile_added_loop.Run(); + base::RunLoop().RunUntilIdle(); + + // The profile was created, but tokens not loaded. The callback has not been + // called yet. + EXPECT_FALSE(creator_callback_called()); + EXPECT_TRUE(added_profile()); + EXPECT_NE(profile(), added_profile()); + + DeleteProfiles(); + creator_loop.Run(); + + // The callback is called with nullptr profile. + EXPECT_TRUE(creator_callback_called()); + EXPECT_FALSE(signed_in_profile()); +} diff --git a/chromium/chrome/browser/signin/dice_tab_helper.cc b/chromium/chrome/browser/signin/dice_tab_helper.cc new file mode 100644 index 00000000000..4e538d31a98 --- /dev/null +++ b/chromium/chrome/browser/signin/dice_tab_helper.cc @@ -0,0 +1,117 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/dice_tab_helper.h" + +#include "base/check_op.h" +#include "base/metrics/user_metrics.h" +#include "chrome/browser/signin/dice_tab_helper.h" +#include "chrome/browser/signin/signin_util.h" +#include "chrome/browser/ui/browser_finder.h" +#include "content/public/browser/navigation_controller.h" +#include "content/public/browser/navigation_entry.h" +#include "content/public/browser/navigation_handle.h" +#include "google_apis/gaia/gaia_urls.h" + +DiceTabHelper::DiceTabHelper(content::WebContents* web_contents) + : content::WebContentsUserData(*web_contents), + content::WebContentsObserver(web_contents) {} + +DiceTabHelper::~DiceTabHelper() = default; + +void DiceTabHelper::InitializeSigninFlow( + const GURL& signin_url, + signin_metrics::AccessPoint access_point, + signin_metrics::Reason reason, + signin_metrics::PromoAction promo_action, + const GURL& redirect_url) { + DCHECK(signin_url.is_valid()); + DCHECK(signin_url_.is_empty() || signin_url_ == signin_url); + + signin_url_ = signin_url; + signin_access_point_ = access_point; + signin_reason_ = reason; + signin_promo_action_ = promo_action; + is_chrome_signin_page_ = true; + signin_page_load_recorded_ = false; + redirect_url_ = redirect_url; + sync_signin_flow_status_ = SyncSigninFlowStatus::kNotStarted; + + if (reason == signin_metrics::Reason::kSigninPrimaryAccount) { + sync_signin_flow_status_ = SyncSigninFlowStatus::kStarted; + signin_metrics::LogSigninAccessPointStarted(access_point, promo_action); + signin_metrics::RecordSigninUserActionForAccessPoint(access_point, + promo_action); + base::RecordAction(base::UserMetricsAction("Signin_SigninPage_Loading")); + } +} + +bool DiceTabHelper::IsChromeSigninPage() const { + return is_chrome_signin_page_; +} + +bool DiceTabHelper::IsSyncSigninInProgress() const { + return sync_signin_flow_status_ == SyncSigninFlowStatus::kStarted; +} + +void DiceTabHelper::OnSyncSigninFlowComplete() { + // The flow is complete, reset to initial state. + sync_signin_flow_status_ = SyncSigninFlowStatus::kNotStarted; +} + +void DiceTabHelper::DidStartNavigation( + content::NavigationHandle* navigation_handle) { + if (!is_chrome_signin_page_) + return; + + // Ignore internal navigations. + if (!navigation_handle->IsInPrimaryMainFrame() || + navigation_handle->IsSameDocument()) { + return; + } + + if (!IsSigninPageNavigation(navigation_handle)) { + // Navigating away from the signin page. + // Note that currently any indication of a navigation is enough to consider + // this tab unsuitable for re-use, even if the navigation does not end up + // committing. + is_chrome_signin_page_ = false; + } +} + +void DiceTabHelper::DidFinishNavigation( + content::NavigationHandle* navigation_handle) { + if (!is_chrome_signin_page_) + return; + + // Ignore internal navigations. + if (!navigation_handle->IsInPrimaryMainFrame() || + navigation_handle->IsSameDocument()) { + return; + } + + if (!IsSigninPageNavigation(navigation_handle)) { + // Navigating away from the signin page. + // Note that currently any indication of a navigation is enough to consider + // this tab unsuitable for re-use, even if the navigation does not end up + // committing. + is_chrome_signin_page_ = false; + return; + } + + if (!signin_page_load_recorded_) { + signin_page_load_recorded_ = true; + base::RecordAction(base::UserMetricsAction("Signin_SigninPage_Shown")); + } +} + +bool DiceTabHelper::IsSigninPageNavigation( + content::NavigationHandle* navigation_handle) const { + return !navigation_handle->IsErrorPage() && + navigation_handle->GetRedirectChain()[0] == signin_url_ && + navigation_handle->GetURL().DeprecatedGetOriginAsURL() == + GaiaUrls::GetInstance()->gaia_url(); +} + +WEB_CONTENTS_USER_DATA_KEY_IMPL(DiceTabHelper); diff --git a/chromium/chrome/browser/signin/dice_tab_helper.h b/chromium/chrome/browser/signin/dice_tab_helper.h new file mode 100644 index 00000000000..2f6190a4f98 --- /dev/null +++ b/chromium/chrome/browser/signin/dice_tab_helper.h @@ -0,0 +1,97 @@ +// Copyright 2017 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 CHROME_BROWSER_SIGNIN_DICE_TAB_HELPER_H_ +#define CHROME_BROWSER_SIGNIN_DICE_TAB_HELPER_H_ + +#include "components/signin/public/base/signin_metrics.h" +#include "content/public/browser/web_contents_observer.h" +#include "content/public/browser/web_contents_user_data.h" + +namespace content { +class NavigationHandle; +} + +// Tab helper used for DICE to tag signin tabs. Signin tabs can be reused. +class DiceTabHelper : public content::WebContentsUserData, + public content::WebContentsObserver { + public: + DiceTabHelper(const DiceTabHelper&) = delete; + DiceTabHelper& operator=(const DiceTabHelper&) = delete; + + ~DiceTabHelper() override; + + signin_metrics::AccessPoint signin_access_point() const { + return signin_access_point_; + } + + signin_metrics::PromoAction signin_promo_action() const { + return signin_promo_action_; + } + + signin_metrics::Reason signin_reason() const { return signin_reason_; } + + const GURL& redirect_url() const { return redirect_url_; } + + const GURL& signin_url() const { return signin_url_; } + + // Initializes the DiceTabHelper for a new signin flow. Must be called once + // per signin flow happening in the tab, when the signin URL is being loaded. + void InitializeSigninFlow(const GURL& signin_url, + signin_metrics::AccessPoint access_point, + signin_metrics::Reason reason, + signin_metrics::PromoAction promo_action, + const GURL& redirect_url); + + // Returns true if this the tab is a re-usable chrome sign-in page (the signin + // page is loading or loaded in the tab). + // Returns false if the user or the page has navigated away from |signin_url|. + bool IsChromeSigninPage() const; + + // Returns true if a signin flow was initialized with the reason + // kSigninPrimaryAccount and is not yet complete. + // Note that there is not guarantee that the flow would ever finish, and in + // some rare cases it is possible that a "non-sync" signin happens while this + // is true (if the user aborts the flow and then re-uses the same tab for a + // normal web signin). + bool IsSyncSigninInProgress() const; + + // Called to notify that the sync signin is complete. + void OnSyncSigninFlowComplete(); + + private: + friend class content::WebContentsUserData; + explicit DiceTabHelper(content::WebContents* web_contents); + + // kStarted: a Sync signin flow was started and not completed. + // kNotStarted: there is no sync signin flow in progress. + enum class SyncSigninFlowStatus { kNotStarted, kStarted }; + + // content::WebContentsObserver: + void DidStartNavigation( + content::NavigationHandle* navigation_handle) override; + void DidFinishNavigation( + content::NavigationHandle* navigation_handle) override; + + // Returns true if this is a navigation to the signin URL. + bool IsSigninPageNavigation( + content::NavigationHandle* navigation_handle) const; + + GURL redirect_url_; + GURL signin_url_; + signin_metrics::AccessPoint signin_access_point_ = + signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN; + signin_metrics::PromoAction signin_promo_action_ = + signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO; + signin_metrics::Reason signin_reason_ = + signin_metrics::Reason::kUnknownReason; + bool is_chrome_signin_page_ = false; + bool signin_page_load_recorded_ = false; + SyncSigninFlowStatus sync_signin_flow_status_ = + SyncSigninFlowStatus::kNotStarted; + + WEB_CONTENTS_USER_DATA_KEY_DECL(); +}; + +#endif // CHROME_BROWSER_SIGNIN_DICE_TAB_HELPER_H_ diff --git a/chromium/chrome/browser/signin/dice_tab_helper_unittest.cc b/chromium/chrome/browser/signin/dice_tab_helper_unittest.cc new file mode 100644 index 00000000000..96eab075f2a --- /dev/null +++ b/chromium/chrome/browser/signin/dice_tab_helper_unittest.cc @@ -0,0 +1,253 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/dice_tab_helper.h" + +#include "base/test/metrics/histogram_tester.h" +#include "base/test/metrics/user_action_tester.h" +#include "chrome/test/base/chrome_render_view_host_test_harness.h" +#include "chrome/test/base/testing_profile.h" +#include "components/signin/public/base/signin_metrics.h" +#include "content/public/common/content_features.h" +#include "content/public/test/back_forward_cache_util.h" +#include "content/public/test/navigation_simulator.h" +#include "content/public/test/test_renderer_host.h" +#include "content/public/test/web_contents_tester.h" +#include "google_apis/gaia/gaia_urls.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/public/common/features.h" + +class DiceTabHelperTest : public ChromeRenderViewHostTestHarness { + public: + DiceTabHelperTest() { + signin_url_ = GaiaUrls::GetInstance()->signin_chrome_sync_dice(); + feature_list_.InitWithFeaturesAndParameters( + {{features::kBackForwardCache, {}}, + {features::kBackForwardCacheMemoryControls, {}}}, + {}); + } + + // Does a navigation to Gaia and initializes the tab helper. + void InitializeDiceTabHelper(DiceTabHelper* helper, + signin_metrics::AccessPoint access_point, + signin_metrics::Reason reason) { + // Load the signin page. + std::unique_ptr simulator = + content::NavigationSimulator::CreateRendererInitiated(signin_url_, + main_rfh()); + simulator->Start(); + helper->InitializeSigninFlow( + signin_url_, access_point, reason, + signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO, + GURL::EmptyGURL()); + EXPECT_TRUE(helper->IsChromeSigninPage()); + simulator->Commit(); + } + + GURL signin_url_; + base::test::ScopedFeatureList feature_list_; +}; + +// Tests DiceTabHelper intialization. +TEST_F(DiceTabHelperTest, Initialization) { + DiceTabHelper::CreateForWebContents(web_contents()); + DiceTabHelper* dice_tab_helper = + DiceTabHelper::FromWebContents(web_contents()); + + // Check default state. + EXPECT_EQ(signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN, + dice_tab_helper->signin_access_point()); + EXPECT_EQ(signin_metrics::Reason::kUnknownReason, + dice_tab_helper->signin_reason()); + EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage()); + + // Initialize the signin flow. + signin_metrics::AccessPoint access_point = + signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_BUBBLE; + signin_metrics::Reason reason = signin_metrics::Reason::kSigninPrimaryAccount; + InitializeDiceTabHelper(dice_tab_helper, access_point, reason); + EXPECT_EQ(access_point, dice_tab_helper->signin_access_point()); + EXPECT_EQ(reason, dice_tab_helper->signin_reason()); + EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage()); +} + +TEST_F(DiceTabHelperTest, SigninPageStatus) { + // The test assumes the previous page gets deleted after navigation and will + // be recreated after navigation (which resets the signin page state). Disable + // back/forward cache to ensure that it doesn't get preserved in the cache. + content::DisableBackForwardCacheForTesting( + web_contents(), content::BackForwardCache::TEST_ASSUMES_NO_CACHING); + DiceTabHelper::CreateForWebContents(web_contents()); + DiceTabHelper* dice_tab_helper = + DiceTabHelper::FromWebContents(web_contents()); + EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage()); + + // Load the signin page. + signin_metrics::AccessPoint access_point = + signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_BUBBLE; + signin_metrics::Reason reason = signin_metrics::Reason::kSigninPrimaryAccount; + InitializeDiceTabHelper(dice_tab_helper, access_point, reason); + EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage()); + + // Reloading the signin page does not interrupt the signin flow. + content::NavigationSimulator::Reload(web_contents()); + EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage()); + + // Subframe navigation are ignored. + std::unique_ptr simulator = + content::NavigationSimulator::CreateRendererInitiated( + signin_url_.Resolve("#baz"), main_rfh()); + simulator->CommitSameDocument(); + EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage()); + + // Navigation in subframe does not interrupt the signin flow. + content::RenderFrameHostTester* render_frame_host_tester = + content::RenderFrameHostTester::For(main_rfh()); + content::RenderFrameHost* sub_frame = + render_frame_host_tester->AppendChild("subframe"); + content::NavigationSimulator::NavigateAndCommitFromDocument(signin_url_, + sub_frame); + EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage()); + + // Navigating to a different page resets the page status. + simulator = content::NavigationSimulator::CreateRendererInitiated( + signin_url_.Resolve("/foo"), main_rfh()); + simulator->Start(); + EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage()); + simulator->Commit(); + EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage()); + + // Go Back to the signin page + content::NavigationSimulator::GoBack(web_contents()); + // IsChromeSigninPage() returns false after navigating away from the + // signin page. + EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage()); + + // Navigate away from the signin page + content::NavigationSimulator::GoForward(web_contents()); + EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage()); +} + +// Tests DiceTabHelper metrics. +TEST_F(DiceTabHelperTest, Metrics) { + base::UserActionTester ua_tester; + base::HistogramTester h_tester; + DiceTabHelper::CreateForWebContents(web_contents()); + DiceTabHelper* dice_tab_helper = + DiceTabHelper::FromWebContents(web_contents()); + + // No metrics are logged when the Dice tab helper is created. + EXPECT_EQ(0, ua_tester.GetActionCount("Signin_Signin_FromStartPage")); + EXPECT_EQ(0, ua_tester.GetActionCount("Signin_SigninPage_Loading")); + EXPECT_EQ(0, ua_tester.GetActionCount("Signin_SigninPage_Shown")); + + // Check metrics logged when the Dice tab helper is initialized. + std::unique_ptr simulator = + content::NavigationSimulator::CreateRendererInitiated(signin_url_, + main_rfh()); + simulator->Start(); + dice_tab_helper->InitializeSigninFlow( + signin_url_, signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, + signin_metrics::Reason::kSigninPrimaryAccount, + signin_metrics::PromoAction::PROMO_ACTION_NEW_ACCOUNT_NO_EXISTING_ACCOUNT, + GURL::EmptyGURL()); + EXPECT_EQ(1, ua_tester.GetActionCount("Signin_Signin_FromSettings")); + EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Loading")); + EXPECT_EQ(0, ua_tester.GetActionCount("Signin_SigninPage_Shown")); + h_tester.ExpectUniqueSample( + "Signin.SigninStartedAccessPoint", + signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, 1); + h_tester.ExpectUniqueSample( + "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", + signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, 1); + + // First call to did finish load does logs any Signin_SigninPage_Shown user + // action. + simulator->Commit(); + EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Loading")); + EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Shown")); + + // Second call to did finish load does not log any metrics. + dice_tab_helper->DidFinishLoad(main_rfh(), signin_url_); + EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Loading")); + EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Shown")); + + // Check metrics are logged again when the Dice tab helper is re-initialized. + simulator = content::NavigationSimulator::CreateRendererInitiated(signin_url_, + main_rfh()); + simulator->Start(); + dice_tab_helper->InitializeSigninFlow( + signin_url_, signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, + signin_metrics::Reason::kSigninPrimaryAccount, + signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT, + GURL::EmptyGURL()); + EXPECT_EQ(2, ua_tester.GetActionCount("Signin_Signin_FromSettings")); + EXPECT_EQ(2, ua_tester.GetActionCount("Signin_SigninPage_Loading")); + h_tester.ExpectUniqueSample( + "Signin.SigninStartedAccessPoint", + signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, 2); + h_tester.ExpectUniqueSample( + "Signin.SigninStartedAccessPoint.WithDefault", + signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, 1); +} + +TEST_F(DiceTabHelperTest, IsSyncSigninInProgress) { + DiceTabHelper::CreateForWebContents(web_contents()); + DiceTabHelper* dice_tab_helper = + DiceTabHelper::FromWebContents(web_contents()); + EXPECT_FALSE(dice_tab_helper->IsSyncSigninInProgress()); + + // Non-sync signin. + InitializeDiceTabHelper(dice_tab_helper, + signin_metrics::AccessPoint::ACCESS_POINT_EXTENSIONS, + signin_metrics::Reason::kAddSecondaryAccount); + EXPECT_FALSE(dice_tab_helper->IsSyncSigninInProgress()); + + // Sync signin + InitializeDiceTabHelper(dice_tab_helper, + signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, + signin_metrics::Reason::kSigninPrimaryAccount); + EXPECT_TRUE(dice_tab_helper->IsSyncSigninInProgress()); + dice_tab_helper->OnSyncSigninFlowComplete(); + EXPECT_FALSE(dice_tab_helper->IsSyncSigninInProgress()); +} + +class DiceTabHelperPrerenderTest : public DiceTabHelperTest { + public: + DiceTabHelperPrerenderTest() { + feature_list_.InitWithFeatures( + {blink::features::kPrerender2}, + // Disable the memory requirement of Prerender2 so the test can run on + // any bot. + {blink::features::kPrerender2MemoryControls}); + } + + ~DiceTabHelperPrerenderTest() override = default; + + private: + base::test::ScopedFeatureList feature_list_; +}; + +TEST_F(DiceTabHelperPrerenderTest, SigninStatusAfterPrerendering) { + base::UserActionTester ua_tester; + DiceTabHelper::CreateForWebContents(web_contents()); + DiceTabHelper* dice_tab_helper = + DiceTabHelper::FromWebContents(web_contents()); + EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage()); + EXPECT_EQ(0, ua_tester.GetActionCount("Signin_SigninPage_Shown")); + + // Sync signin + InitializeDiceTabHelper(dice_tab_helper, + signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, + signin_metrics::Reason::kSigninPrimaryAccount); + dice_tab_helper->OnSyncSigninFlowComplete(); + EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage()); + EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Shown")); + + // Starting prerendering a page doesn't navigate away from the signin page. + content::WebContentsTester::For(web_contents()) + ->AddPrerenderAndCommitNavigation(signin_url_.Resolve("/foo/test.html")); + EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage()); + EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Shown")); +} diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor.cc b/chromium/chrome/browser/signin/dice_web_signin_interceptor.cc new file mode 100644 index 00000000000..751c3a58fad --- /dev/null +++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor.cc @@ -0,0 +1,851 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/dice_web_signin_interceptor.h" + +#include + +#include "base/check.h" +#include "base/hash/hash.h" +#include "base/i18n/case_conversion.h" +#include "base/metrics/field_trial_params.h" +#include "base/metrics/histogram_functions.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/enterprise/util/managed_browser_utils.h" +#include "chrome/browser/net/system_network_context_manager.h" +#include "chrome/browser/new_tab_page/chrome_colors/generated_colors_info.h" +#include "chrome/browser/password_manager/chrome_password_manager_client.h" +#include "chrome/browser/policy/chrome_browser_policy_connector.h" +#include "chrome/browser/policy/profile_policy_connector.h" +#include "chrome/browser/profiles/profile_attributes_entry.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/profiles/profile_avatar_icon_util.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/profiles/profile_metrics.h" +#include "chrome/browser/profiles/profiles_state.h" +#include "chrome/browser/signin/dice_intercepted_session_startup_helper.h" +#include "chrome/browser/signin/dice_signed_in_profile_creator.h" +#include "chrome/browser/signin/dice_web_signin_interceptor_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/signin_features.h" +#include "chrome/browser/signin/signin_util.h" +#include "chrome/browser/themes/theme_service.h" +#include "chrome/browser/themes/theme_service_factory.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/browser_list.h" +#include "chrome/browser/ui/browser_window.h" +#include "chrome/browser/ui/passwords/manage_passwords_ui_controller.h" +#include "chrome/browser/ui/signin/profile_colors_util.h" +#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h" +#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper_delegate_impl.h" +#include "chrome/common/pref_names.h" +#include "chrome/common/themes/autogenerated_theme_util.h" +#include "components/password_manager/core/browser/password_manager.h" +#include "components/password_manager/core/common/password_manager_ui.h" +#include "components/policy/core/browser/browser_policy_connector.h" +#include "components/policy/core/browser/signin/user_cloud_signin_restriction_policy_fetcher.h" +#include "components/policy/core/common/features.h" +#include "components/policy/core/common/policy_map.h" +#include "components/policy/core/common/policy_namespace.h" +#include "components/policy/core/common/policy_service.h" +#include "components/policy/policy_constants.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/prefs/pref_service.h" +#include "components/prefs/scoped_user_pref_update.h" +#include "components/signin/public/base/signin_metrics.h" +#include "components/signin/public/identity_manager/accounts_mutator.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "google_apis/gaia/gaia_auth_util.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#include "ui/base/l10n/l10n_util.h" + +namespace { + +constexpr char kProfileCreationInterceptionDeclinedPref[] = + "signin.ProfileCreationInterceptionDeclinedPref"; + +void RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome outcome) { + base::UmaHistogramEnumeration("Signin.Intercept.HeuristicOutcome", outcome); +} + +// Helper function to return the primary account info. The returned info is +// empty if there is no primary account, and non-empty otherwise. Extended +// fields may be missing if they are not available. +AccountInfo GetPrimaryAccountInfo(signin::IdentityManager* manager) { + CoreAccountInfo primary_core_account_info = + manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin); + if (primary_core_account_info.IsEmpty()) + return AccountInfo(); + + AccountInfo primary_account_info = + manager->FindExtendedAccountInfo(primary_core_account_info); + + if (!primary_account_info.IsEmpty()) + return primary_account_info; + + // Return an AccountInfo without extended fields, based on the core info. + AccountInfo account_info; + account_info.gaia = primary_core_account_info.gaia; + account_info.email = primary_core_account_info.email; + account_info.account_id = primary_core_account_info.account_id; + return account_info; +} + +bool HasNoBrowser(content::WebContents* web_contents) { + return chrome::FindBrowserWithWebContents(web_contents) == nullptr; +} + +// Returns true if enterprise separation is required. +// Returns false is enterprise separation is not required. +// Returns no value if info is required to determine if enterprise separation is +// required. +// If `managed_account_profile_level_signin_restriction` is `absl::nullopt` then +// the user cloud policy value of ManagedAccountsSigninRestriction has not yet +// been fetched. If it is an empty string, then the value has been fetched but +// no policy was set. +absl::optional EnterpriseSeparationMaybeRequired( + Profile* profile, + const std::string& email, + signin::IdentityManager* identity_manager, + bool is_new_account_interception, + absl::optional + managed_account_profile_level_signin_restriction) { + // No enterprise separation required if the feature is disabled. + if (!base::FeatureList::IsEnabled(kAccountPoliciesLoadedWithoutSync)) + return false; + // No enterprise separation required for consumer accounts. + if (policy::BrowserPolicyConnector::IsNonEnterpriseUser(email)) + return false; + + auto intercepted_account_info = + identity_manager->FindExtendedAccountInfoByEmailAddress(email); + // If the account info is not found, we need to wait for the info to be + // available. + if (!intercepted_account_info.IsValid()) + return absl::nullopt; + // If the intercepted account is not managed, no interception required. + if (!intercepted_account_info.IsManaged()) + return false; + // If `profile` requires enterprise profile separation, return true. + if (signin_util::ProfileSeparationEnforcedByPolicy( + profile, managed_account_profile_level_signin_restriction.value_or( + std::string()))) { + return true; + } + // If we still do not know if profile separation is required, the account + // level policies for the intercepted account must be fetched if possible. + if (is_new_account_interception && + base::FeatureList::IsEnabled( + policy::features::kEnableUserCloudSigninRestrictionPolicyFetcher) && + !managed_account_profile_level_signin_restriction.has_value() && + g_browser_process->system_network_context_manager()) { + return absl::nullopt; + } + + return false; +} + +} // namespace + +ScopedDiceWebSigninInterceptionBubbleHandle:: + ~ScopedDiceWebSigninInterceptionBubbleHandle() = default; + +bool SigninInterceptionHeuristicOutcomeIsSuccess( + SigninInterceptionHeuristicOutcome outcome) { + return outcome == SigninInterceptionHeuristicOutcome::kInterceptEnterprise || + outcome == SigninInterceptionHeuristicOutcome::kInterceptMultiUser || + outcome == SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch; +} + +DiceWebSigninInterceptor::DiceWebSigninInterceptor( + Profile* profile, + std::unique_ptr delegate) + : profile_(profile), + identity_manager_(IdentityManagerFactory::GetForProfile(profile)), + delegate_(std::move(delegate)) { + DCHECK(profile_); + DCHECK(identity_manager_); + DCHECK(delegate_); +} + +DiceWebSigninInterceptor::~DiceWebSigninInterceptor() = default; + +// static +void DiceWebSigninInterceptor::RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) { + registry->RegisterDictionaryPref(kProfileCreationInterceptionDeclinedPref); + registry->RegisterBooleanPref(prefs::kSigninInterceptionEnabled, true); + registry->RegisterStringPref(prefs::kManagedAccountsSigninRestriction, + std::string()); + registry->RegisterBooleanPref( + prefs::kManagedAccountsSigninRestrictionScopeMachine, false); +} + +absl::optional +DiceWebSigninInterceptor::GetHeuristicOutcome( + bool is_new_account, + bool is_sync_signin, + const std::string& email, + const ProfileAttributesEntry** entry) const { + if (!profile_->GetPrefs()->GetBoolean(prefs::kSigninInterceptionEnabled)) + return SigninInterceptionHeuristicOutcome::kAbortInterceptionDisabled; + + if (is_sync_signin) { + // Do not intercept signins from the Sync startup flow. + // Note: |is_sync_signin| is an approximation, and in rare cases it may be + // true when in fact the signin was not a sync signin. In this case the + // interception is missed. + return SigninInterceptionHeuristicOutcome::kAbortSyncSignin; + } + // Wait for more account info is enterprise separation is required or if more + // info is needed. + if (EnterpriseSeparationMaybeRequired( + profile_, email, identity_manager_, is_new_account, + /*managed_account_profile_level_signin_restriction=*/absl::nullopt) + .value_or(true)) { + return absl::nullopt; + } + + if (!is_new_account) { + // Do not intercept reauth. + return SigninInterceptionHeuristicOutcome::kAbortAccountNotNew; + } + + const ProfileAttributesEntry* switch_to_entry = ShouldShowProfileSwitchBubble( + email, + &g_browser_process->profile_manager()->GetProfileAttributesStorage()); + if (switch_to_entry) { + if (entry) + *entry = switch_to_entry; + return SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch; + } + + // From this point the remaining possible interceptions involve creating a new + // profile. + if (!profiles::IsProfileCreationAllowed()) { + return SigninInterceptionHeuristicOutcome::kAbortProfileCreationDisallowed; + } + + std::vector accounts_in_chrome = + identity_manager_->GetAccountsWithRefreshTokens(); + if (accounts_in_chrome.size() == 0 || + (accounts_in_chrome.size() == 1 && + gaia::AreEmailsSame(email, accounts_in_chrome[0].email))) { + // Enterprise and multi-user bubbles are only shown if there are multiple + // accounts. The intercepted account may not be added to chrome yet. + return SigninInterceptionHeuristicOutcome::kAbortSingleAccount; + } + + if (HasUserDeclinedProfileCreation(email)) { + return SigninInterceptionHeuristicOutcome:: + kAbortUserDeclinedProfileForAccount; + } + + return absl::nullopt; +} + +void DiceWebSigninInterceptor::MaybeInterceptWebSignin( + content::WebContents* web_contents, + CoreAccountId account_id, + bool is_new_account, + bool is_sync_signin) { + if (is_interception_in_progress_) { + // Multiple concurrent interceptions are not supported. + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kAbortInterceptInProgress); + return; + } + + if (!web_contents) { + // The tab has been closed (typically during the token exchange, which may + // take some time). + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kAbortTabClosed); + return; + } + + if (HasNoBrowser(web_contents)) { + // Do not intercept from the profile creation flow. + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kAbortNoBrowser); + return; + } + + // Do not show the interception UI if a password update is required: both + // bubbles cannot be shown at the same time and the password update is more + // important. + ChromePasswordManagerClient* password_manager_client = + ChromePasswordManagerClient::FromWebContents(web_contents); + if (password_manager_client && password_manager_client->GetPasswordManager() + ->IsFormManagerPendingPasswordUpdate()) { + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kAbortPasswordUpdatePending); + return; + } + + ManagePasswordsUIController* password_controller = + ManagePasswordsUIController::FromWebContents(web_contents); + if (password_controller && + password_controller->GetState() == + password_manager::ui::State::PENDING_PASSWORD_UPDATE_STATE) { + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kAbortPasswordUpdate); + return; + } + + AccountInfo account_info = + identity_manager_->FindExtendedAccountInfoByAccountId(account_id); + DCHECK(!account_info.IsEmpty()) << "Intercepting unknown account."; + const ProfileAttributesEntry* entry = nullptr; + absl::optional heuristic_outcome = + GetHeuristicOutcome(is_new_account, is_sync_signin, account_info.email, + &entry); + account_id_ = account_id; + is_interception_in_progress_ = true; + new_account_interception_ = is_new_account; + web_contents_ = web_contents->GetWeakPtr(); + + if (heuristic_outcome) { + RecordSigninInterceptionHeuristicOutcome(*heuristic_outcome); + if (*heuristic_outcome == + SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch) { + DCHECK(entry); + Delegate::BubbleParameters bubble_parameters{ + SigninInterceptionType::kProfileSwitch, account_info, + GetPrimaryAccountInfo(identity_manager_), + entry->GetProfileThemeColors().profile_highlight_color, + /*show_guest_option=*/false}; + interception_bubble_handle_ = delegate_->ShowSigninInterceptionBubble( + web_contents, bubble_parameters, + base::BindOnce(&DiceWebSigninInterceptor::OnProfileSwitchChoice, + base::Unretained(this), account_info.email, + entry->GetPath())); + was_interception_ui_displayed_ = true; + } else { + // Interception is aborted. + DCHECK(!SigninInterceptionHeuristicOutcomeIsSuccess(*heuristic_outcome)); + Reset(); + } + return; + } + + account_info_fetch_start_time_ = base::TimeTicks::Now(); + if (account_info.IsValid()) { + OnExtendedAccountInfoUpdated(account_info); + } else { + on_account_info_update_timeout_.Reset(base::BindOnce( + &DiceWebSigninInterceptor::OnExtendedAccountInfoFetchTimeout, + base::Unretained(this))); + base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, on_account_info_update_timeout_.callback(), + base::Seconds(5)); + account_info_update_observation_.Observe(identity_manager_.get()); + } +} + +void DiceWebSigninInterceptor::CreateBrowserAfterSigninInterception( + CoreAccountId account_id, + content::WebContents* intercepted_contents, + std::unique_ptr bubble_handle, + bool is_new_profile) { + DCHECK(!session_startup_helper_); + DCHECK(bubble_handle); + interception_bubble_handle_ = std::move(bubble_handle); + session_startup_helper_ = + std::make_unique( + profile_, is_new_profile, account_id, intercepted_contents); + session_startup_helper_->Startup( + base::BindOnce(&DiceWebSigninInterceptor::OnNewBrowserCreated, + base::Unretained(this), is_new_profile)); +} + +void DiceWebSigninInterceptor::Shutdown() { + if (is_interception_in_progress_ && !was_interception_ui_displayed_) { + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kAbortShutdown); + } + Reset(); +} + +void DiceWebSigninInterceptor::Reset() { + web_contents_ = nullptr; + account_info_update_observation_.Reset(); + on_account_info_update_timeout_.Cancel(); + is_interception_in_progress_ = false; + account_id_ = CoreAccountId(); + new_account_interception_ = false; + intercepted_account_management_accepted_ = false; + dice_signed_in_profile_creator_.reset(); + was_interception_ui_displayed_ = false; + account_info_fetch_start_time_ = base::TimeTicks(); + profile_creation_start_time_ = base::TimeTicks(); + interception_bubble_handle_.reset(); + on_intercepted_account_level_policy_value_timeout_.Cancel(); + account_level_signin_restriction_policy_fetcher_.reset(); + intercepted_account_level_policy_value_.reset(); +} + +const ProfileAttributesEntry* +DiceWebSigninInterceptor::ShouldShowProfileSwitchBubble( + const std::string& intercepted_email, + ProfileAttributesStorage* profile_attribute_storage) const { + // Check if there is already an existing profile with this account. + base::FilePath profile_path = profile_->GetPath(); + for (const auto* entry : + profile_attribute_storage->GetAllProfilesAttributes()) { + if (entry->GetPath() == profile_path) + continue; + if (gaia::AreEmailsSame(intercepted_email, + base::UTF16ToUTF8(entry->GetUserName()))) { + return entry; + } + } + return nullptr; +} + +bool DiceWebSigninInterceptor::ShouldEnforceEnterpriseProfileSeparation( + const AccountInfo& intercepted_account_info) const { + DCHECK(intercepted_account_info.IsValid()); + + if (!signin_util::ProfileSeparationEnforcedByPolicy( + profile_, + intercepted_account_level_policy_value_.value_or(std::string()))) { + return false; + } + if (new_account_interception_) + return intercepted_account_info.IsManaged(); + + CoreAccountInfo primary_core_account_info = + identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin); + // In case of re-auth, do not show the enterprise separation dialog if the + // user already consented to enterprise management. + if (!new_account_interception_ && primary_core_account_info.account_id == + intercepted_account_info.account_id) { + return !chrome::enterprise_util::UserAcceptedAccountManagement(profile_); + } + + return false; +} + +bool DiceWebSigninInterceptor::ShouldShowEnterpriseBubble( + const AccountInfo& intercepted_account_info) const { + DCHECK(intercepted_account_info.IsValid()); + // Check if the intercepted account or the primary account is managed. + CoreAccountInfo primary_core_account_info = + identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin); + + if (primary_core_account_info.IsEmpty() || + primary_core_account_info.account_id == + intercepted_account_info.account_id) { + return false; + } + + if (intercepted_account_info.IsManaged()) + return true; + + return identity_manager_->FindExtendedAccountInfo(primary_core_account_info) + .IsManaged(); +} + +bool DiceWebSigninInterceptor::ShouldShowMultiUserBubble( + const AccountInfo& intercepted_account_info) const { + DCHECK(intercepted_account_info.IsValid()); + if (identity_manager_->GetAccountsWithRefreshTokens().size() <= 1u) + return false; + // Check if the account has the same name as another account in the profile. + for (const auto& account_info : + identity_manager_->GetExtendedAccountInfoForAccountsWithRefreshToken()) { + if (account_info.account_id == intercepted_account_info.account_id) + continue; + // Case-insensitve comparison supporting non-ASCII characters. + if (base::i18n::FoldCase(base::UTF8ToUTF16(account_info.given_name)) == + base::i18n::FoldCase( + base::UTF8ToUTF16(intercepted_account_info.given_name))) { + return false; + } + } + return true; +} + +void DiceWebSigninInterceptor::OnInterceptionReadyToBeProcessed( + const AccountInfo& info) { + DCHECK_EQ(info.account_id, account_id_); + DCHECK(info.IsValid()); + + absl::optional interception_type; + + ProfileAttributesEntry* entry = + g_browser_process->profile_manager() + ->GetProfileAttributesStorage() + .GetProfileAttributesWithPath(profile_->GetPath()); + SkColor profile_color = GenerateNewProfileColor(entry).color; + + const ProfileAttributesEntry* switch_to_entry = ShouldShowProfileSwitchBubble( + info.email, + &g_browser_process->profile_manager()->GetProfileAttributesStorage()); + + bool force_profile_separation = + ShouldEnforceEnterpriseProfileSeparation(info); + +#if DCHECK_IS_ON() + if (force_profile_separation) { + DCHECK(base::FeatureList::IsEnabled(kAccountPoliciesLoadedWithoutSync) || + !profile_->GetPrefs() + ->GetString(prefs::kManagedAccountsSigninRestriction) + .empty()); + } +#endif + + if (switch_to_entry) { + // Propose account switching if we skipped in GetHeuristicOutcome because we + // returned a nullptr to get more information about forced enterprise + // profile separation. + interception_type = force_profile_separation + ? SigninInterceptionType::kProfileSwitchForced + : SigninInterceptionType::kProfileSwitch; + RecordSigninInterceptionHeuristicOutcome( + force_profile_separation + ? SigninInterceptionHeuristicOutcome:: + kInterceptEnterpriseForcedProfileSwitch + : SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch); + } else if (force_profile_separation) { + // In case of a reauth of an account that already had sync enabled, + // the user already accepted to use a managed profile. Simply update that + // fact. + if (!new_account_interception_ && + identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSync) == + info.account_id) { + chrome::enterprise_util::SetUserAcceptedAccountManagement(profile_, true); + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kAbortAccountNotNew); + Reset(); + return; + } + interception_type = SigninInterceptionType::kEnterpriseForced; + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced); + } else if (ShouldShowEnterpriseBubble(info)) { + interception_type = SigninInterceptionType::kEnterprise; + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kInterceptEnterprise); + } else if (ShouldShowMultiUserBubble(info)) { + interception_type = SigninInterceptionType::kMultiUser; + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kInterceptMultiUser); + } + + if (!interception_type) { + // Signin should not be intercepted. + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kAbortAccountInfoNotCompatible); + Reset(); + return; + } + + Delegate::BubbleParameters bubble_parameters{ + *interception_type, info, GetPrimaryAccountInfo(identity_manager_), + GetAutogeneratedThemeColors(profile_color).frame_color, + /*show_guest_option=*/false}; + + base::OnceCallback callback; + switch (*interception_type) { + case SigninInterceptionType::kProfileSwitchForced: + callback = base::BindOnce( + &DiceWebSigninInterceptor::OnProfileSwitchChoice, + base::Unretained(this), info.email, switch_to_entry->GetPath()); + break; + case SigninInterceptionType::kEnterpriseForced: + callback = base::BindOnce( + &DiceWebSigninInterceptor::OnEnterpriseProfileCreationResult, + base::Unretained(this), info, profile_color); + break; + case SigninInterceptionType::kProfileSwitch: + case SigninInterceptionType::kEnterprise: + case SigninInterceptionType::kMultiUser: + callback = + base::BindOnce(&DiceWebSigninInterceptor::OnProfileCreationChoice, + base::Unretained(this), info, profile_color); + break; + } + interception_bubble_handle_ = delegate_->ShowSigninInterceptionBubble( + web_contents_.get(), bubble_parameters, std::move(callback)); + + was_interception_ui_displayed_ = true; +} + +void DiceWebSigninInterceptor::OnExtendedAccountInfoUpdated( + const AccountInfo& info) { + if (info.account_id != account_id_) + return; + if (!info.IsValid()) + return; + + account_info_update_observation_.Reset(); + on_account_info_update_timeout_.Cancel(); + base::UmaHistogramTimes( + "Signin.Intercept.AccountInfoFetchDuration", + base::TimeTicks::Now() - account_info_fetch_start_time_); + + // Fetch the ManagedAccountsSigninRestriction policy value for the intercepted + // account with a timeout. + if (!EnterpriseSeparationMaybeRequired( + profile_, info.email, identity_manager_, new_account_interception_, + intercepted_account_level_policy_value_) + .has_value()) { + FetchAccountLevelSigninRestrictionForInterceptedAccount( + info, base::BindOnce( + &DiceWebSigninInterceptor:: + OnAccountLevelManagedAccountsSigninRestrictionReceived, + base::Unretained(this), /*timed_out=*/false, info)); + return; + } + + OnInterceptionReadyToBeProcessed(info); +} + +void DiceWebSigninInterceptor::OnExtendedAccountInfoFetchTimeout() { + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kAbortAccountInfoTimeout); + Reset(); +} + +void DiceWebSigninInterceptor::OnProfileCreationChoice( + const AccountInfo& account_info, + SkColor profile_color, + SigninInterceptionResult create) { + if (create != SigninInterceptionResult::kAccepted && + create != SigninInterceptionResult::kAcceptedWithGuest) { + if (create == SigninInterceptionResult::kDeclined) + RecordProfileCreationDeclined(account_info.email); + Reset(); + return; + } + + DCHECK(interception_bubble_handle_); + profile_creation_start_time_ = base::TimeTicks::Now(); + std::u16string profile_name; + profile_name = profiles::GetDefaultNameForNewSignedInProfile(account_info); + + DCHECK(!dice_signed_in_profile_creator_); + // Unretained is fine because the profile creator is owned by this. + dice_signed_in_profile_creator_ = + std::make_unique( + profile_, account_id_, profile_name, + profiles::GetPlaceholderAvatarIndex(), + create == SigninInterceptionResult::kAcceptedWithGuest, + base::BindOnce(&DiceWebSigninInterceptor::OnNewSignedInProfileCreated, + base::Unretained(this), profile_color)); +} + +void DiceWebSigninInterceptor::OnProfileSwitchChoice( + const std::string& email, + const base::FilePath& profile_path, + SigninInterceptionResult switch_profile) { + if (switch_profile != SigninInterceptionResult::kAccepted) { + Reset(); + return; + } + + DCHECK(interception_bubble_handle_); + DCHECK(!dice_signed_in_profile_creator_); + profile_creation_start_time_ = base::TimeTicks::Now(); + // Unretained is fine because the profile creator is owned by this. + dice_signed_in_profile_creator_ = + std::make_unique( + profile_, account_id_, profile_path, + base::BindOnce(&DiceWebSigninInterceptor::OnNewSignedInProfileCreated, + base::Unretained(this), absl::nullopt)); +} + +void DiceWebSigninInterceptor::OnNewSignedInProfileCreated( + absl::optional profile_color, + Profile* new_profile) { + DCHECK(dice_signed_in_profile_creator_); + dice_signed_in_profile_creator_.reset(); + + if (!new_profile) { + Reset(); + return; + } + + // The profile color is defined only when the profile has just been created + // (with interception type kMultiUser or kEnterprise). If the profile is not + // new (kProfileSwitch) or if it is a guest profile, then the color is not + // updated. + bool is_new_profile = profile_color.has_value(); + if (is_new_profile) { + base::UmaHistogramTimes( + "Signin.Intercept.ProfileCreationDuration", + base::TimeTicks::Now() - profile_creation_start_time_); + ProfileMetrics::LogProfileAddNewUser( + ProfileMetrics::ADD_NEW_USER_SIGNIN_INTERCEPTION); + // TODO(https://crbug.com/1225171): Remove the condition if Guest mode + // option is removed. + if (!new_profile->IsGuestSession()) { + // Apply the new color to the profile. + ThemeServiceFactory::GetForProfile(new_profile) + ->BuildAutogeneratedThemeFromColor(*profile_color); + } + } else { + base::UmaHistogramTimes( + "Signin.Intercept.ProfileSwitchDuration", + base::TimeTicks::Now() - profile_creation_start_time_); + } + + if (base::FeatureList::IsEnabled(kAccountPoliciesLoadedWithoutSync)) { + chrome::enterprise_util::SetUserAcceptedAccountManagement( + new_profile, intercepted_account_management_accepted_); + } + + // Work is done in this profile, the flow continues in the + // DiceWebSigninInterceptor that is attached to the new profile. + DiceWebSigninInterceptorFactory::GetForProfile(new_profile) + ->CreateBrowserAfterSigninInterception( + account_id_, web_contents_.get(), + std::move(interception_bubble_handle_), is_new_profile); + Reset(); +} + +void DiceWebSigninInterceptor::OnEnterpriseProfileCreationResult( + const AccountInfo& account_info, + SkColor profile_color, + SigninInterceptionResult create) { + DCHECK(base::FeatureList::IsEnabled(kAccountPoliciesLoadedWithoutSync)); + if (create == SigninInterceptionResult::kAccepted) { + intercepted_account_management_accepted_ = true; + // In case of a reauth if there was no consent for management, do not create + // a new profile. + if (!new_account_interception_ && + GetPrimaryAccountInfo(identity_manager_).account_id == + account_info.account_id) { + chrome::enterprise_util::SetUserAcceptedAccountManagement( + profile_, intercepted_account_management_accepted_); + Reset(); + } else { + OnProfileCreationChoice(account_info, profile_color, + SigninInterceptionResult::kAccepted); + } + } else { + DCHECK_EQ(SigninInterceptionResult::kDeclined, create) + << "The user can only accept or decline"; + OnProfileCreationChoice(account_info, profile_color, + SigninInterceptionResult::kDeclined); + auto* accounts_mutator = identity_manager_->GetAccountsMutator(); + accounts_mutator->RemoveAccount( + account_info.account_id, + signin_metrics::SourceForRefreshTokenOperation:: + kDiceTurnOnSyncHelper_Abort); + } + signin_util::RecordEnterpriseProfileCreationUserChoice( + /*enforced_by_policy=*/signin_util::ProfileSeparationEnforcedByPolicy( + profile_, + intercepted_account_level_policy_value_.value_or(std::string())), + /*created=*/create == SigninInterceptionResult::kAccepted); +} + +void DiceWebSigninInterceptor::OnNewBrowserCreated(bool is_new_profile) { + DCHECK(interception_bubble_handle_); + interception_bubble_handle_.reset(); // Close the bubble now. + session_startup_helper_.reset(); + + // TODO(https://crbug.com/1225171): Remove |IsGuestSession| if Guest option is + // no more supported. + if (!is_new_profile || profile_->IsGuestSession()) + return; + + // Don't show the customization bubble if a valid policy theme is set. + Browser* browser = chrome::FindBrowserWithProfile(profile_); + if (ThemeServiceFactory::GetForProfile(profile_)->UsingPolicyTheme()) { + // Show the profile switch IPH that is normally shown after the + // customization bubble. + browser->window()->MaybeShowProfileSwitchIPH(); + return; + } + + DCHECK(browser); + delegate_->ShowProfileCustomizationBubble(browser); +} + +// static +std::string DiceWebSigninInterceptor::GetPersistentEmailHash( + const std::string& email) { + int hash = base::PersistentHash( + gaia::CanonicalizeEmail(gaia::SanitizeEmail(email))) & + 0xFF; + return base::StringPrintf("email_%i", hash); +} + +void DiceWebSigninInterceptor::RecordProfileCreationDeclined( + const std::string& email) { + DictionaryPrefUpdate update(profile_->GetPrefs(), + kProfileCreationInterceptionDeclinedPref); + std::string key = GetPersistentEmailHash(email); + absl::optional declined_count = update->FindIntKey(key); + update->SetIntKey(key, declined_count.value_or(0) + 1); +} + +bool DiceWebSigninInterceptor::HasUserDeclinedProfileCreation( + const std::string& email) const { + const base::DictionaryValue* pref_data = profile_->GetPrefs()->GetDictionary( + kProfileCreationInterceptionDeclinedPref); + absl::optional declined_count = + pref_data->FindIntKey(GetPersistentEmailHash(email)); + // Check if the user declined 2 times. + constexpr int kMaxProfileCreationDeclinedCount = 2; + return declined_count && + declined_count.value() >= kMaxProfileCreationDeclinedCount; +} + +void DiceWebSigninInterceptor:: + FetchAccountLevelSigninRestrictionForInterceptedAccount( + const AccountInfo& account_info, + base::OnceCallback callback) { + DCHECK(base::FeatureList::IsEnabled( + policy::features::kEnableUserCloudSigninRestrictionPolicyFetcher)); + if (intercepted_account_level_policy_value_fetch_result_for_testing_ + .has_value()) { + std::move(callback).Run( + intercepted_account_level_policy_value_fetch_result_for_testing_ + .value()); + return; + } + + account_level_signin_restriction_policy_fetcher_ = + std::make_unique( + g_browser_process->browser_policy_connector(), + g_browser_process->system_network_context_manager() + ->GetSharedURLLoaderFactory()); + account_level_signin_restriction_policy_fetcher_ + ->GetManagedAccountsSigninRestriction( + identity_manager_, account_info.account_id, std::move(callback)); + + on_intercepted_account_level_policy_value_timeout_.Reset(base::BindOnce( + &DiceWebSigninInterceptor:: + OnAccountLevelManagedAccountsSigninRestrictionReceived, + base::Unretained(this), /*timed_out=*/true, account_info, std::string())); + base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, on_intercepted_account_level_policy_value_timeout_.callback(), + base::Seconds(5)); +} + +void DiceWebSigninInterceptor:: + OnAccountLevelManagedAccountsSigninRestrictionReceived( + bool timed_out, + const AccountInfo& account_info, + const std::string& signin_restriction) { +#if DCHECK_IS_ON() + if (timed_out) { + DCHECK(signin_restriction.empty()) + << "There should be no signin restriction at the account level in case " + "of a timeout"; + } +#endif + intercepted_account_level_policy_value_ = signin_restriction; + OnInterceptionReadyToBeProcessed(account_info); +} diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor.h b/chromium/chrome/browser/signin/dice_web_signin_interceptor.h new file mode 100644 index 00000000000..0e3a03f41c3 --- /dev/null +++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor.h @@ -0,0 +1,411 @@ +// Copyright 2020 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 CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_H_ +#define CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_H_ + +#include + +#include "base/callback_forward.h" +#include "base/cancelable_callback.h" +#include "base/feature_list.h" +#include "base/gtest_prod_util.h" +#include "base/memory/raw_ptr.h" +#include "base/scoped_observation.h" +#include "base/time/time.h" +#include "chrome/browser/ui/webui/signin/enterprise_profile_welcome_ui.h" +#include "components/keyed_service/core/keyed_service.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "google_apis/gaia/core_account_id.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#include "third_party/skia/include/core/SkColor.h" + +namespace base { +class FilePath; +} + +namespace content { +class WebContents; +} + +namespace policy { +class UserCloudSigninRestrictionPolicyFetcher; +} +namespace user_prefs { +class PrefRegistrySyncable; +} + +struct AccountInfo; +class Browser; +class DiceSignedInProfileCreator; +class DiceInterceptedSessionStartupHelper; +class Profile; +class ProfileAttributesEntry; +class ProfileAttributesStorage; + +// Outcome of the interception heuristic (decision whether the interception +// bubble is shown or not). +// These values are persisted to logs. Entries should not be renumbered and +// numeric values should never be reused. +enum class SigninInterceptionHeuristicOutcome { + // Interception succeeded: + kInterceptProfileSwitch = 0, + kInterceptMultiUser = 1, + kInterceptEnterprise = 2, + + // Interception aborted: + // This is a "Sync" sign in and not a "web" sign in. + kAbortSyncSignin = 3, + // Another interception is already in progress. + kAbortInterceptInProgress = 4, + // This is not a new account (reauth). + kAbortAccountNotNew = 5, + // New profile is not offered when there is only one account. + kAbortSingleAccount = 6, + // Extended account info could not be downloaded. + kAbortAccountInfoTimeout = 7, + // Account info not compatible with interception (e.g. same Gaia name). + kAbortAccountInfoNotCompatible = 8, + // Profile creation disallowed. + kAbortProfileCreationDisallowed = 9, + // The interceptor was shut down before the heuristic completed. + kAbortShutdown = 10, + // The interceptor is not offered when WebContents has no browser associated. + kAbortNoBrowser = 11, + // A password update is required for the account, and this takes priority over + // signin interception. + kAbortPasswordUpdate = 12, + // A password update will be required for the account: the password used on + // the form does not match the stored password. + kAbortPasswordUpdatePending = 13, + // The user already declined a new profile for this account, the UI is not + // shown again. + kAbortUserDeclinedProfileForAccount = 14, + // Signin interception is disabled by the SigninInterceptionEnabled policy. + kAbortInterceptionDisabled = 15, + + // Interception succeeded when enteprise account separation is mandatory. + kInterceptEnterpriseForced = 16, + kInterceptEnterpriseForcedProfileSwitch = 17, + + // The interceptor is not triggered if the tab has already been closed. + kAbortTabClosed = 18, + + kMaxValue = kAbortTabClosed, +}; + +// User selection in the interception bubble. +enum class SigninInterceptionUserChoice { kAccept, kDecline, kGuest }; + +// User action resulting from the interception bubble. +// These values are persisted to logs. Entries should not be renumbered and +// numeric values should never be reused. +enum class SigninInterceptionResult { + kAccepted = 0, + kDeclined = 1, + kIgnored = 2, + + // Used when the bubble was not shown because it's not implemented. + kNotDisplayed = 3, + + // Accepted to be opened in Guest profile. + kAcceptedWithGuest = 4, + + kMaxValue = kAcceptedWithGuest, +}; + +// The ScopedDiceWebSigninInterceptionBubbleHandle closes the signin intercept +// bubble when it is destroyed, if the bubble is still opened. Note that this +// handle does not prevent the bubble from being closed for other reasons. +class ScopedDiceWebSigninInterceptionBubbleHandle { + public: + virtual ~ScopedDiceWebSigninInterceptionBubbleHandle() = 0; +}; + +// Returns whether the heuristic outcome is a success (the signin should be +// intercepted). +bool SigninInterceptionHeuristicOutcomeIsSuccess( + SigninInterceptionHeuristicOutcome outcome); + +// Called after web signed in, after a successful token exchange through Dice. +// The DiceWebSigninInterceptor may offer the user to create a new profile or +// switch to another existing profile. +// +// Implementation notes: here is how an entire interception flow work for the +// enterprise or multi-user case: +// * MaybeInterceptWebSignin() is called when the new signin happens. +// * Wait until the account info is downloaded. +// * Interception UI is shown by the delegate. Keep a handle on the bubble. +// * If the user approved, a new profile is created and the token is moved from +// this profile to the new profile, using DiceSignedInProfileCreator. +// * At this point, the flow ends in this profile, and continues in the new +// profile using DiceInterceptedSessionStartupHelper to add the account. +// * When the account is available on the web in the new profile: +// - A new browser window is created for the new profile, +// - The tab is moved to the new profile, +// - The interception bubble is closed by deleting the handle, +// - The profile customization bubble is shown. +class DiceWebSigninInterceptor : public KeyedService, + public signin::IdentityManager::Observer { + public: + enum class SigninInterceptionType { + kProfileSwitch, + kEnterprise, + kMultiUser, + kEnterpriseForced, + kProfileSwitchForced + }; + + // Delegate class responsible for showing the various interception UIs. + class Delegate { + public: + // Parameters for interception bubble UIs. + struct BubbleParameters { + SigninInterceptionType interception_type; + AccountInfo intercepted_account; + AccountInfo primary_account; + SkColor profile_highlight_color; + bool show_guest_option; + }; + + virtual ~Delegate() = default; + + // Shows the signin interception bubble and calls |callback| to indicate + // whether the user should continue in a new profile. + // The callback is never called if the delegate is deleted before it + // completes. + // May return a nullptr handle if the bubble cannot be shown. + // Warning: the handle closes the bubble when it is destroyed ; it is the + // responsibility of the caller to keep the handle alive until the bubble + // should be closed. + // The callback must not be called synchronously if this function returns a + // valid handle (because the caller needs to be able to close the bubble + // from the callback). + virtual std::unique_ptr + ShowSigninInterceptionBubble( + content::WebContents* web_contents, + const BubbleParameters& bubble_parameters, + base::OnceCallback callback) = 0; + + // Shows the profile customization bubble. + virtual void ShowProfileCustomizationBubble(Browser* browser) = 0; + }; + + DiceWebSigninInterceptor(Profile* profile, + std::unique_ptr delegate); + ~DiceWebSigninInterceptor() override; + + DiceWebSigninInterceptor(const DiceWebSigninInterceptor&) = delete; + DiceWebSigninInterceptor& operator=(const DiceWebSigninInterceptor&) = delete; + + static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry); + + // Called when an account has been added in Chrome from the web (using the + // DICE protocol). + // |web_contents| is the tab where the signin event happened. It must belong + // to the profile associated with this service. It may be nullptr if the tab + // was closed. + // |is_new_account| is true if the account was not already in Chrome (i.e. + // this is not a reauth). + // |is_sync_signin| is true if the user is signing in with the intent of + // enabling sync for that account. + // Virtual for testing. + virtual void MaybeInterceptWebSignin(content::WebContents* web_contents, + CoreAccountId account_id, + bool is_new_account, + bool is_sync_signin); + + // Called after the new profile was created during a signin interception. + // The token has been moved to the new profile, but the account is not yet in + // the cookies. + // `intercepted_contents` may be null if the tab was already closed. + // The intercepted web contents belong to the source profile (which is not the + // profile attached to this service). + void CreateBrowserAfterSigninInterception( + CoreAccountId account_id, + content::WebContents* intercepted_contents, + std::unique_ptr + bubble_handle, + bool is_new_profile); + + // Returns the outcome of the interception heuristic. + // If the outcome is kInterceptProfileSwitch, the target profile is returned + // in |entry|. + // In some cases the outcome cannot be fully computed synchronously, when this + // happens, the signin interception is highly likely (but not guaranteed). + absl::optional GetHeuristicOutcome( + bool is_new_account, + bool is_sync_signin, + const std::string& email, + const ProfileAttributesEntry** entry = nullptr) const; + + // Returns true if the interception is in progress (running the heuristic or + // showing on screen). + bool is_interception_in_progress() const { + return is_interception_in_progress_; + } + + void SetAccountLevelSigninRestrictionFetchResultForTesting( + absl::optional value) { + intercepted_account_level_policy_value_fetch_result_for_testing_ = + std::move(value); + } + + // KeyedService: + void Shutdown() override; + + private: + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest, + ShouldShowProfileSwitchBubble); + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest, + NoBubbleWithSingleAccount); + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest, + ShouldShowEnterpriseBubble); + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest, + ShouldShowEnterpriseBubbleWithoutUPA); + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest, + ShouldShowMultiUserBubble); + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest, PersistentHash); + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest, + ShouldEnforceEnterpriseProfileSeparation); + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest, + ShouldEnforceEnterpriseProfileSeparationWithoutUPA); + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest, + ShouldEnforceEnterpriseProfileSeparationReauth); + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest, + EnforceManagedAccountAsPrimary); + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest, + ShouldEnforceEnterpriseProfileSeparationReauth); + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorEnterpriseBrowserTest, + ForcedEnterpriseInterceptionTestAccountLevelPolicy); + FRIEND_TEST_ALL_PREFIXES( + DiceWebSigninInterceptorEnterpriseBrowserTest, + ForcedEnterpriseInterceptionTestNoForcedInterception); + + // Cancels any current signin interception and resets the interceptor to its + // initial state. + void Reset(); + + // Helper functions to determine which interception UI should be shown. + const ProfileAttributesEntry* ShouldShowProfileSwitchBubble( + const std::string& intercepted_email, + ProfileAttributesStorage* profile_attribute_storage) const; + bool ShouldEnforceEnterpriseProfileSeparation( + const AccountInfo& intercepted_account_info) const; + bool ShouldShowEnterpriseBubble( + const AccountInfo& intercepted_account_info) const; + bool ShouldShowMultiUserBubble( + const AccountInfo& intercepted_account_info) const; + + void OnInterceptionReadyToBeProcessed(const AccountInfo& info); + + // signin::IdentityManager::Observer: + void OnExtendedAccountInfoUpdated(const AccountInfo& info) override; + + // Called when the extended account info was not updated after a timeout. + void OnExtendedAccountInfoFetchTimeout(); + + // Called after the user chose whether a new profile would be created. + void OnProfileCreationChoice(const AccountInfo& account_info, + SkColor profile_color, + SigninInterceptionResult create); + // Called after the user chose whether the session should continue in a new + // profile. + void OnProfileSwitchChoice(const std::string& email, + const base::FilePath& profile_path, + SigninInterceptionResult switch_profile); + + // Called when the new profile is created or loaded from disk. + // `profile_color` is set as theme color for the profile ; it should be + // nullopt if the profile is not new (loaded from disk). + void OnNewSignedInProfileCreated(absl::optional profile_color, + Profile* new_profile); + + // Called after the user choses whether the session should continue in a new + // work profile or not. If the user choses not to continue in a work profile, + // the account is signed out. + void OnEnterpriseProfileCreationResult(const AccountInfo& account_info, + SkColor profile_color, + SigninInterceptionResult create); + + // Called when the new browser is created after interception. Passed as + // callback to `session_startup_helper_`. + void OnNewBrowserCreated(bool is_new_profile); + + // Returns a 8-bit hash of the email that can be persisted. + static std::string GetPersistentEmailHash(const std::string& email); + + // Should be called when the user declines profile creation, in order to + // remember their decision. This information is stored in prefs. Only a hash + // of the email is saved, as Chrome does not need to store the actual email, + // but only need to compare emails. The hash has low entropy to ensure it + // cannot be reversed. + void RecordProfileCreationDeclined(const std::string& email); + + // Checks if the user previously declined 2 times creating a new profile for + // this account. + bool HasUserDeclinedProfileCreation(const std::string& email) const; + + // Fetches the value of the cloud user level value of the + // ManagedAccountsSigninRestriction policy for 'account_info' and runs + // `callback` with the result. This is a network call that has a 5 seconds + // timeout. + void FetchAccountLevelSigninRestrictionForInterceptedAccount( + const AccountInfo& account_info, + base::OnceCallback callback); + + // Called when the the value of the cloud user level value of the + // ManagedAccountsSigninRestriction is received. + void OnAccountLevelManagedAccountsSigninRestrictionReceived( + bool timed_out, + const AccountInfo& account_info, + const std::string& signin_restriction); + + const raw_ptr profile_; + const raw_ptr identity_manager_; + std::unique_ptr delegate_; + + // Used in the profile that was created after the interception succeeded. + std::unique_ptr session_startup_helper_; + + // Members below are related to the interception in progress. + base::WeakPtr web_contents_; + bool is_interception_in_progress_ = false; + CoreAccountId account_id_; + bool new_account_interception_ = false; + bool intercepted_account_management_accepted_ = false; + base::ScopedObservation + account_info_update_observation_{this}; + // Timeout for the fetch of the extended account info. The signin interception + // is cancelled if the account info cannot be fetched quickly. + base::CancelableOnceCallback on_account_info_update_timeout_; + std::unique_ptr dice_signed_in_profile_creator_; + // Used to retain the interception UI bubble until profile creation completes. + std::unique_ptr + interception_bubble_handle_; + // Used for metrics: + bool was_interception_ui_displayed_ = false; + base::TimeTicks account_info_fetch_start_time_; + base::TimeTicks profile_creation_start_time_; + + // Timeout for the fetch of cloud user level policy value of + // ManagedAccountsSigninRestriction. The signin interception continue with an + // empty value for the policy if we cannot get the value. + base::CancelableOnceCallback + on_intercepted_account_level_policy_value_timeout_; + + // Used to fetch the cloud user level policy value of + // ManagedAccountsSigninRestriction. This can only fetch one policy value for + // one account at the time. + std::unique_ptr + account_level_signin_restriction_policy_fetcher_; + // Value of the ManagedAccountsSigninRestriction for the intercepted account. + // If no value is set, then we have not yet received the policy value. + absl::optional intercepted_account_level_policy_value_; + absl::optional + intercepted_account_level_policy_value_fetch_result_for_testing_; +}; + +#endif // CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_H_ diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor_browsertest.cc b/chromium/chrome/browser/signin/dice_web_signin_interceptor_browsertest.cc new file mode 100644 index 00000000000..5a53ea95baa --- /dev/null +++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor_browsertest.cc @@ -0,0 +1,1200 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/dice_web_signin_interceptor.h" + +#include +#include + +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/run_loop.h" +#include "base/strings/utf_string_conversions.h" +#include "base/test/bind.h" +#include "base/test/metrics/histogram_tester.h" +#include "base/threading/thread_task_runner_handle.h" +#include "build/build_config.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/enterprise/util/managed_browser_utils.h" +#include "chrome/browser/profiles/profile_attributes_entry.h" +#include "chrome/browser/profiles/profile_attributes_init_params.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/profiles/profile_manager_observer.h" +#include "chrome/browser/profiles/profile_window.h" +#include "chrome/browser/signin/chrome_signin_client_factory.h" +#include "chrome/browser/signin/chrome_signin_client_test_util.h" +#include "chrome/browser/signin/dice_intercepted_session_startup_helper.h" +#include "chrome/browser/signin/dice_web_signin_interceptor_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h" +#include "chrome/browser/signin/signin_features.h" +#include "chrome/browser/themes/theme_service.h" +#include "chrome/browser/themes/theme_service_factory.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_list.h" +#include "chrome/common/pref_names.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/profile_waiter.h" +#include "chrome/test/base/ui_test_utils.h" +#include "components/account_id/account_id.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/policy/core/common/features.h" +#include "components/prefs/pref_service.h" +#include "components/signin/public/identity_manager/accounts_mutator.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "components/signin/public/identity_manager/primary_account_mutator.h" +#include "content/public/test/browser_test.h" +#include "google_apis/gaia/gaia_urls.h" +#include "services/network/test/test_url_loader_factory.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace { + +// Fake response for OAuth multilogin. +const char kMultiloginSuccessResponse[] = + R"()]}' + { + "status": "OK", + "cookies":[ + { + "name":"SID", + "value":"SID_value", + "domain":".google.fr", + "path":"/", + "isSecure":true, + "isHttpOnly":false, + "priority":"HIGH", + "maxAge":63070000 + } + ] + } + )"; + +class FakeDiceWebSigninInterceptorDelegate; + +class FakeBubbleHandle : public ScopedDiceWebSigninInterceptionBubbleHandle, + public base::SupportsWeakPtr { + public: + ~FakeBubbleHandle() override = default; +}; + +// Dummy interception delegate that automatically accepts multi user +// interception. +class FakeDiceWebSigninInterceptorDelegate + : public DiceWebSigninInterceptor::Delegate { + public: + std::unique_ptr + ShowSigninInterceptionBubble( + content::WebContents* web_contents, + const BubbleParameters& bubble_parameters, + base::OnceCallback callback) override { + EXPECT_EQ(bubble_parameters.interception_type, expected_interception_type_); + auto bubble_handle = std::make_unique(); + weak_bubble_handle_ = bubble_handle->AsWeakPtr(); + // The callback must not be called synchronously (see the documentation for + // ShowSigninInterceptionBubble). + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(std::move(callback), expected_interception_result_)); + return bubble_handle; + } + + void ShowProfileCustomizationBubble(Browser* browser) override { + EXPECT_FALSE(customized_browser_) + << "Customization must be shown only once."; + customized_browser_ = browser; + } + + Browser* customized_browser() { return customized_browser_; } + + void set_expected_interception_type( + DiceWebSigninInterceptor::SigninInterceptionType type) { + expected_interception_type_ = type; + } + + void set_expected_interception_result(SigninInterceptionResult result) { + expected_interception_result_ = result; + } + + bool intercept_bubble_shown() const { return weak_bubble_handle_.get(); } + + bool intercept_bubble_destroyed() const { + return weak_bubble_handle_.WasInvalidated(); + } + + private: + raw_ptr customized_browser_ = nullptr; + DiceWebSigninInterceptor::SigninInterceptionType expected_interception_type_ = + DiceWebSigninInterceptor::SigninInterceptionType::kMultiUser; + SigninInterceptionResult expected_interception_result_ = + SigninInterceptionResult::kAccepted; + base::WeakPtr weak_bubble_handle_; +}; + +class BrowserCloseObserver : public BrowserListObserver { + public: + explicit BrowserCloseObserver(Browser* browser) : browser_(browser) { + BrowserList::AddObserver(this); + } + + BrowserCloseObserver(const BrowserCloseObserver&) = delete; + BrowserCloseObserver& operator=(const BrowserCloseObserver&) = delete; + + ~BrowserCloseObserver() override { BrowserList::RemoveObserver(this); } + + void Wait() { run_loop_.Run(); } + + // BrowserListObserver implementation. + void OnBrowserRemoved(Browser* browser) override { + if (browser == browser_) + run_loop_.Quit(); + } + + private: + raw_ptr browser_; + base::RunLoop run_loop_; +}; + +// Runs the interception and returns the new profile that was created. +Profile* InterceptAndWaitProfileCreation(content::WebContents* contents, + const CoreAccountId& account_id) { + ProfileWaiter profile_waiter; + // Start the interception. + DiceWebSigninInterceptor* interceptor = + DiceWebSigninInterceptorFactory::GetForProfile( + Profile::FromBrowserContext(contents->GetBrowserContext())); + interceptor->MaybeInterceptWebSignin(contents, account_id, + /*is_new_account=*/true, + /*is_sync_signin=*/false); + // Wait for the interception to be complete. + return profile_waiter.WaitForProfileAdded(); +} + +// Checks that the interception histograms were correctly recorded. +void CheckHistograms(const base::HistogramTester& histogram_tester, + SigninInterceptionHeuristicOutcome outcome, + bool reauth = false) { + int profile_switch_count = + outcome == SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch || + outcome == SigninInterceptionHeuristicOutcome:: + kInterceptEnterpriseForcedProfileSwitch + ? 1 + : 0; + int profile_creation_count = reauth ? 0 : 1 - profile_switch_count; + int fetched_account_count = + reauth || outcome == SigninInterceptionHeuristicOutcome:: + kInterceptEnterpriseForcedProfileSwitch + ? 1 + : profile_creation_count; + + histogram_tester.ExpectUniqueSample("Signin.Intercept.HeuristicOutcome", + outcome, 1); + histogram_tester.ExpectTotalCount( + "Signin.Intercept.AccountInfoFetchDuration", + base::FeatureList::IsEnabled( + policy::features::kEnableUserCloudSigninRestrictionPolicyFetcher) + ? 1 + : fetched_account_count); + histogram_tester.ExpectTotalCount("Signin.Intercept.ProfileCreationDuration", + profile_creation_count); + histogram_tester.ExpectTotalCount("Signin.Intercept.ProfileSwitchDuration", + profile_switch_count); +} + +} // namespace + +class DiceWebSigninInterceptorBrowserTest : public InProcessBrowserTest { + public: + DiceWebSigninInterceptorBrowserTest() = default; + + Profile* profile() { return browser()->profile(); } + + signin::IdentityTestEnvironment* identity_test_env() { + return identity_test_env_profile_adaptor_->identity_test_env(); + } + + network::TestURLLoaderFactory* test_url_loader_factory() { + return &test_url_loader_factory_; + } + + content::WebContents* AddTab(const GURL& url) { + ui_test_utils::NavigateToURLWithDisposition( + browser(), url, WindowOpenDisposition::NEW_FOREGROUND_TAB, + ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); + return browser()->tab_strip_model()->GetActiveWebContents(); + } + + FakeDiceWebSigninInterceptorDelegate* GetInterceptorDelegate( + Profile* profile) { + // Make sure the interceptor has been created. + DiceWebSigninInterceptorFactory::GetForProfile(profile); + FakeDiceWebSigninInterceptorDelegate* interceptor_delegate = + interceptor_delegates_[profile]; + return interceptor_delegate; + } + + void SetupGaiaResponses() { + // Instantly return from Gaia calls, to avoid timing out when injecting the + // account in the new profile. + network::TestURLLoaderFactory* loader_factory = test_url_loader_factory(); + loader_factory->SetInterceptor(base::BindLambdaForTesting( + [loader_factory](const network::ResourceRequest& request) { + std::string path = request.url.path(); + if (path == "/ListAccounts" || path == "/GetCheckConnectionInfo") { + loader_factory->AddResponse(request.url.spec(), std::string()); + return; + } + if (path == "/oauth/multilogin") { + loader_factory->AddResponse(request.url.spec(), + kMultiloginSuccessResponse); + return; + } + })); + } + + private: + // InProcessBrowserTest: + void SetUpOnMainThread() override { + ASSERT_TRUE(embedded_test_server()->Start()); + identity_test_env_profile_adaptor_ = + std::make_unique(profile()); + DiceWebSigninInterceptorFactory::GetForProfile(profile()) + ->SetAccountLevelSigninRestrictionFetchResultForTesting(""); + } + + void TearDownOnMainThread() override { + // Must be destroyed before the Profile. + identity_test_env_profile_adaptor_.reset(); + } + + void SetUpInProcessBrowserTestFixture() override { + InProcessBrowserTest::SetUpInProcessBrowserTestFixture(); + create_services_subscription_ = + BrowserContextDependencyManager::GetInstance() + ->RegisterCreateServicesCallbackForTesting( + base::BindRepeating(&DiceWebSigninInterceptorBrowserTest:: + OnWillCreateBrowserContextServices, + base::Unretained(this))); + } + + void OnWillCreateBrowserContextServices(content::BrowserContext* context) { + IdentityTestEnvironmentProfileAdaptor:: + SetIdentityTestEnvironmentFactoriesOnBrowserContext(context); + ChromeSigninClientFactory::GetInstance()->SetTestingFactory( + context, base::BindRepeating(&BuildChromeSigninClientWithURLLoader, + &test_url_loader_factory_)); + DiceWebSigninInterceptorFactory::GetInstance()->SetTestingFactory( + context, + base::BindRepeating(&DiceWebSigninInterceptorBrowserTest:: + BuildDiceWebSigninInterceptorWithFakeDelegate, + base::Unretained(this))); + } + + // Builds a DiceWebSigninInterceptor with a fake delegate. To be used as a + // testing factory. + std::unique_ptr BuildDiceWebSigninInterceptorWithFakeDelegate( + content::BrowserContext* context) { + std::unique_ptr fake_delegate = + std::make_unique(); + interceptor_delegates_[context] = fake_delegate.get(); + return std::make_unique( + Profile::FromBrowserContext(context), std::move(fake_delegate)); + } + + network::TestURLLoaderFactory test_url_loader_factory_; + std::unique_ptr + identity_test_env_profile_adaptor_; + base::CallbackListSubscription create_services_subscription_; + std::map + interceptor_delegates_; +}; + +// Tests the complete interception flow including profile and browser creation. +IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorBrowserTest, InterceptionTest) { + base::HistogramTester histogram_tester; + // Setup profile for interception. + identity_test_env()->MakeAccountAvailable("alice@example.com"); + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + // Fill the account info, in particular for the hosted_domain field. + account_info.full_name = "fullname"; + account_info.given_name = "givenname"; + account_info.hosted_domain = kNoHostedDomainFound; + account_info.locale = "en"; + account_info.picture_url = "https://example.com"; + DCHECK(account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + SetupGaiaResponses(); + + // Add a tab. + GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse"); + content::WebContents* web_contents = AddTab(intercepted_url); + int original_tab_count = browser()->tab_strip_model()->count(); + + // Do the signin interception. + EXPECT_EQ(BrowserList::GetInstance()->size(), 1u); + Profile* new_profile = + InterceptAndWaitProfileCreation(web_contents, account_info.account_id); + ASSERT_TRUE(new_profile); + FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate = + GetInterceptorDelegate(profile()); + EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown()); + signin::IdentityManager* new_identity_manager = + IdentityManagerFactory::GetForProfile(new_profile); + EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken( + account_info.account_id)); + + IdentityTestEnvironmentProfileAdaptor adaptor(new_profile); + adaptor.identity_test_env()->SetAutomaticIssueOfAccessTokens(true); + + // Check the profile name. + ProfileAttributesStorage& storage = + g_browser_process->profile_manager()->GetProfileAttributesStorage(); + ProfileAttributesEntry* entry = + storage.GetProfileAttributesWithPath(new_profile->GetPath()); + ASSERT_TRUE(entry); + EXPECT_EQ("givenname", base::UTF16ToUTF8(entry->GetLocalProfileName())); + // Check the profile color. + EXPECT_TRUE(ThemeServiceFactory::GetForProfile(new_profile) + ->UsingAutogeneratedTheme()); + + // A browser has been created for the new profile and the tab was moved there. + Browser* added_browser = ui_test_utils::WaitForBrowserToOpen(); + ASSERT_TRUE(added_browser); + ASSERT_EQ(BrowserList::GetInstance()->size(), 2u); + EXPECT_EQ(added_browser->profile(), new_profile); + EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1); + EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(), + intercepted_url); + + CheckHistograms(histogram_tester, + SigninInterceptionHeuristicOutcome::kInterceptMultiUser); + // Interception bubble is destroyed in the source profile, and was not shown + // in the new profile. + FakeDiceWebSigninInterceptorDelegate* new_interceptor_delegate = + GetInterceptorDelegate(new_profile); + EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_destroyed()); + EXPECT_FALSE(new_interceptor_delegate->intercept_bubble_shown()); + EXPECT_FALSE(new_interceptor_delegate->intercept_bubble_destroyed()); + // Profile customization UI was shown exactly once in the new profile. + EXPECT_EQ(new_interceptor_delegate->customized_browser(), added_browser); + EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr); +} + +// Tests the complete profile switch flow when the profile is not loaded. +IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorBrowserTest, SwitchAndLoad) { + base::HistogramTester histogram_tester; + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + // Fill the account info, in particular for the hosted_domain field. + account_info.full_name = "fullname"; + account_info.given_name = "givenname"; + account_info.hosted_domain = "example.com"; + account_info.locale = "en"; + account_info.picture_url = "https://example.com"; + DCHECK(account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + // Add a profile in the cache (simulate the profile on disk). + ProfileManager* profile_manager = g_browser_process->profile_manager(); + ProfileAttributesStorage* profile_storage = + &profile_manager->GetProfileAttributesStorage(); + const base::FilePath profile_path = + profile_manager->GenerateNextProfileDirectoryPath(); + ProfileAttributesInitParams params; + params.profile_path = profile_path; + params.profile_name = u"TestProfileName"; + params.gaia_id = account_info.gaia; + params.user_name = base::UTF8ToUTF16(account_info.email); + profile_storage->AddProfile(std::move(params)); + ProfileAttributesEntry* entry = + profile_storage->GetProfileAttributesWithPath(profile_path); + ASSERT_TRUE(entry); + ASSERT_EQ(entry->GetGAIAId(), account_info.gaia); + + // Add a tab. + GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse"); + content::WebContents* web_contents = AddTab(intercepted_url); + int original_tab_count = browser()->tab_strip_model()->count(); + + // Do the signin interception. + FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate = + GetInterceptorDelegate(profile()); + source_interceptor_delegate->set_expected_interception_type( + DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch); + Profile* new_profile = + InterceptAndWaitProfileCreation(web_contents, account_info.account_id); + ASSERT_TRUE(new_profile); + EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown()); + signin::IdentityManager* new_identity_manager = + IdentityManagerFactory::GetForProfile(new_profile); + EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken( + account_info.account_id)); + + // Check that the right profile was opened. + EXPECT_EQ(new_profile->GetPath(), profile_path); + + // Add the account to the cookies (simulates the account reconcilor). + EXPECT_EQ(BrowserList::GetInstance()->size(), 1u); + signin::SetCookieAccounts(new_identity_manager, test_url_loader_factory(), + {{account_info.email, account_info.gaia}}); + + // A browser has been created for the new profile and the tab was moved there. + ASSERT_EQ(BrowserList::GetInstance()->size(), 2u); + Browser* added_browser = BrowserList::GetInstance()->get(1); + ASSERT_TRUE(added_browser); + EXPECT_EQ(added_browser->profile(), new_profile); + EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1); + EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(), + intercepted_url); + + CheckHistograms(histogram_tester, + SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch); + // Interception bubble was closed. + EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_destroyed()); + // Profile customization was not shown. + EXPECT_EQ(GetInterceptorDelegate(new_profile)->customized_browser(), nullptr); + EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr); +} + +// Tests the complete profile switch flow when the profile is already loaded. +IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorBrowserTest, SwitchAlreadyOpen) { + base::HistogramTester histogram_tester; + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + // Fill the account info, in particular for the hosted_domain field. + account_info.full_name = "fullname"; + account_info.given_name = "givenname"; + account_info.hosted_domain = "example.com"; + account_info.locale = "en"; + account_info.picture_url = "https://example.com"; + DCHECK(account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + // Create another profile with a browser window. + ProfileManager* profile_manager = g_browser_process->profile_manager(); + const base::FilePath profile_path = + profile_manager->GenerateNextProfileDirectoryPath(); + base::RunLoop loop; + Profile* other_profile = nullptr; + ProfileManager::CreateCallback callback = base::BindLambdaForTesting( + [&other_profile, &loop](Profile* profile, Profile::CreateStatus status) { + DCHECK_EQ(status, Profile::CREATE_STATUS_INITIALIZED); + other_profile = profile; + loop.Quit(); + }); + profiles::SwitchToProfile(profile_path, /*always_create=*/true, + std::move(callback)); + loop.Run(); + ASSERT_TRUE(other_profile); + ASSERT_EQ(BrowserList::GetInstance()->size(), 2u); + Browser* other_browser = BrowserList::GetInstance()->get(1); + ASSERT_TRUE(other_browser); + ASSERT_EQ(other_browser->profile(), other_profile); + // Add the account to the other profile. + signin::IdentityManager* other_identity_manager = + IdentityManagerFactory::GetForProfile(other_profile); + other_identity_manager->GetAccountsMutator()->AddOrUpdateAccount( + account_info.gaia, account_info.email, "dummy_refresh_token", + /*is_under_advanced_protection=*/false, + signin_metrics::SourceForRefreshTokenOperation::kUnknown); + other_identity_manager->GetPrimaryAccountMutator()->SetPrimaryAccount( + account_info.account_id, signin::ConsentLevel::kSync); + + // Add a tab. + GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse"); + content::WebContents* web_contents = AddTab(intercepted_url); + int original_tab_count = browser()->tab_strip_model()->count(); + int other_original_tab_count = other_browser->tab_strip_model()->count(); + + // Start the interception. + GetInterceptorDelegate(profile())->set_expected_interception_type( + DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch); + DiceWebSigninInterceptor* interceptor = + DiceWebSigninInterceptorFactory::GetForProfile(profile()); + interceptor->MaybeInterceptWebSignin(web_contents, account_info.account_id, + /*is_new_account=*/true, + /*is_sync_signin=*/false); + + // Add the account to the cookies (simulates the account reconcilor). + signin::SetCookieAccounts(other_identity_manager, test_url_loader_factory(), + {{account_info.email, account_info.gaia}}); + + // The tab was moved to the new browser. + ASSERT_EQ(BrowserList::GetInstance()->size(), 2u); + EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1); + EXPECT_EQ(other_browser->tab_strip_model()->count(), + other_original_tab_count + 1); + EXPECT_EQ(other_browser->tab_strip_model()->GetActiveWebContents()->GetURL(), + intercepted_url); + + CheckHistograms(histogram_tester, + SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch); + // Profile customization was not shown. + EXPECT_EQ(GetInterceptorDelegate(other_profile)->customized_browser(), + nullptr); + EXPECT_EQ(GetInterceptorDelegate(profile())->customized_browser(), nullptr); +} + +// Close the source tab during the interception and check that the NTP is opened +// in the new profile (regression test for https://crbug.com/1153321). +IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorBrowserTest, CloseSourceTab) { + // Setup profile for interception. + identity_test_env()->MakeAccountAvailable("alice@example.com"); + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + // Fill the account info, in particular for the hosted_domain field. + account_info.full_name = "fullname"; + account_info.given_name = "givenname"; + account_info.hosted_domain = kNoHostedDomainFound; + account_info.locale = "en"; + account_info.picture_url = "https://example.com"; + DCHECK(account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + // Add a tab. + GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse"); + content::WebContents* contents = AddTab(intercepted_url); + int original_tab_count = browser()->tab_strip_model()->count(); + + // Do the signin interception. + ProfileWaiter profile_waiter; + DiceWebSigninInterceptor* interceptor = + DiceWebSigninInterceptorFactory::GetForProfile( + Profile::FromBrowserContext(contents->GetBrowserContext())); + interceptor->MaybeInterceptWebSignin(contents, account_info.account_id, + /*is_new_account=*/true, + /*is_sync_signin=*/false); + // Close the source tab during the profile creation. + contents->Close(); + // Wait for the interception to be complete. + Profile* new_profile = profile_waiter.WaitForProfileAdded(); + ASSERT_TRUE(new_profile); + signin::IdentityManager* new_identity_manager = + IdentityManagerFactory::GetForProfile(new_profile); + EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken( + account_info.account_id)); + + // Add the account to the cookies (simulates the account reconcilor). + EXPECT_EQ(BrowserList::GetInstance()->size(), 1u); + signin::SetCookieAccounts(new_identity_manager, test_url_loader_factory(), + {{account_info.email, account_info.gaia}}); + + // A browser has been created for the new profile on the new tab page. + ASSERT_EQ(BrowserList::GetInstance()->size(), 2u); + Browser* added_browser = BrowserList::GetInstance()->get(1); + ASSERT_TRUE(added_browser); + EXPECT_EQ(added_browser->profile(), new_profile); + EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1); + EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(), + GURL("chrome://newtab/")); +} + +class DiceWebSigninInterceptorEnterpriseBrowserTest + : public DiceWebSigninInterceptorBrowserTest { + public: + DiceWebSigninInterceptorEnterpriseBrowserTest() { + enterprise_feature_list_.InitWithFeatures( + {kAccountPoliciesLoadedWithoutSync, + policy::features::kEnableUserCloudSigninRestrictionPolicyFetcher}, + {}); + } + + private: + base::test::ScopedFeatureList enterprise_feature_list_; +}; + +// Tests the complete interception flow including profile and browser creation. +IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorEnterpriseBrowserTest, + ForcedEnterpriseInterceptionTestNoForcedInterception) { + base::HistogramTester histogram_tester; + + AccountInfo primary_account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + // Fill the account info, in particular for the hosted_domain field. + primary_account_info.full_name = "fullname"; + primary_account_info.given_name = "givenname"; + primary_account_info.hosted_domain = "example.com"; + primary_account_info.locale = "en"; + primary_account_info.picture_url = "https://example.com"; + DCHECK(primary_account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(primary_account_info); + IdentityManagerFactory::GetForProfile(profile()) + ->GetPrimaryAccountMutator() + ->SetPrimaryAccount(primary_account_info.account_id, + signin::ConsentLevel::kSync); + + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + // Fill the account info, in particular for the hosted_domain field. + account_info.full_name = "fullname"; + account_info.given_name = "givenname"; + account_info.hosted_domain = "example.com"; + account_info.locale = "en"; + account_info.picture_url = "https://example.com"; + DCHECK(account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + // Enforce enterprise profile sepatation. + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "none"); + DiceWebSigninInterceptorFactory::GetForProfile(profile()) + ->SetAccountLevelSigninRestrictionFetchResultForTesting(""); + + // Instantly return from Gaia calls, to avoid timing out when injecting the + // account in the new profile. + network::TestURLLoaderFactory* loader_factory = test_url_loader_factory(); + loader_factory->SetInterceptor(base::BindLambdaForTesting( + [loader_factory](const network::ResourceRequest& request) { + std::string path = request.url.path(); + if (path == "/ListAccounts" || path == "/GetCheckConnectionInfo") { + loader_factory->AddResponse(request.url.spec(), std::string()); + return; + } + if (path == "/oauth/multilogin") { + loader_factory->AddResponse(request.url.spec(), + kMultiloginSuccessResponse); + return; + } + })); + + // Add a tab. + GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse"); + content::WebContents* web_contents = AddTab(intercepted_url); + int original_tab_count = browser()->tab_strip_model()->count(); + + // Do the signin interception. + EXPECT_EQ(BrowserList::GetInstance()->size(), 1u); + FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate = + GetInterceptorDelegate(profile()); + source_interceptor_delegate->set_expected_interception_type( + DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise); + Profile* new_profile = + InterceptAndWaitProfileCreation(web_contents, account_info.account_id); + EXPECT_FALSE( + chrome::enterprise_util::UserAcceptedAccountManagement(new_profile)); + ASSERT_TRUE(new_profile); + EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown()); + signin::IdentityManager* new_identity_manager = + IdentityManagerFactory::GetForProfile(new_profile); + EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken( + account_info.account_id)); + + IdentityTestEnvironmentProfileAdaptor adaptor(new_profile); + adaptor.identity_test_env()->SetAutomaticIssueOfAccessTokens(true); + + // Check the profile name. + ProfileAttributesStorage& storage = + g_browser_process->profile_manager()->GetProfileAttributesStorage(); + ProfileAttributesEntry* entry = + storage.GetProfileAttributesWithPath(new_profile->GetPath()); + ASSERT_TRUE(entry); + EXPECT_EQ("example.com", base::UTF16ToUTF8(entry->GetLocalProfileName())); + // Check the profile color. + EXPECT_TRUE(ThemeServiceFactory::GetForProfile(new_profile) + ->UsingAutogeneratedTheme()); + + // A browser has been created for the new profile and the tab was moved there. + Browser* added_browser = ui_test_utils::WaitForBrowserToOpen(); + ASSERT_TRUE(added_browser); + ASSERT_EQ(BrowserList::GetInstance()->size(), 2u); + EXPECT_EQ(added_browser->profile(), new_profile); + EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1); + EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(), + intercepted_url); + + CheckHistograms(histogram_tester, + SigninInterceptionHeuristicOutcome::kInterceptEnterprise); + // Interception bubble is destroyed in the source profile, and was not shown + // in the new profile. + FakeDiceWebSigninInterceptorDelegate* new_interceptor_delegate = + GetInterceptorDelegate(new_profile); + + // Profile customization UI was shown exactly once in the new profile. + EXPECT_EQ(new_interceptor_delegate->customized_browser(), added_browser); + EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr); +} + +// Tests the complete interception flow including profile and browser creation. +IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorEnterpriseBrowserTest, + ForcedEnterpriseInterceptionTestAccountLevelPolicy) { + base::HistogramTester histogram_tester; + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + // Fill the account info, in particular for the hosted_domain field. + account_info.full_name = "fullname"; + account_info.given_name = "givenname"; + account_info.hosted_domain = "example.com"; + account_info.locale = "en"; + account_info.picture_url = "https://example.com"; + DCHECK(account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + // Enforce enterprise profile sepatation. + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "none"); + DiceWebSigninInterceptorFactory::GetForProfile(profile()) + ->SetAccountLevelSigninRestrictionFetchResultForTesting( + "primary_account"); + + // Instantly return from Gaia calls, to avoid timing out when injecting the + // account in the new profile. + network::TestURLLoaderFactory* loader_factory = test_url_loader_factory(); + loader_factory->SetInterceptor(base::BindLambdaForTesting( + [loader_factory](const network::ResourceRequest& request) { + std::string path = request.url.path(); + if (path == "/ListAccounts" || path == "/GetCheckConnectionInfo") { + loader_factory->AddResponse(request.url.spec(), std::string()); + return; + } + if (path == "/oauth/multilogin") { + loader_factory->AddResponse(request.url.spec(), + kMultiloginSuccessResponse); + return; + } + })); + + // Add a tab. + GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse"); + content::WebContents* web_contents = AddTab(intercepted_url); + int original_tab_count = browser()->tab_strip_model()->count(); + + // Do the signin interception. + EXPECT_EQ(BrowserList::GetInstance()->size(), 1u); + FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate = + GetInterceptorDelegate(profile()); + source_interceptor_delegate->set_expected_interception_type( + DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced); + Profile* new_profile = + InterceptAndWaitProfileCreation(web_contents, account_info.account_id); + EXPECT_TRUE( + chrome::enterprise_util::UserAcceptedAccountManagement(new_profile)); + ASSERT_TRUE(new_profile); + EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown()); + signin::IdentityManager* new_identity_manager = + IdentityManagerFactory::GetForProfile(new_profile); + EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken( + account_info.account_id)); + + IdentityTestEnvironmentProfileAdaptor adaptor(new_profile); + adaptor.identity_test_env()->SetAutomaticIssueOfAccessTokens(true); + + // Check the profile name. + ProfileAttributesStorage& storage = + g_browser_process->profile_manager()->GetProfileAttributesStorage(); + ProfileAttributesEntry* entry = + storage.GetProfileAttributesWithPath(new_profile->GetPath()); + ASSERT_TRUE(entry); + EXPECT_EQ("example.com", base::UTF16ToUTF8(entry->GetLocalProfileName())); + // Check the profile color. + EXPECT_TRUE(ThemeServiceFactory::GetForProfile(new_profile) + ->UsingAutogeneratedTheme()); + + // A browser has been created for the new profile and the tab was moved there. + Browser* added_browser = ui_test_utils::WaitForBrowserToOpen(); + ASSERT_TRUE(added_browser); + ASSERT_EQ(BrowserList::GetInstance()->size(), 2u); + EXPECT_EQ(added_browser->profile(), new_profile); + EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1); + EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(), + intercepted_url); + + CheckHistograms( + histogram_tester, + SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced); + // Interception bubble is destroyed in the source profile, and was not shown + // in the new profile. + FakeDiceWebSigninInterceptorDelegate* new_interceptor_delegate = + GetInterceptorDelegate(new_profile); + + // Profile customization UI was shown exactly once in the new profile. + EXPECT_EQ(new_interceptor_delegate->customized_browser(), added_browser); + EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr); +} + +// Tests the complete interception flow including profile and browser creation. +IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorEnterpriseBrowserTest, + ForcedEnterpriseInterceptionTest) { + base::HistogramTester histogram_tester; + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + // Fill the account info, in particular for the hosted_domain field. + account_info.full_name = "fullname"; + account_info.given_name = "givenname"; + account_info.hosted_domain = "example.com"; + account_info.locale = "en"; + account_info.picture_url = "https://example.com"; + DCHECK(account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + // Enforce enterprise profile separation. + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account_strict"); + + SetupGaiaResponses(); + + // Add a tab. + GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse"); + content::WebContents* web_contents = AddTab(intercepted_url); + int original_tab_count = browser()->tab_strip_model()->count(); + + // Do the signin interception. + EXPECT_EQ(BrowserList::GetInstance()->size(), 1u); + FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate = + GetInterceptorDelegate(profile()); + source_interceptor_delegate->set_expected_interception_type( + DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced); + Profile* new_profile = + InterceptAndWaitProfileCreation(web_contents, account_info.account_id); + EXPECT_TRUE( + chrome::enterprise_util::UserAcceptedAccountManagement(new_profile)); + ASSERT_TRUE(new_profile); + EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown()); + signin::IdentityManager* new_identity_manager = + IdentityManagerFactory::GetForProfile(new_profile); + EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken( + account_info.account_id)); + + IdentityTestEnvironmentProfileAdaptor adaptor(new_profile); + adaptor.identity_test_env()->SetAutomaticIssueOfAccessTokens(true); + + // Check the profile name. + ProfileAttributesStorage& storage = + g_browser_process->profile_manager()->GetProfileAttributesStorage(); + ProfileAttributesEntry* entry = + storage.GetProfileAttributesWithPath(new_profile->GetPath()); + ASSERT_TRUE(entry); + EXPECT_EQ("example.com", base::UTF16ToUTF8(entry->GetLocalProfileName())); + // Check the profile color. + EXPECT_TRUE(ThemeServiceFactory::GetForProfile(new_profile) + ->UsingAutogeneratedTheme()); + + // A browser has been created for the new profile and the tab was moved there. + Browser* added_browser = ui_test_utils::WaitForBrowserToOpen(); + ASSERT_TRUE(added_browser); + ASSERT_EQ(BrowserList::GetInstance()->size(), 2u); + EXPECT_EQ(added_browser->profile(), new_profile); + EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1); + EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(), + intercepted_url); + + CheckHistograms( + histogram_tester, + SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced); + // Interception bubble is destroyed in the source profile, and was not shown + // in the new profile. + FakeDiceWebSigninInterceptorDelegate* new_interceptor_delegate = + GetInterceptorDelegate(new_profile); + + // Profile customization UI was shown exactly once in the new profile. + EXPECT_EQ(new_interceptor_delegate->customized_browser(), added_browser); + EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr); +} + +// Tests the complete interception flow for a reauth of the primary account of a +// non-syncing profile. +IN_PROC_BROWSER_TEST_F( + DiceWebSigninInterceptorEnterpriseBrowserTest, + ForcedEnterpriseInterceptionPrimaryACcountReauthSyncDisabledTest) { + base::HistogramTester histogram_tester; + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + // Fill the account info, in particular for the hosted_domain field. + account_info.full_name = "fullname"; + account_info.given_name = "givenname"; + account_info.hosted_domain = "example.com"; + account_info.locale = "en"; + account_info.picture_url = "https://example.com"; + DCHECK(account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + IdentityManagerFactory::GetForProfile(profile()) + ->GetPrimaryAccountMutator() + ->SetPrimaryAccount(account_info.account_id, + signin::ConsentLevel::kSignin); + + // Enforce enterprise profile separation. + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account_strict"); + + SetupGaiaResponses(); + + // Add a tab. + GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse"); + content::WebContents* web_contents = AddTab(intercepted_url); + int original_tab_count = browser()->tab_strip_model()->count(); + + // Do the signin interception. + EXPECT_EQ(BrowserList::GetInstance()->size(), 1u); + FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate = + GetInterceptorDelegate(profile()); + source_interceptor_delegate->set_expected_interception_type( + DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced); + + EXPECT_FALSE( + chrome::enterprise_util::UserAcceptedAccountManagement(profile())); + // Start the interception. + DiceWebSigninInterceptor* interceptor = + DiceWebSigninInterceptorFactory::GetForProfile(profile()); + interceptor->MaybeInterceptWebSignin(web_contents, account_info.account_id, + /*is_new_account=*/false, + /*is_sync_signin=*/false); + base::RunLoop().RunUntilIdle(); + EXPECT_TRUE( + chrome::enterprise_util::UserAcceptedAccountManagement(profile())); + // Interception bubble was closed. + EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_destroyed()); + EXPECT_TRUE(IdentityManagerFactory::GetForProfile(profile()) + ->HasAccountWithRefreshToken(account_info.account_id)); + + ASSERT_EQ(BrowserList::GetInstance()->size(), 1u); + EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count); + EXPECT_EQ(browser()->tab_strip_model()->GetActiveWebContents()->GetURL(), + intercepted_url); + + CheckHistograms( + histogram_tester, + SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced, + /*reauth=*/true); +} + +// Tests the complete interception flow for a reauth of the primary account of a +// syncing profile. +IN_PROC_BROWSER_TEST_F( + DiceWebSigninInterceptorEnterpriseBrowserTest, + ForcedEnterpriseInterceptionPrimaryACcountReauthSyncEnabledTest) { + base::HistogramTester histogram_tester; + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + // Fill the account info, in particular for the hosted_domain field. + account_info.full_name = "fullname"; + account_info.given_name = "givenname"; + account_info.hosted_domain = "example.com"; + account_info.locale = "en"; + account_info.picture_url = "https://example.com"; + DCHECK(account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + IdentityManagerFactory::GetForProfile(profile()) + ->GetPrimaryAccountMutator() + ->SetPrimaryAccount(account_info.account_id, signin::ConsentLevel::kSync); + + // Enforce enterprise profile separation. + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account_strict"); + + SetupGaiaResponses(); + + // Add a tab. + GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse"); + content::WebContents* web_contents = AddTab(intercepted_url); + int original_tab_count = browser()->tab_strip_model()->count(); + + // Do the signin interception. + EXPECT_EQ(BrowserList::GetInstance()->size(), 1u); + FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate = + GetInterceptorDelegate(profile()); + source_interceptor_delegate->set_expected_interception_type( + DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced); + + EXPECT_FALSE( + chrome::enterprise_util::UserAcceptedAccountManagement(profile())); + // Start the interception. + DiceWebSigninInterceptor* interceptor = + DiceWebSigninInterceptorFactory::GetForProfile(profile()); + interceptor->MaybeInterceptWebSignin(web_contents, account_info.account_id, + /*is_new_account=*/false, + /*is_sync_signin=*/false); + base::RunLoop().RunUntilIdle(); + EXPECT_TRUE( + chrome::enterprise_util::UserAcceptedAccountManagement(profile())); + // Interception bubble was closed. + EXPECT_FALSE(source_interceptor_delegate->intercept_bubble_shown()); + EXPECT_FALSE(source_interceptor_delegate->intercept_bubble_destroyed()); + EXPECT_TRUE(IdentityManagerFactory::GetForProfile(profile()) + ->HasAccountWithRefreshToken(account_info.account_id)); + + ASSERT_EQ(BrowserList::GetInstance()->size(), 1u); + EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count); + EXPECT_EQ(browser()->tab_strip_model()->GetActiveWebContents()->GetURL(), + intercepted_url); + + CheckHistograms(histogram_tester, + SigninInterceptionHeuristicOutcome::kAbortAccountNotNew, + /*reauth=*/true); +} + +// Tests the complete profile switch flow when the profile is not loaded. +IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorEnterpriseBrowserTest, + EnterpriseSwitchAndLoad) { + base::HistogramTester histogram_tester; + // Enforce enterprise profile separation. + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account_strict"); + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + // Fill the account info, in particular for the hosted_domain field. + account_info.full_name = "fullname"; + account_info.given_name = "givenname"; + account_info.hosted_domain = "example.com"; + account_info.locale = "en"; + account_info.picture_url = "https://example.com"; + DCHECK(account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + // Add a profile in the cache (simulate the profile on disk). + ProfileManager* profile_manager = g_browser_process->profile_manager(); + ProfileAttributesStorage* profile_storage = + &profile_manager->GetProfileAttributesStorage(); + const base::FilePath profile_path = + profile_manager->GenerateNextProfileDirectoryPath(); + ProfileAttributesInitParams params; + params.profile_path = profile_path; + params.profile_name = u"TestProfileName"; + params.gaia_id = account_info.gaia; + params.user_name = base::UTF8ToUTF16(account_info.email); + profile_storage->AddProfile(std::move(params)); + ProfileAttributesEntry* entry = + profile_storage->GetProfileAttributesWithPath(profile_path); + ASSERT_TRUE(entry); + ASSERT_EQ(entry->GetGAIAId(), account_info.gaia); + + // Add a tab. + GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse"); + content::WebContents* web_contents = AddTab(intercepted_url); + int original_tab_count = browser()->tab_strip_model()->count(); + + // Do the signin interception. + FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate = + GetInterceptorDelegate(profile()); + source_interceptor_delegate->set_expected_interception_type( + DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitchForced); + Profile* new_profile = + InterceptAndWaitProfileCreation(web_contents, account_info.account_id); + ASSERT_TRUE(new_profile); + EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown()); + signin::IdentityManager* new_identity_manager = + IdentityManagerFactory::GetForProfile(new_profile); + EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken( + account_info.account_id)); + + // Check that the right profile was opened. + EXPECT_EQ(new_profile->GetPath(), profile_path); + + // Add the account to the cookies (simulates the account reconcilor). + EXPECT_EQ(BrowserList::GetInstance()->size(), 1u); + signin::SetCookieAccounts(new_identity_manager, test_url_loader_factory(), + {{account_info.email, account_info.gaia}}); + + // A browser has been created for the new profile and the tab was moved there. + ASSERT_EQ(BrowserList::GetInstance()->size(), 2u); + Browser* added_browser = BrowserList::GetInstance()->get(1); + ASSERT_TRUE(added_browser); + EXPECT_EQ(added_browser->profile(), new_profile); + EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1); + EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(), + intercepted_url); + + CheckHistograms(histogram_tester, + SigninInterceptionHeuristicOutcome:: + kInterceptEnterpriseForcedProfileSwitch); + + // Interception bubble was closed. + EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_destroyed()); + + // Profile customization was not shown. + EXPECT_EQ(GetInterceptorDelegate(new_profile)->customized_browser(), nullptr); + EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr); +} + +// Failed run on MAC CI builder. https://crbug.com/1245200 +#if defined(OS_MAC) +#define MAYBE_EnterpriseSwitchAlreadyOpen DISABLED_EnterpriseSwitchAlreadyOpen +#else +#define MAYBE_EnterpriseSwitchAlreadyOpen EnterpriseSwitchAlreadyOpen +#endif +// Tests the complete profile switch flow when the profile is already loaded. +IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorEnterpriseBrowserTest, + MAYBE_EnterpriseSwitchAlreadyOpen) { + base::HistogramTester histogram_tester; + // Enforce enterprise profile separation. + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account_strict"); + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + // Fill the account info, in particular for the hosted_domain field. + account_info.full_name = "fullname"; + account_info.given_name = "givenname"; + account_info.hosted_domain = "example.com"; + account_info.locale = "en"; + account_info.picture_url = "https://example.com"; + DCHECK(account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + // Create another profile with a browser window. + ProfileManager* profile_manager = g_browser_process->profile_manager(); + const base::FilePath profile_path = + profile_manager->GenerateNextProfileDirectoryPath(); + base::RunLoop loop; + Profile* other_profile = nullptr; + ProfileManager::CreateCallback callback = base::BindLambdaForTesting( + [&other_profile, &loop](Profile* profile, Profile::CreateStatus status) { + DCHECK_EQ(status, Profile::CREATE_STATUS_INITIALIZED); + other_profile = profile; + loop.Quit(); + }); + profiles::SwitchToProfile(profile_path, /*always_create=*/true, + std::move(callback)); + loop.Run(); + ASSERT_TRUE(other_profile); + ASSERT_EQ(BrowserList::GetInstance()->size(), 2u); + Browser* other_browser = BrowserList::GetInstance()->get(1); + ASSERT_TRUE(other_browser); + ASSERT_EQ(other_browser->profile(), other_profile); + // Add the account to the other profile. + signin::IdentityManager* other_identity_manager = + IdentityManagerFactory::GetForProfile(other_profile); + other_identity_manager->GetAccountsMutator()->AddOrUpdateAccount( + account_info.gaia, account_info.email, "dummy_refresh_token", + /*is_under_advanced_protection=*/false, + signin_metrics::SourceForRefreshTokenOperation::kUnknown); + other_identity_manager->GetPrimaryAccountMutator()->SetPrimaryAccount( + account_info.account_id, signin::ConsentLevel::kSync); + + // Add a tab. + GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse"); + content::WebContents* web_contents = AddTab(intercepted_url); + int original_tab_count = browser()->tab_strip_model()->count(); + int other_original_tab_count = other_browser->tab_strip_model()->count(); + + // Start the interception. + GetInterceptorDelegate(profile())->set_expected_interception_type( + DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitchForced); + DiceWebSigninInterceptor* interceptor = + DiceWebSigninInterceptorFactory::GetForProfile(profile()); + interceptor->MaybeInterceptWebSignin(web_contents, account_info.account_id, + /*is_new_account=*/true, + /*is_sync_signin=*/false); + + // Add the account to the cookies (simulates the account reconcilor). + signin::SetCookieAccounts(other_identity_manager, test_url_loader_factory(), + {{account_info.email, account_info.gaia}}); + + // The tab was moved to the new browser. + ASSERT_EQ(BrowserList::GetInstance()->size(), 2u); + EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1); + EXPECT_EQ(other_browser->tab_strip_model()->count(), + other_original_tab_count + 1); + EXPECT_EQ(other_browser->tab_strip_model()->GetActiveWebContents()->GetURL(), + intercepted_url); + + CheckHistograms(histogram_tester, + SigninInterceptionHeuristicOutcome:: + kInterceptEnterpriseForcedProfileSwitch); + // Profile customization was not shown. + EXPECT_EQ(GetInterceptorDelegate(other_profile)->customized_browser(), + nullptr); + EXPECT_EQ(GetInterceptorDelegate(profile())->customized_browser(), nullptr); +} diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.cc b/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.cc new file mode 100644 index 00000000000..9377d94d2a8 --- /dev/null +++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.cc @@ -0,0 +1,45 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/dice_web_signin_interceptor_factory.h" + +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/dice_web_signin_interceptor.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/ui/signin/dice_web_signin_interceptor_delegate.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" + +// static +DiceWebSigninInterceptor* DiceWebSigninInterceptorFactory::GetForProfile( + Profile* profile) { + return static_cast( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +// static +DiceWebSigninInterceptorFactory* +DiceWebSigninInterceptorFactory::GetInstance() { + return base::Singleton::get(); +} + +DiceWebSigninInterceptorFactory::DiceWebSigninInterceptorFactory() + : BrowserContextKeyedServiceFactory( + "DiceWebSigninInterceptor", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(IdentityManagerFactory::GetInstance()); +} + +DiceWebSigninInterceptorFactory::~DiceWebSigninInterceptorFactory() = default; + +void DiceWebSigninInterceptorFactory::RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) { + DiceWebSigninInterceptor::RegisterProfilePrefs(registry); +} + +KeyedService* DiceWebSigninInterceptorFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + return new DiceWebSigninInterceptor( + Profile::FromBrowserContext(context), + std::make_unique()); +} diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.h b/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.h new file mode 100644 index 00000000000..ad6aac1ca3e --- /dev/null +++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.h @@ -0,0 +1,37 @@ +// Copyright 2020 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 CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +class DiceWebSigninInterceptor; +class Profile; + +class DiceWebSigninInterceptorFactory + : public BrowserContextKeyedServiceFactory { + public: + static DiceWebSigninInterceptor* GetForProfile(Profile* profile); + static DiceWebSigninInterceptorFactory* GetInstance(); + + DiceWebSigninInterceptorFactory(const DiceWebSigninInterceptorFactory&) = + delete; + DiceWebSigninInterceptorFactory& operator=( + const DiceWebSigninInterceptorFactory&) = delete; + + private: + friend struct base::DefaultSingletonTraits; + DiceWebSigninInterceptorFactory(); + ~DiceWebSigninInterceptorFactory() override; + + // BrowserContextKeyedServiceFactory: + void RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) override; + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor_unittest.cc b/chromium/chrome/browser/signin/dice_web_signin_interceptor_unittest.cc new file mode 100644 index 00000000000..1f66d5d96ed --- /dev/null +++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor_unittest.cc @@ -0,0 +1,953 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/dice_web_signin_interceptor.h" + +#include + +#include "base/callback.h" +#include "base/memory/raw_ptr.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/utf_string_conversions.h" +#include "base/test/metrics/histogram_tester.h" +#include "base/test/scoped_feature_list.h" +#include "build/buildflag.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profiles/profile_attributes_entry.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/signin/chrome_signin_client_factory.h" +#include "chrome/browser/signin/chrome_signin_client_test_util.h" +#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h" +#include "chrome/browser/signin/signin_features.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "chrome/common/chrome_constants.h" +#include "chrome/common/pref_names.h" +#include "chrome/test/base/browser_with_test_window_test.h" +#include "chrome/test/base/testing_browser_process.h" +#include "chrome/test/base/testing_profile.h" +#include "chrome/test/base/testing_profile_manager.h" +#include "components/prefs/pref_service.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/account_info.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "services/network/test/test_url_loader_factory.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#include "url/gurl.h" + +namespace { + +class MockDiceWebSigninInterceptorDelegate + : public DiceWebSigninInterceptor::Delegate { + public: + MOCK_METHOD(std::unique_ptr, + ShowSigninInterceptionBubble, + (content::WebContents * web_contents, + const BubbleParameters& bubble_parameters, + base::OnceCallback callback), + (override)); + void ShowProfileCustomizationBubble(Browser* browser) override {} +}; + +// Matches BubbleParameters fields excepting the color. This is useful in the +// test because the color is randomly generated. +testing::Matcher +MatchBubbleParameters( + const DiceWebSigninInterceptor::Delegate::BubbleParameters& parameters) { + return testing::AllOf( + testing::Field("interception_type", + &DiceWebSigninInterceptor::Delegate::BubbleParameters:: + interception_type, + parameters.interception_type), + testing::Field("intercepted_account", + &DiceWebSigninInterceptor::Delegate::BubbleParameters:: + intercepted_account, + parameters.intercepted_account), + testing::Field("primary_account", + &DiceWebSigninInterceptor::Delegate::BubbleParameters:: + primary_account, + parameters.primary_account)); +} + +// If the account info is valid, does nothing. Otherwise fills the extended +// fields with default values. +void MakeValidAccountInfo(AccountInfo* info) { + if (info->IsValid()) + return; + info->full_name = "fullname"; + info->given_name = "givenname"; + info->hosted_domain = kNoHostedDomainFound; + info->locale = "en"; + info->picture_url = "https://example.com"; + DCHECK(info->IsValid()); +} + +} // namespace + +class DiceWebSigninInterceptorTest : public BrowserWithTestWindowTest { + public: + DiceWebSigninInterceptorTest() = default; + ~DiceWebSigninInterceptorTest() override = default; + + DiceWebSigninInterceptor* interceptor() { + return dice_web_signin_interceptor_.get(); + } + + MockDiceWebSigninInterceptorDelegate* mock_delegate() { + return mock_delegate_; + } + + content::WebContents* web_contents() { + return browser()->tab_strip_model()->GetActiveWebContents(); + } + + ProfileAttributesStorage* profile_attributes_storage() { + return profile_manager()->profile_attributes_storage(); + } + + signin::IdentityTestEnvironment* identity_test_env() { + return identity_test_env_profile_adaptor_->identity_test_env(); + } + + Profile* CreateTestingProfile(const std::string& name) { + return profile_manager()->CreateTestingProfile(name); + } + + // Helper function that calls MaybeInterceptWebSignin with parameters + // compatible with interception. + void MaybeIntercept(CoreAccountId account_id) { + interceptor()->MaybeInterceptWebSignin(web_contents(), account_id, + /*is_new_account=*/true, + /*is_sync_signin=*/false); + } + + // Calls MaybeInterceptWebSignin and verifies the heuristic outcome, the + // histograms and whether the interception is in progress. + // This function only works if the interception decision can be made + // synchronously (GetHeuristicOutcome() returns a value). + void TestSynchronousInterception( + AccountInfo account_info, + bool is_new_account, + bool is_sync_signin, + SigninInterceptionHeuristicOutcome expected_outcome) { + ASSERT_EQ(interceptor()->GetHeuristicOutcome(is_new_account, is_sync_signin, + account_info.email, + /*entry=*/nullptr), + expected_outcome); + base::HistogramTester histogram_tester; + interceptor()->MaybeInterceptWebSignin(web_contents(), + account_info.account_id, + is_new_account, is_sync_signin); + testing::Mock::VerifyAndClearExpectations(mock_delegate()); + histogram_tester.ExpectUniqueSample("Signin.Intercept.HeuristicOutcome", + expected_outcome, 1); + EXPECT_EQ(interceptor()->is_interception_in_progress(), + SigninInterceptionHeuristicOutcomeIsSuccess(expected_outcome)); + } + + // Calls MaybeInterceptWebSignin and verifies the heuristic outcome and the + // histograms. + // This function only works if the interception decision cannot be made + // synchronously (GetHeuristicOutcome() returns no value). + void TestAsynchronousInterception( + AccountInfo account_info, + bool is_new_account, + bool is_sync_signin, + SigninInterceptionHeuristicOutcome expected_outcome) { + ASSERT_EQ(interceptor()->GetHeuristicOutcome(is_new_account, is_sync_signin, + account_info.email, + /*entry=*/nullptr), + absl::nullopt); + base::HistogramTester histogram_tester; + interceptor()->MaybeInterceptWebSignin(web_contents(), + account_info.account_id, + is_new_account, is_sync_signin); + testing::Mock::VerifyAndClearExpectations(mock_delegate()); + histogram_tester.ExpectUniqueSample("Signin.Intercept.HeuristicOutcome", + expected_outcome, 1); + EXPECT_TRUE(interceptor()->is_interception_in_progress()); + } + + private: + // testing::Test: + void SetUp() override { + BrowserWithTestWindowTest::SetUp(); + + identity_test_env_profile_adaptor_ = + std::make_unique(profile()); + identity_test_env_profile_adaptor_->identity_test_env() + ->SetTestURLLoaderFactory(&test_url_loader_factory_); + + auto delegate = std::make_unique< + testing::StrictMock>(); + mock_delegate_ = delegate.get(); + dice_web_signin_interceptor_ = std::make_unique( + profile(), std::move(delegate)); + + // Create the first tab so that web_contents() exists. + AddTab(browser(), GURL("http://foo/1")); + } + + void TearDown() override { + dice_web_signin_interceptor_->Shutdown(); + identity_test_env_profile_adaptor_.reset(); + BrowserWithTestWindowTest::TearDown(); + } + + TestingProfile::TestingFactories GetTestingFactories() override { + TestingProfile::TestingFactories factories = + IdentityTestEnvironmentProfileAdaptor:: + GetIdentityTestEnvironmentFactories(); + factories.push_back( + {ChromeSigninClientFactory::GetInstance(), + base::BindRepeating(&BuildChromeSigninClientWithURLLoader, + &test_url_loader_factory_)}); + return factories; + } + + network::TestURLLoaderFactory test_url_loader_factory_; + std::unique_ptr + identity_test_env_profile_adaptor_; + std::unique_ptr dice_web_signin_interceptor_; + raw_ptr mock_delegate_ = nullptr; +}; + +TEST_F(DiceWebSigninInterceptorTest, ShouldShowProfileSwitchBubble) { + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + const std::string& email = account_info.email; + EXPECT_FALSE(interceptor()->ShouldShowProfileSwitchBubble( + email, profile_attributes_storage())); + + // Add another profile with no account. + CreateTestingProfile("Profile 1"); + EXPECT_FALSE(interceptor()->ShouldShowProfileSwitchBubble( + email, profile_attributes_storage())); + + // Add another profile with a different account. + Profile* profile_2 = CreateTestingProfile("Profile 2"); + ProfileAttributesEntry* entry = + profile_attributes_storage()->GetProfileAttributesWithPath( + profile_2->GetPath()); + ASSERT_NE(entry, nullptr); + std::string kOtherGaiaID = "SomeOtherGaiaID"; + ASSERT_NE(kOtherGaiaID, account_info.gaia); + entry->SetAuthInfo(kOtherGaiaID, u"alice@gmail.com", + /*is_consented_primary_account=*/true); + EXPECT_FALSE(interceptor()->ShouldShowProfileSwitchBubble( + email, profile_attributes_storage())); + + // Change the account to match. + entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email), + /*is_consented_primary_account=*/false); + const ProfileAttributesEntry* switch_to_entry = + interceptor()->ShouldShowProfileSwitchBubble( + email, profile_attributes_storage()); + EXPECT_EQ(entry, switch_to_entry); +} + +TEST_F(DiceWebSigninInterceptorTest, NoBubbleWithSingleAccount) { + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + MakeValidAccountInfo(&account_info); + account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + // Without UPA. + EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info)); + EXPECT_FALSE(interceptor()->ShouldShowMultiUserBubble(account_info)); + + // With UPA. + identity_test_env()->SetPrimaryAccount("bob@example.com", + signin::ConsentLevel::kSignin); + EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info)); +} + +TEST_F(DiceWebSigninInterceptorTest, ShouldShowEnterpriseBubble) { + // Setup 3 accounts in the profile: + // - primary account + // - other enterprise account that is not primary (should be ignored) + // - intercepted account. + AccountInfo primary_account_info = + identity_test_env()->MakePrimaryAccountAvailable( + "alice@example.com", signin::ConsentLevel::kSignin); + AccountInfo other_account_info = + identity_test_env()->MakeAccountAvailable("dummy@example.com"); + MakeValidAccountInfo(&other_account_info); + other_account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(other_account_info); + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + MakeValidAccountInfo(&account_info); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + ASSERT_EQ(identity_test_env()->identity_manager()->GetPrimaryAccountId( + signin::ConsentLevel::kSignin), + primary_account_info.account_id); + + // The primary account does not have full account info (empty domain). + ASSERT_TRUE(identity_test_env() + ->identity_manager() + ->FindExtendedAccountInfo(primary_account_info) + .hosted_domain.empty()); + EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info)); + account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info); + EXPECT_TRUE(interceptor()->ShouldShowEnterpriseBubble(account_info)); + + // The primary account has full info. + MakeValidAccountInfo(&primary_account_info); + identity_test_env()->UpdateAccountInfoForAccount(primary_account_info); + // The intercepted account is enterprise. + EXPECT_TRUE(interceptor()->ShouldShowEnterpriseBubble(account_info)); + // Two consummer accounts. + account_info.hosted_domain = kNoHostedDomainFound; + identity_test_env()->UpdateAccountInfoForAccount(account_info); + EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info)); + // The primary account is enterprise. + primary_account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(primary_account_info); + EXPECT_TRUE(interceptor()->ShouldShowEnterpriseBubble(account_info)); +} + +class DiceWebSigninInterceptorForcedSeparationTest + : public DiceWebSigninInterceptorTest { + public: + DiceWebSigninInterceptorForcedSeparationTest() + : feature_list_(kAccountPoliciesLoadedWithoutSync) {} + + private: + base::test::ScopedFeatureList feature_list_; +}; + +TEST_F(DiceWebSigninInterceptorForcedSeparationTest, + ShouldEnforceEnterpriseProfileSeparation) { + profile()->GetPrefs()->SetBoolean( + prefs::kManagedAccountsSigninRestrictionScopeMachine, true); + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account_strict"); + + // Setup 3 accounts in the profile: + // - primary account + // - other enterprise account that is not primary (should be ignored) + // - intercepted account. + AccountInfo primary_account_info = + identity_test_env()->MakePrimaryAccountAvailable( + "alice@gmail.com", signin::ConsentLevel::kSignin); + + AccountInfo other_account_info = + identity_test_env()->MakeAccountAvailable("dummy@example.com"); + MakeValidAccountInfo(&other_account_info); + other_account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(other_account_info); + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + MakeValidAccountInfo(&account_info); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + ASSERT_EQ(identity_test_env()->identity_manager()->GetPrimaryAccountId( + signin::ConsentLevel::kSignin), + primary_account_info.account_id); + interceptor()->new_account_interception_ = true; + // Consumer account not intercepted. + EXPECT_FALSE( + interceptor()->ShouldEnforceEnterpriseProfileSeparation(account_info)); + account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info); + // Managed account intercepted. + EXPECT_TRUE( + interceptor()->ShouldEnforceEnterpriseProfileSeparation(account_info)); +} + +TEST_F(DiceWebSigninInterceptorForcedSeparationTest, + ShouldEnforceEnterpriseProfileSeparationWithoutUPA) { + profile()->GetPrefs()->SetBoolean( + prefs::kManagedAccountsSigninRestrictionScopeMachine, true); + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account_strict"); + AccountInfo account_info_1 = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + MakeValidAccountInfo(&account_info_1); + account_info_1.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info_1); + + interceptor()->new_account_interception_ = true; + // Primary account is not set. + ASSERT_FALSE(identity_test_env()->identity_manager()->HasPrimaryAccount( + signin::ConsentLevel::kSignin)); + EXPECT_TRUE( + interceptor()->ShouldEnforceEnterpriseProfileSeparation(account_info_1)); +} + +TEST_F(DiceWebSigninInterceptorForcedSeparationTest, + ShouldEnforceEnterpriseProfileSeparationReauth) { + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account_strict"); + AccountInfo primary_account_info = + identity_test_env()->MakePrimaryAccountAvailable( + "alice@example.com", signin::ConsentLevel::kSignin); + MakeValidAccountInfo(&primary_account_info); + primary_account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(primary_account_info); + + // Primary account is set. + ASSERT_TRUE(identity_test_env()->identity_manager()->HasPrimaryAccount( + signin::ConsentLevel::kSignin)); + EXPECT_TRUE(interceptor()->ShouldEnforceEnterpriseProfileSeparation( + primary_account_info)); + + ProfileAttributesEntry* entry = + profile_attributes_storage()->GetProfileAttributesWithPath( + profile()->GetPath()); + entry->SetUserAcceptedAccountManagement(true); + + EXPECT_FALSE(interceptor()->ShouldEnforceEnterpriseProfileSeparation( + primary_account_info)); +} + +TEST_F(DiceWebSigninInterceptorForcedSeparationTest, + EnforceManagedAccountAsPrimaryReauth) { + profile()->GetPrefs()->SetBoolean( + prefs::kManagedAccountsSigninRestrictionScopeMachine, true); + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account"); + + // Reauth intercepted if enterprise confirmation not shown yet for forced + // managed separation. + AccountInfo account_info = identity_test_env()->MakePrimaryAccountAvailable( + "alice@example.com", signin::ConsentLevel::kSignin); + MakeValidAccountInfo(&account_info); + account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info); + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account"); + + // Check that interception works otherwise, as a sanity check. + DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced, + account_info, account_info, SkColor()}; + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)); + + TestAsynchronousInterception( + account_info, /*is_new_account=*/false, /*is_sync_signin=*/false, + SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced); +} + +TEST_F(DiceWebSigninInterceptorForcedSeparationTest, + EnforceManagedAccountAsPrimaryManaged) { + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + MakeValidAccountInfo(&account_info); + account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account_strict"); + + // Check that interception works otherwise, as a sanity check. + DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced, + account_info, AccountInfo(), SkColor()}; + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)); + TestAsynchronousInterception( + account_info, /*is_new_account=*/true, /*is_sync_signin=*/false, + SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced); +} + +TEST_F(DiceWebSigninInterceptorForcedSeparationTest, + EnforceManagedAccountAsPrimaryProfileSwitch) { + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + MakeValidAccountInfo(&account_info); + account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + profile()->GetPrefs()->SetBoolean( + prefs::kManagedAccountsSigninRestrictionScopeMachine, true); + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account_strict"); + + // Setup for profile switch interception. + Profile* profile_2 = CreateTestingProfile("Profile 2"); + ProfileAttributesEntry* entry = + profile_attributes_storage()->GetProfileAttributesWithPath( + profile_2->GetPath()); + ASSERT_NE(entry, nullptr); + entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(account_info.email), + /*is_consented_primary_account=*/false); + // Check that interception works otherwise, as a sanity check. + DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitchForced, + account_info, AccountInfo(), SkColor()}; + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)); + TestAsynchronousInterception(account_info, /*is_new_account=*/true, + /*is_sync_signin=*/false, + SigninInterceptionHeuristicOutcome:: + kInterceptEnterpriseForcedProfileSwitch); +} + +TEST_F(DiceWebSigninInterceptorTest, ShouldShowEnterpriseBubbleWithoutUPA) { + AccountInfo account_info_1 = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + MakeValidAccountInfo(&account_info_1); + account_info_1.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info_1); + AccountInfo account_info_2 = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + MakeValidAccountInfo(&account_info_2); + account_info_2.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info_2); + + // Primary account is not set. + ASSERT_FALSE(identity_test_env()->identity_manager()->HasPrimaryAccount( + signin::ConsentLevel::kSignin)); + EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info_1)); +} + +TEST_F(DiceWebSigninInterceptorTest, ShouldShowMultiUserBubble) { + // Setup two accounts in the profile. + AccountInfo account_info_1 = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + MakeValidAccountInfo(&account_info_1); + account_info_1.given_name = "Bob"; + identity_test_env()->UpdateAccountInfoForAccount(account_info_1); + AccountInfo account_info_2 = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + + // The other account does not have full account info (empty name). + ASSERT_TRUE(account_info_2.given_name.empty()); + EXPECT_TRUE(interceptor()->ShouldShowMultiUserBubble(account_info_1)); + + // Accounts with different names. + account_info_1.given_name = "Bob"; + identity_test_env()->UpdateAccountInfoForAccount(account_info_1); + MakeValidAccountInfo(&account_info_2); + account_info_2.given_name = "Alice"; + identity_test_env()->UpdateAccountInfoForAccount(account_info_2); + EXPECT_TRUE(interceptor()->ShouldShowMultiUserBubble(account_info_1)); + + // Accounts with same names. + account_info_1.given_name = "Alice"; + identity_test_env()->UpdateAccountInfoForAccount(account_info_1); + EXPECT_FALSE(interceptor()->ShouldShowMultiUserBubble(account_info_1)); + + // Comparison is case insensitive. + account_info_1.given_name = "alice"; + identity_test_env()->UpdateAccountInfoForAccount(account_info_1); + EXPECT_FALSE(interceptor()->ShouldShowMultiUserBubble(account_info_1)); +} + +TEST_F(DiceWebSigninInterceptorTest, NoInterception) { + // Setup for profile switch interception. + std::string email = "bob@example.com"; + AccountInfo account_info = identity_test_env()->MakeAccountAvailable(email); + Profile* profile_2 = CreateTestingProfile("Profile 2"); + ProfileAttributesEntry* entry = + profile_attributes_storage()->GetProfileAttributesWithPath( + profile_2->GetPath()); + ASSERT_NE(entry, nullptr); + entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email), + /*is_consented_primary_account=*/false); + + // Check that Sync signin is not intercepted. + TestSynchronousInterception( + account_info, /*is_new_account=*/true, /*is_sync_signin=*/true, + SigninInterceptionHeuristicOutcome::kAbortSyncSignin); + + // Check that reauth is not intercepted. + TestSynchronousInterception( + account_info, /*is_new_account=*/false, /*is_sync_signin=*/false, + SigninInterceptionHeuristicOutcome::kAbortAccountNotNew); + + // Check that interception works otherwise, as a sanity check. + DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch, + account_info, AccountInfo(), SkColor()}; + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)); + TestSynchronousInterception( + account_info, /*is_new_account=*/true, /*is_sync_signin=*/false, + SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch); +} + +// Checks that the heuristic still works if the account was not added to Chrome +// yet. +TEST_F(DiceWebSigninInterceptorTest, HeuristicAccountNotAdded) { + // Setup for profile switch interception. + std::string email = "bob@example.com"; + Profile* profile_2 = CreateTestingProfile("Profile 2"); + ProfileAttributesEntry* entry = + profile_attributes_storage()->GetProfileAttributesWithPath( + profile_2->GetPath()); + ASSERT_NE(entry, nullptr); + entry->SetAuthInfo("dummy_gaia_id", base::UTF8ToUTF16(email), + /*is_consented_primary_account=*/false); + EXPECT_EQ(interceptor()->GetHeuristicOutcome( + /*is_new_account=*/true, /*is_sync_signin=*/false, email, + /*entry=*/nullptr), + SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch); +} + +// Checks that the heuristic defaults to gmail.com when no domain is specified. +TEST_F(DiceWebSigninInterceptorTest, HeuristicDefaultsToGmail) { + // Setup for profile switch interception. + std::string email = "bob@gmail.com"; + Profile* profile_2 = CreateTestingProfile("Profile 2"); + ProfileAttributesEntry* entry = + profile_attributes_storage()->GetProfileAttributesWithPath( + profile_2->GetPath()); + ASSERT_NE(entry, nullptr); + entry->SetAuthInfo("dummy_gaia_id", base::UTF8ToUTF16(email), + /*is_consented_primary_account=*/false); + // No domain defaults to gmail.com + EXPECT_EQ(interceptor()->GetHeuristicOutcome( + /*is_new_account=*/true, /*is_sync_signin=*/false, "bob", + /*entry=*/nullptr), + SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch); + // Using wrong domain does not trigger the interception. + EXPECT_EQ( + interceptor()->GetHeuristicOutcome( + /*is_new_account=*/true, /*is_sync_signin=*/false, "bob@example.com", + /*entry=*/nullptr), + SigninInterceptionHeuristicOutcome::kAbortSingleAccount); +} + +// Checks that no heuristic is returned if signin interception is disabled. +TEST_F(DiceWebSigninInterceptorTest, InterceptionDisabled) { + // Setup for profile switch interception. + std::string email = "bob@gmail.com"; + Profile* profile_2 = CreateTestingProfile("Profile 2"); + profile()->GetPrefs()->SetBoolean(prefs::kSigninInterceptionEnabled, false); + ProfileAttributesEntry* entry = + profile_attributes_storage()->GetProfileAttributesWithPath( + profile_2->GetPath()); + ASSERT_NE(entry, nullptr); + entry->SetAuthInfo("dummy_gaia_id", base::UTF8ToUTF16(email), + /*is_consented_primary_account=*/false); + EXPECT_EQ(interceptor()->GetHeuristicOutcome( + /*is_new_account=*/true, /*is_sync_signin=*/false, "bob", + /*entry=*/nullptr), + SigninInterceptionHeuristicOutcome::kAbortInterceptionDisabled); + EXPECT_EQ( + interceptor()->GetHeuristicOutcome( + /*is_new_account=*/true, /*is_sync_signin=*/false, "bob@example.com", + /*entry=*/nullptr), + SigninInterceptionHeuristicOutcome::kAbortInterceptionDisabled); +} + +TEST_F(DiceWebSigninInterceptorTest, TabClosed) { + base::HistogramTester histogram_tester; + interceptor()->MaybeInterceptWebSignin( + /*web_contents=*/nullptr, CoreAccountId(), + /*is_new_account=*/true, /*is_sync_signin=*/false); + histogram_tester.ExpectUniqueSample( + "Signin.Intercept.HeuristicOutcome", + SigninInterceptionHeuristicOutcome::kAbortTabClosed, 1); +} + +TEST_F(DiceWebSigninInterceptorTest, InterceptionInProgress) { + // Setup for profile switch interception. + std::string email = "bob@example.com"; + AccountInfo account_info = identity_test_env()->MakeAccountAvailable(email); + Profile* profile_2 = CreateTestingProfile("Profile 2"); + ProfileAttributesEntry* entry = + profile_attributes_storage()->GetProfileAttributesWithPath( + profile_2->GetPath()); + ASSERT_NE(entry, nullptr); + entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email), + /*is_consented_primary_account=*/false); + + // Start an interception. + DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch, + account_info, AccountInfo(), SkColor()}; + base::OnceCallback delegate_callback; + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)) + .WillOnce(testing::WithArg<2>(testing::Invoke( + [&delegate_callback]( + base::OnceCallback callback) { + delegate_callback = std::move(callback); + return nullptr; + }))); + MaybeIntercept(account_info.account_id); + testing::Mock::VerifyAndClearExpectations(mock_delegate()); + EXPECT_TRUE(interceptor()->is_interception_in_progress()); + + // Check that there is no interception while another one is in progress. + base::HistogramTester histogram_tester; + MaybeIntercept(account_info.account_id); + testing::Mock::VerifyAndClearExpectations(mock_delegate()); + histogram_tester.ExpectUniqueSample( + "Signin.Intercept.HeuristicOutcome", + SigninInterceptionHeuristicOutcome::kAbortInterceptInProgress, 1); + + // Complete the interception that was in progress. + std::move(delegate_callback).Run(SigninInterceptionResult::kDeclined); + EXPECT_FALSE(interceptor()->is_interception_in_progress()); + + // A new interception can now start. + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)); + MaybeIntercept(account_info.account_id); +} + +TEST_F(DiceWebSigninInterceptorTest, DeclineCreationRepeatedly) { + base::HistogramTester histogram_tester; + AccountInfo primary_account_info = + identity_test_env()->MakePrimaryAccountAvailable( + "bob@example.com", signin::ConsentLevel::kSignin); + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + MakeValidAccountInfo(&account_info); + account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + const int kMaxProfileCreationDeclinedCount = 2; + // Decline the interception kMaxProfileCreationDeclinedCount times. + DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise, + account_info, primary_account_info, SkColor()}; + for (int i = 0; i < kMaxProfileCreationDeclinedCount; ++i) { + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)) + .WillOnce(testing::WithArg<2>(testing::Invoke( + [](base::OnceCallback callback) { + std::move(callback).Run(SigninInterceptionResult::kDeclined); + return nullptr; + }))); + MaybeIntercept(account_info.account_id); + EXPECT_EQ(interceptor()->is_interception_in_progress(), false); + histogram_tester.ExpectUniqueSample( + "Signin.Intercept.HeuristicOutcome", + SigninInterceptionHeuristicOutcome::kInterceptEnterprise, i + 1); + } + + // Next time the interception is not shown again. + MaybeIntercept(account_info.account_id); + EXPECT_EQ(interceptor()->is_interception_in_progress(), false); + histogram_tester.ExpectBucketCount( + "Signin.Intercept.HeuristicOutcome", + SigninInterceptionHeuristicOutcome::kAbortUserDeclinedProfileForAccount, + 1); + + // Another account can still be intercepted. + account_info.email = "oscar@example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info); + expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise, + account_info, primary_account_info, SkColor()}; + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)); + MaybeIntercept(account_info.account_id); + histogram_tester.ExpectBucketCount( + "Signin.Intercept.HeuristicOutcome", + SigninInterceptionHeuristicOutcome::kInterceptEnterprise, + kMaxProfileCreationDeclinedCount + 1); + EXPECT_EQ(interceptor()->is_interception_in_progress(), true); +} + +TEST_F(DiceWebSigninInterceptorTest, DeclineSwitchRepeatedly_NoLimit) { + base::HistogramTester histogram_tester; + // Setup for profile switch interception. + std::string email = "bob@example.com"; + AccountInfo account_info = identity_test_env()->MakeAccountAvailable(email); + Profile* profile_2 = CreateTestingProfile("Profile 2"); + ProfileAttributesEntry* entry = + profile_attributes_storage()->GetProfileAttributesWithPath( + profile_2->GetPath()); + ASSERT_NE(entry, nullptr); + entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email), + /*is_consented_primary_account=*/false); + + // Test that the profile switch can be declined multiple times. + DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch, + account_info, AccountInfo(), SkColor()}; + for (int i = 0; i < 10; ++i) { + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)) + .WillOnce(testing::WithArg<2>(testing::Invoke( + [](base::OnceCallback callback) { + std::move(callback).Run(SigninInterceptionResult::kDeclined); + return nullptr; + }))); + MaybeIntercept(account_info.account_id); + EXPECT_EQ(interceptor()->is_interception_in_progress(), false); + histogram_tester.ExpectUniqueSample( + "Signin.Intercept.HeuristicOutcome", + SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch, i + 1); + } +} + +TEST_F(DiceWebSigninInterceptorTest, PersistentHash) { + // The hash is persistent (the value should never change). + EXPECT_EQ("email_174", + interceptor()->GetPersistentEmailHash("alice@example.com")); + // Different email get another hash. + EXPECT_NE(interceptor()->GetPersistentEmailHash("bob@gmail.com"), + interceptor()->GetPersistentEmailHash("alice@example.com")); + // Equivalent emails get the same hash. + EXPECT_EQ(interceptor()->GetPersistentEmailHash("bob"), + interceptor()->GetPersistentEmailHash("bob@gmail.com")); + EXPECT_EQ(interceptor()->GetPersistentEmailHash("bo.b@gmail.com"), + interceptor()->GetPersistentEmailHash("bob@gmail.com")); + // Dots are removed only for gmail accounts. + EXPECT_NE(interceptor()->GetPersistentEmailHash("alice@example.com"), + interceptor()->GetPersistentEmailHash("al.ice@example.com")); +} + +// Interception other than the profile switch require at least 2 accounts. +TEST_F(DiceWebSigninInterceptorTest, NoInterceptionWithOneAccount) { + base::HistogramTester histogram_tester; + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + // Interception aborts even if the account info is not available. + ASSERT_FALSE(identity_test_env() + ->identity_manager() + ->FindExtendedAccountInfoByAccountId(account_info.account_id) + .IsValid()); + TestSynchronousInterception( + account_info, /*is_new_account=*/true, /*is_sync_signin=*/false, + SigninInterceptionHeuristicOutcome::kAbortSingleAccount); +} + +// When profile creation is disallowed, profile switch interception is still +// enabled, but others are disabled. +TEST_F(DiceWebSigninInterceptorTest, ProfileCreationDisallowed) { + base::HistogramTester histogram_tester; + g_browser_process->local_state()->SetBoolean(prefs::kBrowserAddPersonEnabled, + false); + // Setup for profile switch interception. + std::string email = "bob@example.com"; + AccountInfo account_info = identity_test_env()->MakeAccountAvailable(email); + AccountInfo other_account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + Profile* profile_2 = CreateTestingProfile("Profile 2"); + ProfileAttributesEntry* entry = + profile_attributes_storage()->GetProfileAttributesWithPath( + profile_2->GetPath()); + ASSERT_NE(entry, nullptr); + entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email), + /*is_consented_primary_account=*/false); + + // Interception that would offer creating a new profile does not work. + TestSynchronousInterception( + other_account_info, /*is_new_account=*/true, /*is_sync_signin=*/false, + SigninInterceptionHeuristicOutcome::kAbortProfileCreationDisallowed); + + // Profile switch interception still works. + DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch, + account_info, AccountInfo(), SkColor()}; + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)); + MaybeIntercept(account_info.account_id); +} + +TEST_F(DiceWebSigninInterceptorTest, WaitForAccountInfoAvailable) { + base::HistogramTester histogram_tester; + AccountInfo primary_account_info = + identity_test_env()->MakePrimaryAccountAvailable( + "bob@example.com", signin::ConsentLevel::kSignin); + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + EXPECT_FALSE(interceptor() + ->GetHeuristicOutcome(/*is_new_account=*/true, + /*is_sync_signin=*/false, + account_info.email, + /*entry=*/nullptr) + .has_value()); + MaybeIntercept(account_info.account_id); + // Delegate was not called yet. + testing::Mock::VerifyAndClearExpectations(mock_delegate()); + + // Account info becomes available, interception happens. + DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise, + account_info, primary_account_info, SkColor()}; + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)); + MakeValidAccountInfo(&account_info); + account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info); + histogram_tester.ExpectTotalCount("Signin.Intercept.AccountInfoFetchDuration", + 1); +} + +TEST_F(DiceWebSigninInterceptorTest, AccountInfoAlreadyAvailable) { + base::HistogramTester histogram_tester; + AccountInfo primary_account_info = + identity_test_env()->MakePrimaryAccountAvailable( + "bob@example.com", signin::ConsentLevel::kSignin); + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + MakeValidAccountInfo(&account_info); + account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + // Account info is already available, interception happens immediately. + DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise, + account_info, primary_account_info, SkColor()}; + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)); + MaybeIntercept(account_info.account_id); + histogram_tester.ExpectTotalCount("Signin.Intercept.AccountInfoFetchDuration", + 1); + histogram_tester.ExpectUniqueSample( + "Signin.Intercept.HeuristicOutcome", + SigninInterceptionHeuristicOutcome::kInterceptEnterprise, 1); +} + +TEST_F(DiceWebSigninInterceptorTest, MultiUserInterception) { + base::HistogramTester histogram_tester; + AccountInfo primary_account_info = + identity_test_env()->MakePrimaryAccountAvailable( + "bob@example.com", signin::ConsentLevel::kSignin); + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + MakeValidAccountInfo(&account_info); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + // Account info is already available, interception happens immediately. + DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kMultiUser, + account_info, primary_account_info, SkColor()}; + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)); + MaybeIntercept(account_info.account_id); + histogram_tester.ExpectUniqueSample( + "Signin.Intercept.HeuristicOutcome", + SigninInterceptionHeuristicOutcome::kInterceptMultiUser, 1); +} diff --git a/chromium/chrome/browser/signin/e2e_tests/live_sign_in_test.cc b/chromium/chrome/browser/signin/e2e_tests/live_sign_in_test.cc new file mode 100644 index 00000000000..3e042e76b87 --- /dev/null +++ b/chromium/chrome/browser/signin/e2e_tests/live_sign_in_test.cc @@ -0,0 +1,776 @@ +// Copyright 2019 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/memory/raw_ptr.h" +#include "base/run_loop.h" +#include "base/scoped_observation.h" +#include "base/strings/stringprintf.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/signin/account_reconcilor_factory.h" +#include "chrome/browser/signin/e2e_tests/live_test.h" +#include "chrome/browser/signin/e2e_tests/test_accounts_util.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/sync/sync_service_factory.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/tabs/tab_strip_model_observer.h" +#include "chrome/browser/ui/webui/signin/login_ui_test_utils.h" +#include "chrome/browser/ui/webui/signin/signin_email_confirmation_dialog.h" +#include "chrome/test/base/ui_test_utils.h" +#include "components/signin/core/browser/account_reconcilor.h" +#include "components/signin/public/base/consent_level.h" +#include "components/signin/public/identity_manager/account_capabilities.h" +#include "components/signin/public/identity_manager/account_info.h" +#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_test_utils.h" +#include "components/signin/public/identity_manager/tribool.h" +#include "components/sync/driver/sync_service.h" +#include "content/public/test/browser_test.h" +#include "google_apis/gaia/core_account_id.h" +#include "google_apis/gaia/gaia_auth_util.h" +#include "google_apis/gaia/gaia_urls.h" +#include "ui/compositor/scoped_animation_duration_scale_mode.h" + +#if !BUILDFLAG(IS_CHROMEOS_ASH) +#include "chrome/browser/sync/sync_ui_util.h" +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) + +namespace signin { +namespace test { + +const base::TimeDelta kDialogTimeout = base::Seconds(10); + +// A wrapper importing the settings module when the chrome://settings serve the +// Polymer 3 version. +const char kSettingsScriptWrapperFormat[] = + "import('./settings.js').then(settings => {%s});"; + +enum class PrimarySyncAccountWait { kWaitForAdded, kWaitForCleared, kNotWait }; + +// Observes various sign-in events and allows to wait for a specific state of +// signed-in accounts. +class SignInTestObserver : public IdentityManager::Observer, + public AccountReconcilor::Observer { + public: + explicit SignInTestObserver(IdentityManager* identity_manager, + AccountReconcilor* reconcilor) + : identity_manager_(identity_manager), reconcilor_(reconcilor) { + identity_manager_observation_.Observe(identity_manager_.get()); + account_reconcilor_observation_.Observe(reconcilor_.get()); + } + ~SignInTestObserver() override = default; + + // IdentityManager::Observer: + void OnPrimaryAccountChanged( + const PrimaryAccountChangeEvent& event) override { + if (event.GetEventTypeFor(ConsentLevel::kSync) == + PrimaryAccountChangeEvent::Type::kNone) { + return; + } + QuitIfConditionIsSatisfied(); + } + void OnRefreshTokenUpdatedForAccount(const CoreAccountInfo&) override { + QuitIfConditionIsSatisfied(); + } + void OnRefreshTokenRemovedForAccount(const CoreAccountId&) override { + QuitIfConditionIsSatisfied(); + } + void OnErrorStateOfRefreshTokenUpdatedForAccount( + const CoreAccountInfo&, + const GoogleServiceAuthError&) override { + QuitIfConditionIsSatisfied(); + } + void OnAccountsInCookieUpdated(const AccountsInCookieJarInfo&, + const GoogleServiceAuthError&) override { + QuitIfConditionIsSatisfied(); + } + + // AccountReconcilor::Observer: + // TODO(https://crbug.com/1051864): Remove this obsever method once the bug is + // fixed. + void OnStateChanged(signin_metrics::AccountReconcilorState state) override { + if (state == signin_metrics::ACCOUNT_RECONCILOR_OK) { + // This will trigger cookie update if accounts are stale. + identity_manager_->GetAccountsInCookieJar(); + } + } + + void WaitForAccountChanges(int signed_in_accounts, + PrimarySyncAccountWait primary_sync_account_wait) { + expected_signed_in_accounts_ = signed_in_accounts; + primary_sync_account_wait_ = primary_sync_account_wait; + are_expectations_set = true; + QuitIfConditionIsSatisfied(); + run_loop_.Run(); + } + + private: + void QuitIfConditionIsSatisfied() { + if (!are_expectations_set) + return; + + int accounts_with_valid_refresh_token = + CountAccountsWithValidRefreshToken(); + int accounts_in_cookie = CountSignedInAccountsInCookie(); + + if (accounts_with_valid_refresh_token != accounts_in_cookie || + accounts_with_valid_refresh_token != expected_signed_in_accounts_) { + return; + } + + switch (primary_sync_account_wait_) { + case PrimarySyncAccountWait::kWaitForAdded: + if (!HasValidPrimarySyncAccount()) + return; + break; + case PrimarySyncAccountWait::kWaitForCleared: + if (identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync)) + return; + break; + case PrimarySyncAccountWait::kNotWait: + break; + } + + run_loop_.Quit(); + } + + int CountAccountsWithValidRefreshToken() const { + std::vector accounts_with_refresh_tokens = + identity_manager_->GetAccountsWithRefreshTokens(); + int valid_accounts = 0; + for (const auto& account_info : accounts_with_refresh_tokens) { + if (!identity_manager_->HasAccountWithRefreshTokenInPersistentErrorState( + account_info.account_id)) { + ++valid_accounts; + } + } + return valid_accounts; + } + + int CountSignedInAccountsInCookie() const { + signin::AccountsInCookieJarInfo accounts_in_cookie_jar = + identity_manager_->GetAccountsInCookieJar(); + if (!accounts_in_cookie_jar.accounts_are_fresh) + return -1; + + return accounts_in_cookie_jar.signed_in_accounts.size(); + } + + bool HasValidPrimarySyncAccount() const { + CoreAccountId primary_account_id = + identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSync); + if (primary_account_id.empty()) + return false; + + return !identity_manager_->HasAccountWithRefreshTokenInPersistentErrorState( + primary_account_id); + } + + const raw_ptr identity_manager_; + const raw_ptr reconcilor_; + base::ScopedObservation + identity_manager_observation_{this}; + base::ScopedObservation + account_reconcilor_observation_{this}; + base::RunLoop run_loop_; + + bool are_expectations_set = false; + int expected_signed_in_accounts_ = 0; + PrimarySyncAccountWait primary_sync_account_wait_ = + PrimarySyncAccountWait::kNotWait; +}; + +// Observer class allowing to wait for account capabilities to be known. +class AccountCapabilitiesObserver : public IdentityManager::Observer { + public: + explicit AccountCapabilitiesObserver(IdentityManager* identity_manager) + : identity_manager_(identity_manager) { + identity_manager_observation_.Observe(identity_manager); + } + + // IdentityManager::Observer: + void OnExtendedAccountInfoUpdated(const AccountInfo& info) override { + if (info.account_id != account_id_) + return; + + if (info.capabilities.AreAllCapabilitiesKnown()) + run_loop_.Quit(); + } + + // This should be called only once per AccountCapabilitiesObserver instance. + void WaitForAllCapabilitiesToBeKnown(CoreAccountId account_id) { + DCHECK(identity_manager_observation_.IsObservingSource( + identity_manager_.get())); + AccountInfo info = + identity_manager_->FindExtendedAccountInfoByAccountId(account_id); + if (info.capabilities.AreAllCapabilitiesKnown()) + return; + + account_id_ = account_id; + run_loop_.Run(); + identity_manager_observation_.Reset(); + } + + private: + raw_ptr identity_manager_ = nullptr; + CoreAccountId account_id_; + base::RunLoop run_loop_; + base::ScopedObservation + identity_manager_observation_{this}; +}; + +// Live tests for SignIn. +// These tests can be run with: +// browser_tests --gtest_filter=LiveSignInTest.* --run-live-tests --run-manual +class LiveSignInTest : public signin::test::LiveTest { + public: + LiveSignInTest() = default; + ~LiveSignInTest() override = default; + + void SetUp() override { + LiveTest::SetUp(); + // Always disable animation for stability. + ui::ScopedAnimationDurationScaleMode disable_animation( + ui::ScopedAnimationDurationScaleMode::ZERO_DURATION); + } + + void SignInFromWeb(const TestAccount& test_account, + int previously_signed_in_accounts) { + AddTabAtIndex(0, GaiaUrls::GetInstance()->add_account_url(), + ui::PageTransition::PAGE_TRANSITION_TYPED); + SignInFromCurrentPage(test_account, previously_signed_in_accounts); + } + + void SignInFromSettings(const TestAccount& test_account, + int previously_signed_in_accounts) { + GURL settings_url("chrome://settings"); + AddTabAtIndex(0, settings_url, ui::PageTransition::PAGE_TRANSITION_TYPED); + auto* settings_tab = browser()->tab_strip_model()->GetActiveWebContents(); + EXPECT_TRUE(content::ExecuteScript( + settings_tab, + base::StringPrintf( + kSettingsScriptWrapperFormat, + "settings.SyncBrowserProxyImpl.getInstance().startSignIn();"))); + SignInFromCurrentPage(test_account, previously_signed_in_accounts); + } + + void SignInFromCurrentPage(const TestAccount& test_account, + int previously_signed_in_accounts) { + SignInTestObserver observer(identity_manager(), account_reconcilor()); + login_ui_test_utils::ExecuteJsToSigninInSigninFrame( + browser(), test_account.user, test_account.password); + observer.WaitForAccountChanges(previously_signed_in_accounts + 1, + PrimarySyncAccountWait::kNotWait); + } + + void TurnOnSync(const TestAccount& test_account, + int previously_signed_in_accounts) { + SignInFromSettings(test_account, previously_signed_in_accounts); + + SignInTestObserver observer(identity_manager(), account_reconcilor()); + EXPECT_TRUE(login_ui_test_utils::ConfirmSyncConfirmationDialog( + browser(), kDialogTimeout)); + observer.WaitForAccountChanges(previously_signed_in_accounts + 1, + PrimarySyncAccountWait::kWaitForAdded); + } + + void SignOutFromWeb() { + SignInTestObserver observer(identity_manager(), account_reconcilor()); + AddTabAtIndex(0, GaiaUrls::GetInstance()->service_logout_url(), + ui::PageTransition::PAGE_TRANSITION_TYPED); + observer.WaitForAccountChanges(0, PrimarySyncAccountWait::kNotWait); + } + + void TurnOffSync() { + GURL settings_url("chrome://settings"); + AddTabAtIndex(0, settings_url, ui::PageTransition::PAGE_TRANSITION_TYPED); + SignInTestObserver observer(identity_manager(), account_reconcilor()); + auto* settings_tab = browser()->tab_strip_model()->GetActiveWebContents(); + EXPECT_TRUE(content::ExecuteScript( + settings_tab, + base::StringPrintf( + kSettingsScriptWrapperFormat, + "settings.SyncBrowserProxyImpl.getInstance().signOut(false)"))); + observer.WaitForAccountChanges(0, PrimarySyncAccountWait::kWaitForCleared); + } + + signin::IdentityManager* identity_manager() { + return identity_manager(browser()); + } + + signin::IdentityManager* identity_manager(Browser* browser) { + return IdentityManagerFactory::GetForProfile(browser->profile()); + } + + syncer::SyncService* sync_service() { return sync_service(browser()); } + + syncer::SyncService* sync_service(Browser* browser) { + return SyncServiceFactory::GetForProfile(browser->profile()); + } + + AccountReconcilor* account_reconcilor() { + return account_reconcilor(browser()); + } + + AccountReconcilor* account_reconcilor(Browser* browser) { + return AccountReconcilorFactory::GetForProfile(browser->profile()); + } +}; + +// This test can pass. Marked as manual because it TIMED_OUT on Win7. +// See crbug.com/1025335. +// Sings in an account through the settings page and checks that the account is +// added to Chrome. Sync should be disabled because the test doesn't pass +// through the Sync confirmation dialog. +IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_SimpleSignInFlow) { + TestAccount ta; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", ta)); + SignInFromSettings(ta, 0); + + const AccountsInCookieJarInfo& accounts_in_cookie_jar = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh); + ASSERT_EQ(1u, accounts_in_cookie_jar.signed_in_accounts.size()); + EXPECT_TRUE(accounts_in_cookie_jar.signed_out_accounts.empty()); + const gaia::ListedAccount& account = + accounts_in_cookie_jar.signed_in_accounts[0]; + EXPECT_TRUE(gaia::AreEmailsSame(ta.user, account.email)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account.id)); + EXPECT_FALSE(sync_service()->IsSyncFeatureEnabled()); +} + +// This test can pass. Marked as manual because it TIMED_OUT on Win7. +// See crbug.com/1025335. +// Signs in an account through the settings page and enables Sync. Checks that +// Sync is enabled. +// Then, signs out on the web and checks that the account is removed from +// cookies and Sync paused error is displayed. +IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_WebSignOut) { + TestAccount test_account; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account)); + TurnOnSync(test_account, 0); + + const CoreAccountInfo& primary_account = + identity_manager()->GetPrimaryAccountInfo(signin::ConsentLevel::kSync); + EXPECT_FALSE(primary_account.IsEmpty()); + EXPECT_TRUE(gaia::AreEmailsSame(test_account.user, primary_account.email)); + EXPECT_TRUE(sync_service()->IsSyncFeatureEnabled()); + + SignOutFromWeb(); + + const AccountsInCookieJarInfo& accounts_in_cookie_jar = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh); + ASSERT_TRUE(accounts_in_cookie_jar.signed_in_accounts.empty()); + ASSERT_EQ(1u, accounts_in_cookie_jar.signed_out_accounts.size()); + EXPECT_TRUE(gaia::AreEmailsSame( + test_account.user, accounts_in_cookie_jar.signed_out_accounts[0].email)); + EXPECT_TRUE( + identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState( + primary_account.account_id)); +#if !BUILDFLAG(IS_CHROMEOS_ASH) + EXPECT_EQ(GetAvatarSyncErrorType(browser()->profile()), + AvatarSyncErrorType::kAuthError); +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) +} + +// This test can pass. Marked as manual because it TIMED_OUT on Win7. +// See crbug.com/1025335. +// Sings in two accounts on the web and checks that cookies and refresh tokens +// are added to Chrome. Sync should be disabled. +// Then, signs out on the web and checks that accounts are removed from Chrome. +IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_WebSignInAndSignOut) { + TestAccount test_account_1; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1)); + SignInFromWeb(test_account_1, 0); + + const AccountsInCookieJarInfo& accounts_in_cookie_jar_1 = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar_1.accounts_are_fresh); + ASSERT_EQ(1u, accounts_in_cookie_jar_1.signed_in_accounts.size()); + EXPECT_TRUE(accounts_in_cookie_jar_1.signed_out_accounts.empty()); + const gaia::ListedAccount& account_1 = + accounts_in_cookie_jar_1.signed_in_accounts[0]; + EXPECT_TRUE(gaia::AreEmailsSame(test_account_1.user, account_1.email)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_1.id)); + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); + + TestAccount test_account_2; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_2", test_account_2)); + SignInFromWeb(test_account_2, 1); + + const AccountsInCookieJarInfo& accounts_in_cookie_jar_2 = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar_2.accounts_are_fresh); + ASSERT_EQ(2u, accounts_in_cookie_jar_2.signed_in_accounts.size()); + EXPECT_TRUE(accounts_in_cookie_jar_2.signed_out_accounts.empty()); + EXPECT_EQ(accounts_in_cookie_jar_2.signed_in_accounts[0].id, account_1.id); + const gaia::ListedAccount& account_2 = + accounts_in_cookie_jar_2.signed_in_accounts[1]; + EXPECT_TRUE(gaia::AreEmailsSame(test_account_2.user, account_2.email)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_2.id)); + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); + + SignOutFromWeb(); + + const AccountsInCookieJarInfo& accounts_in_cookie_jar_3 = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar_3.accounts_are_fresh); + ASSERT_TRUE(accounts_in_cookie_jar_3.signed_in_accounts.empty()); + EXPECT_EQ(2u, accounts_in_cookie_jar_3.signed_out_accounts.size()); + EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty()); +} + +// This test can pass. Marked as manual because it TIMED_OUT on Win7. +// See crbug.com/1025335. +// Signs in an account through the settings page and enables Sync. Checks that +// Sync is enabled. Signs in a second account on the web. +// Then, turns Sync off from the settings page and checks that both accounts are +// removed from Chrome and from cookies. +IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_TurnOffSync) { + TestAccount test_account_1; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1)); + TurnOnSync(test_account_1, 0); + + TestAccount test_account_2; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_2", test_account_2)); + SignInFromWeb(test_account_2, 1); + + const CoreAccountInfo& primary_account = + identity_manager()->GetPrimaryAccountInfo(signin::ConsentLevel::kSync); + EXPECT_FALSE(primary_account.IsEmpty()); + EXPECT_TRUE(gaia::AreEmailsSame(test_account_1.user, primary_account.email)); + EXPECT_TRUE(sync_service()->IsSyncFeatureEnabled()); + + TurnOffSync(); + + const AccountsInCookieJarInfo& accounts_in_cookie_jar_2 = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar_2.accounts_are_fresh); + ASSERT_TRUE(accounts_in_cookie_jar_2.signed_in_accounts.empty()); + EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty()); + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); +} + +// In "Sync paused" state, when the primary account is invalid, turns off sync +// from settings. Checks that the account is removed from Chrome. +// Regression test for https://crbug.com/1114646 +IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_TurnOffSyncWhenPaused) { + TestAccount test_account_1; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1)); + TurnOnSync(test_account_1, 0); + + // Get in sync paused state. + SignOutFromWeb(); + + const CoreAccountInfo& primary_account = + identity_manager()->GetPrimaryAccountInfo(signin::ConsentLevel::kSync); + EXPECT_FALSE(primary_account.IsEmpty()); + EXPECT_TRUE(gaia::AreEmailsSame(test_account_1.user, primary_account.email)); + EXPECT_TRUE(sync_service()->IsSyncFeatureEnabled()); + EXPECT_TRUE( + identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState( + primary_account.account_id)); + + TurnOffSync(); + EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty()); + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); +} + +// This test can pass. Marked as manual because it TIMED_OUT on Win7. +// See crbug.com/1025335. +// Signs in an account on the web. Goes to the Chrome settings to enable Sync +// but cancels the sync confirmation dialog. Checks that the account is still +// signed in on the web but Sync is disabled. +IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_CancelSyncWithWebAccount) { + TestAccount test_account; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account)); + SignInFromWeb(test_account, 0); + + SignInTestObserver observer(identity_manager(), account_reconcilor()); + GURL settings_url("chrome://settings"); + AddTabAtIndex(0, settings_url, ui::PageTransition::PAGE_TRANSITION_TYPED); + auto* settings_tab = browser()->tab_strip_model()->GetActiveWebContents(); + std::string start_syncing_script = base::StringPrintf( + "settings.SyncBrowserProxyImpl.getInstance()." + "startSyncingWithEmail(\"%s\", true);", + test_account.user.c_str()); + EXPECT_TRUE(content::ExecuteScript( + settings_tab, base::StringPrintf(kSettingsScriptWrapperFormat, + start_syncing_script.c_str()))); + EXPECT_TRUE(login_ui_test_utils::CancelSyncConfirmationDialog( + browser(), kDialogTimeout)); + observer.WaitForAccountChanges(1, PrimarySyncAccountWait::kWaitForCleared); + + const AccountsInCookieJarInfo& accounts_in_cookie_jar = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh); + ASSERT_EQ(1u, accounts_in_cookie_jar.signed_in_accounts.size()); + const gaia::ListedAccount& account = + accounts_in_cookie_jar.signed_in_accounts[0]; + EXPECT_TRUE(gaia::AreEmailsSame(test_account.user, account.email)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account.id)); + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); +} + +// This test can pass. Marked as manual because it TIMED_OUT on Win7. +// See crbug.com/1025335. +// Starts the sign in flow from the settings page, enters credentials on the +// login page but cancels the Sync confirmation dialog. Checks that Sync is +// disabled and no account was added to Chrome. +IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_CancelSync) { + TestAccount test_account; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account)); + SignInFromSettings(test_account, 0); + + SignInTestObserver observer(identity_manager(), account_reconcilor()); + EXPECT_TRUE(login_ui_test_utils::CancelSyncConfirmationDialog( + browser(), kDialogTimeout)); + observer.WaitForAccountChanges(0, PrimarySyncAccountWait::kWaitForCleared); + + const AccountsInCookieJarInfo& accounts_in_cookie_jar = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh); + EXPECT_TRUE(accounts_in_cookie_jar.signed_in_accounts.empty()); + EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty()); + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); +} + +// This test can pass. Marked as manual because it TIMED_OUT on Win7. +// See crbug.com/1025335. +// Enables and disables sync to account 1. Enables sync to account 2 and clicks +// on "This wasn't me" in the email confirmation dialog. Checks that the new +// profile is created. Checks that Sync to account 2 is enabled in the new +// profile. Checks that account 2 was removed from the original profile. +IN_PROC_BROWSER_TEST_F(LiveSignInTest, + MANUAL_SyncSecondAccount_CreateNewProfile) { + // Enable and disable sync for the first account. + TestAccount test_account_1; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1)); + TurnOnSync(test_account_1, 0); + TurnOffSync(); + + // Start enable sync for the second account. + TestAccount test_account_2; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_2", test_account_2)); + SignInFromSettings(test_account_2, 0); + + // Set up an observer for removing the second account from the original + // profile. + SignInTestObserver original_browser_observer(identity_manager(), + account_reconcilor()); + + // Check there is only one profile. + ProfileManager* profile_manager = g_browser_process->profile_manager(); + EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 1U); + EXPECT_EQ(chrome::GetTotalBrowserCount(), 1U); + + // Click "This wasn't me" on the email confirmation dialog and wait for a new + // browser and profile created. + EXPECT_TRUE(login_ui_test_utils::CompleteSigninEmailConfirmationDialog( + browser(), kDialogTimeout, + SigninEmailConfirmationDialog::CREATE_NEW_USER)); + Browser* new_browser = ui_test_utils::WaitForBrowserToOpen(); + EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 2U); + EXPECT_EQ(chrome::GetTotalBrowserCount(), 2U); + EXPECT_NE(browser()->profile(), new_browser->profile()); + + // Confirm sync in the new browser window. + SignInTestObserver new_browser_observer(identity_manager(new_browser), + account_reconcilor(new_browser)); + EXPECT_TRUE(login_ui_test_utils::ConfirmSyncConfirmationDialog( + new_browser, kDialogTimeout)); + new_browser_observer.WaitForAccountChanges( + 1, PrimarySyncAccountWait::kWaitForAdded); + + // Check accounts in cookies in the new profile. + const AccountsInCookieJarInfo& accounts_in_cookie_jar = + identity_manager(new_browser)->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh); + ASSERT_EQ(1u, accounts_in_cookie_jar.signed_in_accounts.size()); + const gaia::ListedAccount& account = + accounts_in_cookie_jar.signed_in_accounts[0]; + EXPECT_TRUE(gaia::AreEmailsSame(test_account_2.user, account.email)); + + // Check the primary account in the new profile is set and syncing. + const CoreAccountInfo& primary_account = + identity_manager(new_browser) + ->GetPrimaryAccountInfo(signin::ConsentLevel::kSync); + EXPECT_FALSE(primary_account.IsEmpty()); + EXPECT_TRUE(gaia::AreEmailsSame(test_account_2.user, primary_account.email)); + EXPECT_TRUE(identity_manager(new_browser) + ->HasAccountWithRefreshToken(primary_account.account_id)); + EXPECT_TRUE(sync_service(new_browser)->IsSyncFeatureEnabled()); + + // Check that the second account was removed from the original profile. + original_browser_observer.WaitForAccountChanges( + 0, PrimarySyncAccountWait::kWaitForCleared); + const AccountsInCookieJarInfo& accounts_in_cookie_jar_2 = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar_2.accounts_are_fresh); + ASSERT_TRUE(accounts_in_cookie_jar_2.signed_in_accounts.empty()); + EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty()); + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); +} + +// This test can pass. Marked as manual because it TIMED_OUT on Win7. +// See crbug.com/1025335. +// Enables and disables sync to account 1. Enables sync to account 2 and clicks +// on "This was me" in the email confirmation dialog. Checks that Sync to +// account 2 is enabled in the current profile. +IN_PROC_BROWSER_TEST_F(LiveSignInTest, + MANUAL_SyncSecondAccount_InExistingProfile) { + // Enable and disable sync for the first account. + TestAccount test_account_1; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1)); + TurnOnSync(test_account_1, 0); + TurnOffSync(); + + // Start enable sync for the second account. + TestAccount test_account_2; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_2", test_account_2)); + SignInFromSettings(test_account_2, 0); + + // Check there is only one profile. + ProfileManager* profile_manager = g_browser_process->profile_manager(); + EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 1U); + EXPECT_EQ(chrome::GetTotalBrowserCount(), 1U); + + // Click "This was me" on the email confirmation dialog, confirm sync and wait + // for a primary account to be set. + SignInTestObserver observer(identity_manager(), account_reconcilor()); + EXPECT_TRUE(login_ui_test_utils::CompleteSigninEmailConfirmationDialog( + browser(), kDialogTimeout, SigninEmailConfirmationDialog::START_SYNC)); + EXPECT_TRUE(login_ui_test_utils::ConfirmSyncConfirmationDialog( + browser(), kDialogTimeout)); + observer.WaitForAccountChanges(1, PrimarySyncAccountWait::kWaitForAdded); + + // Check no profile was created. + EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 1U); + EXPECT_EQ(chrome::GetTotalBrowserCount(), 1U); + + // Check accounts in cookies. + const AccountsInCookieJarInfo& accounts_in_cookie_jar = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh); + ASSERT_EQ(1u, accounts_in_cookie_jar.signed_in_accounts.size()); + const gaia::ListedAccount& account = + accounts_in_cookie_jar.signed_in_accounts[0]; + EXPECT_TRUE(gaia::AreEmailsSame(test_account_2.user, account.email)); + + // Check the primary account is set and syncing. + const CoreAccountInfo& primary_account = + identity_manager()->GetPrimaryAccountInfo(signin::ConsentLevel::kSync); + EXPECT_FALSE(primary_account.IsEmpty()); + EXPECT_TRUE(gaia::AreEmailsSame(test_account_2.user, primary_account.email)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken( + primary_account.account_id)); + EXPECT_TRUE(sync_service()->IsSyncFeatureEnabled()); +} + +// This test can pass. Marked as manual because it TIMED_OUT on Win7. +// See crbug.com/1025335. +// Enables and disables sync to account 1. Enables sync to account 2 and clicks +// on "Cancel" in the email confirmation dialog. Checks that the signin flow is +// canceled and no accounts are added to Chrome. +IN_PROC_BROWSER_TEST_F(LiveSignInTest, + MANUAL_SyncSecondAccount_CancelOnEmailConfirmation) { + // Enable and disable sync for the first account. + TestAccount test_account_1; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1)); + TurnOnSync(test_account_1, 0); + TurnOffSync(); + + // Start enable sync for the second account. + TestAccount test_account_2; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_2", test_account_2)); + SignInFromSettings(test_account_2, 0); + + // Check there is only one profile. + ProfileManager* profile_manager = g_browser_process->profile_manager(); + EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 1U); + EXPECT_EQ(chrome::GetTotalBrowserCount(), 1U); + + // Click "Cancel" on the email confirmation dialog and wait for an account to + // removed from Chrome. + SignInTestObserver observer(identity_manager(), account_reconcilor()); + EXPECT_TRUE(login_ui_test_utils::CompleteSigninEmailConfirmationDialog( + browser(), kDialogTimeout, SigninEmailConfirmationDialog::CLOSE)); + observer.WaitForAccountChanges(0, PrimarySyncAccountWait::kWaitForCleared); + + // Check no profile was created. + EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 1U); + EXPECT_EQ(chrome::GetTotalBrowserCount(), 1U); + + // Check Chrome has no accounts. + const AccountsInCookieJarInfo& accounts_in_cookie_jar = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh); + EXPECT_TRUE(accounts_in_cookie_jar.signed_in_accounts.empty()); + EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty()); + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); +} + +IN_PROC_BROWSER_TEST_F(LiveSignInTest, + MANUAL_AccountCapabilities_FetchedOnSignIn) { + EnableAccountCapabilitiesFetches(identity_manager()); + + // Test primary adult account. + { + AccountCapabilitiesObserver capabilities_observer(identity_manager()); + + TestAccount ta; + ASSERT_TRUE(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", ta)); + SignInFromSettings(ta, 0); + + CoreAccountInfo core_account_info = + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin); + ASSERT_TRUE(gaia::AreEmailsSame(core_account_info.email, ta.user)); + + capabilities_observer.WaitForAllCapabilitiesToBeKnown( + core_account_info.account_id); + AccountInfo account_info = + identity_manager()->FindExtendedAccountInfoByAccountId( + core_account_info.account_id); + EXPECT_EQ(account_info.capabilities.can_offer_extended_chrome_sync_promos(), + Tribool::kTrue); + } + + // Test secondary minor account. + { + AccountCapabilitiesObserver capabilities_observer(identity_manager()); + + TestAccount ta; + ASSERT_TRUE(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_MINOR", ta)); + SignInFromWeb(ta, /*previously_signed_in_accounts=*/1); + + CoreAccountInfo core_account_info = + identity_manager()->FindExtendedAccountInfoByEmailAddress(ta.user); + ASSERT_FALSE(core_account_info.IsEmpty()); + + capabilities_observer.WaitForAllCapabilitiesToBeKnown( + core_account_info.account_id); + AccountInfo account_info = + identity_manager()->FindExtendedAccountInfoByAccountId( + core_account_info.account_id); + EXPECT_EQ(account_info.capabilities.can_offer_extended_chrome_sync_promos(), + Tribool::kFalse); + } +} + +} // namespace test +} // namespace signin diff --git a/chromium/chrome/browser/signin/e2e_tests/live_test.cc b/chromium/chrome/browser/signin/e2e_tests/live_test.cc new file mode 100644 index 00000000000..e8d22189e73 --- /dev/null +++ b/chromium/chrome/browser/signin/e2e_tests/live_test.cc @@ -0,0 +1,61 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#include "chrome/browser/signin/e2e_tests/live_test.h" + +#include "base/files/file_util.h" +#include "base/path_service.h" +#include "net/dns/mock_host_resolver.h" + +base::FilePath::StringPieceType kTestAccountFilePath = FILE_PATH_LITERAL( + "chrome/browser/internal/resources/signin/test_accounts.json"); + +const char* kRunLiveTestFlag = "run-live-tests"; + +namespace signin { +namespace test { + +void LiveTest::SetUpInProcessBrowserTestFixture() { + // Whitelists a bunch of hosts. + host_resolver()->AllowDirectLookup("*.google.com"); + host_resolver()->AllowDirectLookup("*.geotrust.com"); + host_resolver()->AllowDirectLookup("*.gstatic.com"); + host_resolver()->AllowDirectLookup("*.googleapis.com"); + // Allows country-specific TLDs. + host_resolver()->AllowDirectLookup("accounts.google.*"); + + InProcessBrowserTest::SetUpInProcessBrowserTestFixture(); +} + +void LiveTest::SetUp() { + // Only run live tests when specified. + auto* cmd_line = base::CommandLine::ForCurrentProcess(); + if (!cmd_line->HasSwitch(kRunLiveTestFlag)) { + LOG(INFO) << "This test should get skipped."; + skip_test_ = true; + GTEST_SKIP(); + } + base::FilePath root_path; + base::PathService::Get(base::BasePathKey::DIR_SOURCE_ROOT, &root_path); + base::FilePath config_path = + base::MakeAbsoluteFilePath(root_path.Append(kTestAccountFilePath)); + test_accounts_.Init(config_path); + InProcessBrowserTest::SetUp(); +} + +void LiveTest::TearDown() { + // This test was skipped, no need to tear down. + if (skip_test_) + return; + InProcessBrowserTest::TearDown(); +} + +void LiveTest::PostRunTestOnMainThread() { + // This test was skipped. Running PostRunTestOnMainThread can cause + // TIMED_OUT on Win7. + if (skip_test_) + return; + InProcessBrowserTest::PostRunTestOnMainThread(); +} +} // namespace test +} // namespace signin diff --git a/chromium/chrome/browser/signin/e2e_tests/live_test.h b/chromium/chrome/browser/signin/e2e_tests/live_test.h new file mode 100644 index 00000000000..6de9f8f4ded --- /dev/null +++ b/chromium/chrome/browser/signin/e2e_tests/live_test.h @@ -0,0 +1,33 @@ +// Copyright 2019 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 CHROME_BROWSER_SIGNIN_E2E_TESTS_LIVE_TEST_H_ +#define CHROME_BROWSER_SIGNIN_E2E_TESTS_LIVE_TEST_H_ + +#include "chrome/browser/signin/e2e_tests/test_accounts_util.h" +#include "chrome/test/base/in_process_browser_test.h" + +namespace signin { +namespace test { + +class LiveTest : public InProcessBrowserTest { + protected: + void SetUpInProcessBrowserTestFixture() override; + void SetUp() override; + void TearDown() override; + void PostRunTestOnMainThread() override; + + const TestAccountsUtil* GetTestAccountsUtil() const { + return &test_accounts_; + } + + private: + TestAccountsUtil test_accounts_; + bool skip_test_ = false; +}; + +} // namespace test +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_E2E_TESTS_LIVE_TEST_H_ diff --git a/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.cc b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.cc new file mode 100644 index 00000000000..c533ea0fb3c --- /dev/null +++ b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.cc @@ -0,0 +1,75 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/e2e_tests/test_accounts_util.h" + +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/json/json_file_value_serializer.h" +#include "base/json/json_reader.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" + +using base::Value; + +namespace signin { +namespace test { + +#if defined(OS_WIN) +std::string kPlatform = "win"; +#elif defined(OS_MAC) +std::string kPlatform = "mac"; +#elif BUILDFLAG(IS_CHROMEOS_ASH) +std::string kPlatform = "chromeos"; +#elif defined(OS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS) +std::string kPlatform = "linux"; +#elif defined(OS_ANDROID) +std::string kPlatform = "android"; +#else +std::string kPlatform = "all_platform"; +#endif + +TestAccountsUtil::TestAccountsUtil() = default; +TestAccountsUtil::~TestAccountsUtil() = default; + +bool TestAccountsUtil::Init(const base::FilePath& config_path) { + int error_code = 0; + std::string error_str; + JSONFileValueDeserializer deserializer(config_path); + std::unique_ptr content_json = + deserializer.Deserialize(&error_code, &error_str); + CHECK(error_code == 0) << "Error reading json file. Error code: " + << error_code << " " << error_str; + CHECK(content_json); + + // Only store platform specific users. If an account does not have + // platform specific user, try to use all_platform user. + for (auto account : content_json->DictItems()) { + const Value* platform_account = account.second.FindDictKey(kPlatform); + if (platform_account == nullptr) { + platform_account = account.second.FindDictKey("all_platform"); + if (platform_account == nullptr) { + continue; + } + } + TestAccount ta(*(platform_account->FindStringKey("user")), + *(platform_account->FindStringKey("password"))); + all_accounts_.insert( + std::pair(account.first, ta)); + } + return true; +} + +bool TestAccountsUtil::GetAccount(const std::string& name, + TestAccount& out_account) const { + auto it = all_accounts_.find(name); + if (it == all_accounts_.end()) { + return false; + } + out_account = it->second; + return true; +} + +} // namespace test +} // namespace signin diff --git a/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.h b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.h new file mode 100644 index 00000000000..4f73dd76d32 --- /dev/null +++ b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.h @@ -0,0 +1,46 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_SIGNIN_E2E_TESTS_TEST_ACCOUNTS_UTIL_H_ +#define CHROME_BROWSER_SIGNIN_E2E_TESTS_TEST_ACCOUNTS_UTIL_H_ + +#include +#include + +namespace base { +class FilePath; +} + +namespace signin { +namespace test { + +struct TestAccount { + std::string user; + std::string password; + TestAccount() = default; + TestAccount(const std::string& user, const std::string& password) { + this->user = user; + this->password = password; + } +}; + +class TestAccountsUtil { + public: + TestAccountsUtil(); + + TestAccountsUtil(const TestAccountsUtil&) = delete; + TestAccountsUtil& operator=(const TestAccountsUtil&) = delete; + + virtual ~TestAccountsUtil(); + bool Init(const base::FilePath& config_path); + bool GetAccount(const std::string& name, TestAccount& out_account) const; + + private: + std::map all_accounts_; +}; + +} // namespace test +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_E2E_TESTS_TEST_ACCOUNTS_UTIL_H_ diff --git a/chromium/chrome/browser/signin/e2e_tests/test_accounts_util_unittest.cc b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util_unittest.cc new file mode 100644 index 00000000000..7b63dd6128a --- /dev/null +++ b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util_unittest.cc @@ -0,0 +1,100 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/e2e_tests/test_accounts_util.h" +#include "base/files/file_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +using base::FilePath; + +namespace signin { +namespace test { + +class TestAccountsUtilTest : public testing::Test {}; + +FilePath WriteContentToTemporaryFile(const char* contents, + unsigned int length) { + FilePath tmp_file; + CHECK(base::CreateTemporaryFile(&tmp_file)); + unsigned int bytes_written = base::WriteFile(tmp_file, contents, length); + CHECK_EQ(bytes_written, length); + return tmp_file; +} + +TEST(TestAccountsUtilTest, ParsingJson) { + const char contents[] = + "{ \n" + " \"TEST_ACCOUNT_1\": {\n" + " \"win\": {\n" + " \"user\": \"user1\",\n" + " \"password\": \"pwd1\"\n" + " }\n" + " }\n" + "}"; + FilePath tmp_file = + WriteContentToTemporaryFile(contents, sizeof(contents) - 1); + TestAccountsUtil util; + util.Init(tmp_file); +} + +TEST(TestAccountsUtilTest, GetAccountForPlatformSpecific) { + const char contents[] = + "{ \n" + " \"TEST_ACCOUNT_1\": {\n" + " \"win\": {\n" + " \"user\": \"user1\",\n" + " \"password\": \"pwd1\"\n" + " },\n" + " \"mac\": {\n" + " \"user\": \"user1\",\n" + " \"password\": \"pwd1\"\n" + " },\n" + " \"linux\": {\n" + " \"user\": \"user1\",\n" + " \"password\": \"pwd1\"\n" + " },\n" + " \"chromeos\": {\n" + " \"user\": \"user1\",\n" + " \"password\": \"pwd1\"\n" + " },\n" + " \"android\": {\n" + " \"user\": \"user1\",\n" + " \"password\": \"pwd1\"\n" + " }\n" + " }\n" + "}"; + FilePath tmp_file = + WriteContentToTemporaryFile(contents, sizeof(contents) - 1); + TestAccountsUtil util; + util.Init(tmp_file); + TestAccount ta; + bool ret = util.GetAccount("TEST_ACCOUNT_1", ta); + ASSERT_TRUE(ret); + ASSERT_EQ(ta.user, "user1"); + ASSERT_EQ(ta.password, "pwd1"); +} + +TEST(TestAccountsUtilTest, GetAccountForAllPlatform) { + const char contents[] = + "{ \n" + " \"TEST_ACCOUNT_1\": {\n" + " \"all_platform\": {\n" + " \"user\": \"user_allplatform\",\n" + " \"password\": \"pwd_allplatform\"\n" + " }\n" + " }\n" + "}"; + FilePath tmp_file = + WriteContentToTemporaryFile(contents, sizeof(contents) - 1); + TestAccountsUtil util; + util.Init(tmp_file); + TestAccount ta; + bool ret = util.GetAccount("TEST_ACCOUNT_1", ta); + ASSERT_TRUE(ret); + ASSERT_EQ(ta.user, "user_allplatform"); + ASSERT_EQ(ta.password, "pwd_allplatform"); +} + +} // namespace test +} // namespace signin diff --git a/chromium/chrome/browser/signin/force_signin_verifier.cc b/chromium/chrome/browser/signin/force_signin_verifier.cc new file mode 100644 index 00000000000..951efcd003c --- /dev/null +++ b/chromium/chrome/browser/signin/force_signin_verifier.cc @@ -0,0 +1,195 @@ +// Copyright 2017 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 + +#include "chrome/browser/signin/force_signin_verifier.h" + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/files/file_path.h" +#include "base/metrics/histogram_macros.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_attributes_entry.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/ui/browser_list.h" +#include "chrome/browser/ui/profile_picker.h" +#include "chrome/browser/ui/ui_features.h" +#include "components/signin/public/base/signin_metrics.h" +#include "components/signin/public/identity_manager/access_token_info.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/primary_account_access_token_fetcher.h" +#include "components/signin/public/identity_manager/primary_account_mutator.h" +#include "components/signin/public/identity_manager/scope_set.h" +#include "content/public/browser/network_service_instance.h" +#include "google_apis/gaia/gaia_constants.h" + +namespace { +const net::BackoffEntry::Policy kForceSigninVerifierBackoffPolicy = { + 0, // Number of initial errors to ignore before applying + // exponential back-off rules. + 2000, // Initial delay in ms. + 2, // Factor by which the waiting time will be multiplied. + 0.2, // Fuzzing percentage. + 4 * 60 * 1000, // Maximum amount of time to delay th request in ms. + -1, // Never discard the entry. + false // Do not always use initial delay. +}; + +} // namespace + +ForceSigninVerifier::ForceSigninVerifier( + Profile* profile, + signin::IdentityManager* identity_manager) + : has_token_verified_(false), + backoff_entry_(&kForceSigninVerifierBackoffPolicy), + creation_time_(base::TimeTicks::Now()), + profile_(profile), + identity_manager_(identity_manager) { + content::GetNetworkConnectionTracker()->AddNetworkConnectionObserver(this); + // Most of time (~94%), sign-in token can be verified with server. + SendRequest(); +} + +ForceSigninVerifier::~ForceSigninVerifier() { + Cancel(); +} + +void ForceSigninVerifier::OnAccessTokenFetchComplete( + GoogleServiceAuthError error, + signin::AccessTokenInfo token_info) { + if (error.state() != GoogleServiceAuthError::NONE) { + if (error.IsPersistentError()) { + // Based on the obsolete UMA Signin.ForceSigninVerificationTime.Failure, + // about 7% verifications are failed. Most of them are finished within + // 113ms but some of them (<3%) could take longer than 3 minutes. + has_token_verified_ = true; + CloseAllBrowserWindows(); + content::GetNetworkConnectionTracker()->RemoveNetworkConnectionObserver( + this); + Cancel(); + } else { + backoff_entry_.InformOfRequest(false); + backoff_request_timer_.Start( + FROM_HERE, backoff_entry_.GetTimeUntilRelease(), + base::BindOnce(&ForceSigninVerifier::SendRequest, + weak_factory_.GetWeakPtr())); + access_token_fetcher_.reset(); + } + return; + } + + // Based on the obsolete UMA Signin.ForceSigninVerificationTime.Success, about + // 93% verifications are succeeded. Most of them are finished ~1 second but + // some of them (<3%) could take longer than 3 minutes. + has_token_verified_ = true; + content::GetNetworkConnectionTracker()->RemoveNetworkConnectionObserver(this); + Cancel(); +} + +void ForceSigninVerifier::OnConnectionChanged( + network::mojom::ConnectionType type) { + // Try again immediately once the network is back and cancel any pending + // request. + backoff_entry_.Reset(); + if (backoff_request_timer_.IsRunning()) + backoff_request_timer_.Stop(); + + SendRequestIfNetworkAvailable(type); +} + +void ForceSigninVerifier::Cancel() { + backoff_entry_.Reset(); + backoff_request_timer_.Stop(); + access_token_fetcher_.reset(); + content::GetNetworkConnectionTracker()->RemoveNetworkConnectionObserver(this); +} + +bool ForceSigninVerifier::HasTokenBeenVerified() { + return has_token_verified_; +} + +void ForceSigninVerifier::SendRequest() { + auto type = network::mojom::ConnectionType::CONNECTION_NONE; + if (content::GetNetworkConnectionTracker()->GetConnectionType( + &type, + base::BindOnce(&ForceSigninVerifier::SendRequestIfNetworkAvailable, + weak_factory_.GetWeakPtr()))) { + SendRequestIfNetworkAvailable(type); + } +} + +void ForceSigninVerifier::SendRequestIfNetworkAvailable( + network::mojom::ConnectionType network_type) { + if (network_type == network::mojom::ConnectionType::CONNECTION_NONE || + !ShouldSendRequest()) { + return; + } + + signin::ScopeSet oauth2_scopes; + oauth2_scopes.insert(GaiaConstants::kChromeSyncOAuth2Scope); + access_token_fetcher_ = + std::make_unique( + "force_signin_verifier", identity_manager_, oauth2_scopes, + base::BindOnce(&ForceSigninVerifier::OnAccessTokenFetchComplete, + weak_factory_.GetWeakPtr()), + signin::PrimaryAccountAccessTokenFetcher::Mode::kImmediate); +} + +bool ForceSigninVerifier::ShouldSendRequest() { + return !has_token_verified_ && access_token_fetcher_.get() == nullptr && + identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync); +} + +void ForceSigninVerifier::CloseAllBrowserWindows() { + if (base::FeatureList::IsEnabled(features::kForceSignInReauth)) { + // Do not sign the user out to allow them to reauthenticate from the profile + // picker. + BrowserList::CloseAllBrowsersWithProfile( + profile_, + base::BindRepeating(&ForceSigninVerifier::OnCloseBrowsersSuccess, + weak_factory_.GetWeakPtr()), + base::DoNothing(), + /*skip_beforeunload=*/true); + } else { + // Do not close window if there is ongoing reauth. If it fails later, the + // signin process should take care of the signout. + auto* primary_account_mutator = + identity_manager_->GetPrimaryAccountMutator(); + if (!primary_account_mutator) + return; + primary_account_mutator->ClearPrimaryAccount( + signin_metrics::AUTHENTICATION_FAILED_WITH_FORCE_SIGNIN, + signin_metrics::SignoutDelete::kIgnoreMetric); + } +} + +void ForceSigninVerifier::OnCloseBrowsersSuccess( + const base::FilePath& profile_path) { + Cancel(); + + ProfileAttributesEntry* entry = + g_browser_process->profile_manager() + ->GetProfileAttributesStorage() + .GetProfileAttributesWithPath(profile_path); + if (!entry) + return; + entry->LockForceSigninProfile(true); + ProfilePicker::Show(ProfilePicker::EntryPoint::kProfileLocked); +} + +signin::PrimaryAccountAccessTokenFetcher* +ForceSigninVerifier::GetAccessTokenFetcherForTesting() { + return access_token_fetcher_.get(); +} + +net::BackoffEntry* ForceSigninVerifier::GetBackoffEntryForTesting() { + return &backoff_entry_; +} + +base::OneShotTimer* ForceSigninVerifier::GetOneShotTimerForTesting() { + return &backoff_request_timer_; +} diff --git a/chromium/chrome/browser/signin/force_signin_verifier.h b/chromium/chrome/browser/signin/force_signin_verifier.h new file mode 100644 index 00000000000..7260aa6f3fe --- /dev/null +++ b/chromium/chrome/browser/signin/force_signin_verifier.h @@ -0,0 +1,95 @@ +// Copyright 2017 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 CHROME_BROWSER_SIGNIN_FORCE_SIGNIN_VERIFIER_H_ +#define CHROME_BROWSER_SIGNIN_FORCE_SIGNIN_VERIFIER_H_ + +#include + +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/time/time.h" +#include "base/timer/timer.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "net/base/backoff_entry.h" +#include "services/network/public/cpp/network_connection_tracker.h" + +class Profile; + +namespace base { +class FilePath; +} + +namespace signin { +class IdentityManager; +class PrimaryAccountAccessTokenFetcher; +struct AccessTokenInfo; +} // namespace signin + +// ForceSigninVerifier will verify profile's auth token when profile is loaded +// into memory by the first time via gaia server. It will retry on any transient +// error. +class ForceSigninVerifier + : public network::NetworkConnectionTracker::NetworkConnectionObserver { + public: + explicit ForceSigninVerifier(Profile* profile, + signin::IdentityManager* identity_manager); + + ForceSigninVerifier(const ForceSigninVerifier&) = delete; + ForceSigninVerifier& operator=(const ForceSigninVerifier&) = delete; + + ~ForceSigninVerifier() override; + + void OnAccessTokenFetchComplete(GoogleServiceAuthError error, + signin::AccessTokenInfo token_info); + + // override network::NetworkConnectionTracker::NetworkConnectionObserver + void OnConnectionChanged(network::mojom::ConnectionType type) override; + + // Cancel any pending or ongoing verification. + void Cancel(); + + // Return the value of |has_token_verified_|. + bool HasTokenBeenVerified(); + + protected: + // Send the token verification request. The request will be sent only if + // - The token has never been verified before. + // - There is no on going verification. + // - There is network connection. + // - The profile has signed in. + void SendRequest(); + + // Send the request if |network_type| is not CONNECTION_NONE and + // ShouldSendRequest returns true. + void SendRequestIfNetworkAvailable( + network::mojom::ConnectionType network_type); + + bool ShouldSendRequest(); + + virtual void CloseAllBrowserWindows(); + void OnCloseBrowsersSuccess(const base::FilePath& profile_path); + + signin::PrimaryAccountAccessTokenFetcher* GetAccessTokenFetcherForTesting(); + net::BackoffEntry* GetBackoffEntryForTesting(); + base::OneShotTimer* GetOneShotTimerForTesting(); + + private: + std::unique_ptr + access_token_fetcher_; + + // Indicates whether the verification is finished successfully or with a + // persistent error. + bool has_token_verified_ = false; + net::BackoffEntry backoff_entry_; + base::OneShotTimer backoff_request_timer_; + base::TimeTicks creation_time_; + + raw_ptr profile_ = nullptr; + raw_ptr identity_manager_ = nullptr; + + base::WeakPtrFactory weak_factory_{this}; +}; + +#endif // CHROME_BROWSER_SIGNIN_FORCE_SIGNIN_VERIFIER_H_ diff --git a/chromium/chrome/browser/signin/force_signin_verifier_unittest.cc b/chromium/chrome/browser/signin/force_signin_verifier_unittest.cc new file mode 100644 index 00000000000..dcd382d6b38 --- /dev/null +++ b/chromium/chrome/browser/signin/force_signin_verifier_unittest.cc @@ -0,0 +1,416 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/force_signin_verifier.h" + +#include "base/run_loop.h" +#include "base/test/task_environment.h" +#include "base/threading/thread_task_runner_handle.h" +#include "chrome/browser/profiles/profile.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "content/public/browser/network_service_instance.h" +#include "services/network/test/test_network_connection_tracker.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +class ForceSigninVerifierWithAccessToInternalsForTesting + : public ForceSigninVerifier { + public: + explicit ForceSigninVerifierWithAccessToInternalsForTesting( + signin::IdentityManager* identity_manager) + : ForceSigninVerifier(nullptr, identity_manager) {} + + bool IsDelayTaskPosted() { return GetOneShotTimerForTesting()->IsRunning(); } + + int FailureCount() { return GetBackoffEntryForTesting()->failure_count(); } + + signin::PrimaryAccountAccessTokenFetcher* access_token_fetcher() { + return GetAccessTokenFetcherForTesting(); + } + + MOCK_METHOD0(CloseAllBrowserWindows, void(void)); +}; + +// A NetworkConnectionObserver that invokes a base::RepeatingClosure when +// NetworkConnectionObserver::OnConnectionChanged() is invoked. +class NetworkConnectionObserverHelper + : public network::NetworkConnectionTracker::NetworkConnectionObserver { + public: + explicit NetworkConnectionObserverHelper(base::RepeatingClosure closure) + : closure_(std::move(closure)) { + DCHECK(!closure_.is_null()); + content::GetNetworkConnectionTracker()->AddNetworkConnectionObserver(this); + } + + NetworkConnectionObserverHelper(const NetworkConnectionObserverHelper&) = + delete; + NetworkConnectionObserverHelper& operator=( + const NetworkConnectionObserverHelper&) = delete; + + ~NetworkConnectionObserverHelper() override { + content::GetNetworkConnectionTracker()->RemoveNetworkConnectionObserver( + this); + } + + void OnConnectionChanged(network::mojom::ConnectionType type) override { + closure_.Run(); + } + + private: + base::RepeatingClosure closure_; +}; + +// Used to select which type of network type NetworkConnectionTracker should +// be configured to. +enum class NetworkConnectionType { + Undecided, + ConnectionNone, + ConnectionWifi, + Connection4G, +}; + +// Used to select which type of response NetworkConnectionTracker should give. +enum class NetworkResponseType { + Undecided, + Synchronous, + Asynchronous, +}; + +// Forces the network connection type to change to |connection_type| and wait +// till the notification has been propagated to the observers. Also change the +// response type to be synchronous/asynchronous based on |response_type|. +void ConfigureNetworkConnectionTracker(NetworkConnectionType connection_type, + NetworkResponseType response_type) { + network::TestNetworkConnectionTracker* tracker = + network::TestNetworkConnectionTracker::GetInstance(); + + switch (response_type) { + case NetworkResponseType::Undecided: + // nothing to do + break; + + case NetworkResponseType::Synchronous: + tracker->SetRespondSynchronously(true); + break; + + case NetworkResponseType::Asynchronous: + tracker->SetRespondSynchronously(false); + break; + } + + if (connection_type != NetworkConnectionType::Undecided) { + network::mojom::ConnectionType mojom_connection_type = + network::mojom::ConnectionType::CONNECTION_UNKNOWN; + + switch (connection_type) { + case NetworkConnectionType::Undecided: + NOTREACHED(); + break; + + case NetworkConnectionType::ConnectionNone: + mojom_connection_type = network::mojom::ConnectionType::CONNECTION_NONE; + break; + + case NetworkConnectionType::ConnectionWifi: + mojom_connection_type = network::mojom::ConnectionType::CONNECTION_WIFI; + break; + + case NetworkConnectionType::Connection4G: + mojom_connection_type = network::mojom::ConnectionType::CONNECTION_4G; + break; + } + + DCHECK_NE(mojom_connection_type, + network::mojom::ConnectionType::CONNECTION_UNKNOWN); + + base::RunLoop wait_for_network_type_change; + NetworkConnectionObserverHelper scoped_observer( + wait_for_network_type_change.QuitWhenIdleClosure()); + + tracker->SetConnectionType(mojom_connection_type); + + wait_for_network_type_change.Run(); + } +} + +// Forces the current sequence's task runner to spin. This is used because the +// ForceSigninVerifier ends up posting task to the sequence's task runner when +// MetworkConnectionTracker is returning results asynchronously. +void SpinCurrentSequenceTaskRunner() { + base::RunLoop run_loop; + base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, + run_loop.QuitClosure()); + run_loop.Run(); +} + +} // namespace + +TEST(ForceSigninVerifierTest, OnGetTokenSuccess) { + base::test::TaskEnvironment scoped_task_env; + signin::IdentityTestEnvironment identity_test_env; + const AccountInfo account_info = + identity_test_env.MakePrimaryAccountAvailable( + "email@test.com", signin::ConsentLevel::kSync); + + ForceSigninVerifierWithAccessToInternalsForTesting verifier( + identity_test_env.identity_manager()); + + ASSERT_NE(nullptr, verifier.access_token_fetcher()); + ASSERT_FALSE(verifier.HasTokenBeenVerified()); + ASSERT_FALSE(verifier.IsDelayTaskPosted()); + EXPECT_CALL(verifier, CloseAllBrowserWindows()).Times(0); + + identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken( + account_info.account_id, /*token=*/"", base::Time()); + + ASSERT_EQ(nullptr, verifier.access_token_fetcher()); + ASSERT_TRUE(verifier.HasTokenBeenVerified()); + ASSERT_FALSE(verifier.IsDelayTaskPosted()); + ASSERT_EQ(0, verifier.FailureCount()); +} + +TEST(ForceSigninVerifierTest, OnGetTokenPersistentFailure) { + base::test::TaskEnvironment scoped_task_env; + signin::IdentityTestEnvironment identity_test_env; + const AccountInfo account_info = + identity_test_env.MakePrimaryAccountAvailable( + "email@test.com", signin::ConsentLevel::kSync); + + ForceSigninVerifierWithAccessToInternalsForTesting verifier( + identity_test_env.identity_manager()); + + ASSERT_NE(nullptr, verifier.access_token_fetcher()); + ASSERT_FALSE(verifier.HasTokenBeenVerified()); + ASSERT_FALSE(verifier.IsDelayTaskPosted()); + EXPECT_CALL(verifier, CloseAllBrowserWindows()).Times(1); + + identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithError( + GoogleServiceAuthError( + GoogleServiceAuthError::State::INVALID_GAIA_CREDENTIALS)); + + ASSERT_EQ(nullptr, verifier.access_token_fetcher()); + ASSERT_TRUE(verifier.HasTokenBeenVerified()); + ASSERT_FALSE(verifier.IsDelayTaskPosted()); + ASSERT_EQ(0, verifier.FailureCount()); +} + +TEST(ForceSigninVerifierTest, OnGetTokenTransientFailure) { + base::test::TaskEnvironment scoped_task_env; + signin::IdentityTestEnvironment identity_test_env; + const AccountInfo account_info = + identity_test_env.MakePrimaryAccountAvailable( + "email@test.com", signin::ConsentLevel::kSync); + + ForceSigninVerifierWithAccessToInternalsForTesting verifier( + identity_test_env.identity_manager()); + + ASSERT_NE(nullptr, verifier.access_token_fetcher()); + ASSERT_FALSE(verifier.HasTokenBeenVerified()); + ASSERT_FALSE(verifier.IsDelayTaskPosted()); + EXPECT_CALL(verifier, CloseAllBrowserWindows()).Times(0); + + identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithError( + GoogleServiceAuthError(GoogleServiceAuthError::State::CONNECTION_FAILED)); + + ASSERT_EQ(nullptr, verifier.access_token_fetcher()); + ASSERT_FALSE(verifier.HasTokenBeenVerified()); + ASSERT_TRUE(verifier.IsDelayTaskPosted()); + ASSERT_EQ(1, verifier.FailureCount()); +} + +TEST(ForceSigninVerifierTest, OnLostConnection) { + base::test::TaskEnvironment scoped_task_env; + signin::IdentityTestEnvironment identity_test_env; + const AccountInfo account_info = + identity_test_env.MakePrimaryAccountAvailable( + "email@test.com", signin::ConsentLevel::kSync); + + ForceSigninVerifierWithAccessToInternalsForTesting verifier( + identity_test_env.identity_manager()); + + identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithError( + GoogleServiceAuthError(GoogleServiceAuthError::State::CONNECTION_FAILED)); + + ASSERT_EQ(1, verifier.FailureCount()); + ASSERT_EQ(nullptr, verifier.access_token_fetcher()); + ASSERT_TRUE(verifier.IsDelayTaskPosted()); + + ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionNone, + NetworkResponseType::Undecided); + + ASSERT_EQ(0, verifier.FailureCount()); + ASSERT_EQ(nullptr, verifier.access_token_fetcher()); + ASSERT_FALSE(verifier.IsDelayTaskPosted()); +} + +TEST(ForceSigninVerifierTest, OnReconnected) { + base::test::TaskEnvironment scoped_task_env; + signin::IdentityTestEnvironment identity_test_env; + const AccountInfo account_info = + identity_test_env.MakePrimaryAccountAvailable( + "email@test.com", signin::ConsentLevel::kSync); + + ForceSigninVerifierWithAccessToInternalsForTesting verifier( + identity_test_env.identity_manager()); + + identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithError( + GoogleServiceAuthError(GoogleServiceAuthError::State::CONNECTION_FAILED)); + + ASSERT_EQ(1, verifier.FailureCount()); + ASSERT_EQ(nullptr, verifier.access_token_fetcher()); + ASSERT_TRUE(verifier.IsDelayTaskPosted()); + + ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionWifi, + NetworkResponseType::Undecided); + + ASSERT_EQ(0, verifier.FailureCount()); + ASSERT_NE(nullptr, verifier.access_token_fetcher()); + ASSERT_FALSE(verifier.IsDelayTaskPosted()); +} + +TEST(ForceSigninVerifierTest, GetNetworkStatusAsync) { + base::test::TaskEnvironment scoped_task_env; + signin::IdentityTestEnvironment identity_test_env; + const AccountInfo account_info = + identity_test_env.MakePrimaryAccountAvailable( + "email@test.com", signin::ConsentLevel::kSync); + + ConfigureNetworkConnectionTracker(NetworkConnectionType::Undecided, + NetworkResponseType::Asynchronous); + + ForceSigninVerifierWithAccessToInternalsForTesting verifier( + identity_test_env.identity_manager()); + + // There is no network type at first. + ASSERT_EQ(nullptr, verifier.access_token_fetcher()); + + // Waiting for the network type returns. + SpinCurrentSequenceTaskRunner(); + + // Get the type and send the request. + ASSERT_NE(nullptr, verifier.access_token_fetcher()); +} + +TEST(ForceSigninVerifierTest, LaunchVerifierWithoutNetwork) { + base::test::TaskEnvironment scoped_task_env; + signin::IdentityTestEnvironment identity_test_env; + const AccountInfo account_info = + identity_test_env.MakePrimaryAccountAvailable( + "email@test.com", signin::ConsentLevel::kSync); + + ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionNone, + NetworkResponseType::Asynchronous); + + ForceSigninVerifierWithAccessToInternalsForTesting verifier( + identity_test_env.identity_manager()); + + // There is no network type. + ASSERT_EQ(nullptr, verifier.access_token_fetcher()); + + // Waiting for the network type returns. + SpinCurrentSequenceTaskRunner(); + + // Get the type, there is no network connection, don't send the request. + ASSERT_EQ(nullptr, verifier.access_token_fetcher()); + + // Network is resumed. + ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionWifi, + NetworkResponseType::Undecided); + + // Send the request. + ASSERT_NE(nullptr, verifier.access_token_fetcher()); +} + +TEST(ForceSigninVerifierTest, ChangeNetworkFromWIFITo4GWithOnGoingRequest) { + base::test::TaskEnvironment scoped_task_env; + signin::IdentityTestEnvironment identity_test_env; + const AccountInfo account_info = + identity_test_env.MakePrimaryAccountAvailable( + "email@test.com", signin::ConsentLevel::kSync); + + ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionWifi, + NetworkResponseType::Asynchronous); + + ForceSigninVerifierWithAccessToInternalsForTesting verifier( + identity_test_env.identity_manager()); + + EXPECT_EQ(nullptr, verifier.access_token_fetcher()); + + // Waiting for the network type returns. + SpinCurrentSequenceTaskRunner(); + + // The network type if wifi, send the request. + auto* first_request = verifier.access_token_fetcher(); + EXPECT_NE(nullptr, first_request); + + // Network is changed to 4G. + ConfigureNetworkConnectionTracker(NetworkConnectionType::Connection4G, + NetworkResponseType::Undecided); + + // There is still one on-going request. + EXPECT_EQ(first_request, verifier.access_token_fetcher()); + identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken( + account_info.account_id, /*token=*/"", base::Time()); +} + +TEST(ForceSigninVerifierTest, ChangeNetworkFromWIFITo4GWithFinishedRequest) { + base::test::TaskEnvironment scoped_task_env; + signin::IdentityTestEnvironment identity_test_env; + const AccountInfo account_info = + identity_test_env.MakePrimaryAccountAvailable( + "email@test.com", signin::ConsentLevel::kSync); + + ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionWifi, + NetworkResponseType::Asynchronous); + + ForceSigninVerifierWithAccessToInternalsForTesting verifier( + identity_test_env.identity_manager()); + + EXPECT_EQ(nullptr, verifier.access_token_fetcher()); + + // Waiting for the network type returns. + SpinCurrentSequenceTaskRunner(); + + // The network type if wifi, send the request. + EXPECT_NE(nullptr, verifier.access_token_fetcher()); + + // Finishes the request. + identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken( + account_info.account_id, /*token=*/"", base::Time()); + EXPECT_EQ(nullptr, verifier.access_token_fetcher()); + + // Network is changed to 4G. + ConfigureNetworkConnectionTracker(NetworkConnectionType::Connection4G, + NetworkResponseType::Undecided); + + // No more request because it's verfied already. + EXPECT_EQ(nullptr, verifier.access_token_fetcher()); +} + +// Regression test for https://crbug.com/1259864 +TEST(ForceSigninVerifierTest, DeleteWithPendingRequestShouldNotCrash) { + base::test::TaskEnvironment scoped_task_env; + signin::IdentityTestEnvironment identity_test_env; + const AccountInfo account_info = + identity_test_env.MakePrimaryAccountAvailable( + "email@test.com", signin::ConsentLevel::kSync); + + ConfigureNetworkConnectionTracker(NetworkConnectionType::Undecided, + NetworkResponseType::Asynchronous); + + { + ForceSigninVerifierWithAccessToInternalsForTesting verifier( + identity_test_env.identity_manager()); + + // There is no network type at first. + ASSERT_EQ(nullptr, verifier.access_token_fetcher()); + + // Delete the verifier while the request is pending. + } + + // Waiting for the network type returns, this should not crash. + SpinCurrentSequenceTaskRunner(); +} diff --git a/chromium/chrome/browser/signin/header_modification_delegate.h b/chromium/chrome/browser/signin/header_modification_delegate.h new file mode 100644 index 00000000000..8d3bce133ac --- /dev/null +++ b/chromium/chrome/browser/signin/header_modification_delegate.h @@ -0,0 +1,38 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_SIGNIN_HEADER_MODIFICATION_DELEGATE_H_ +#define CHROME_BROWSER_SIGNIN_HEADER_MODIFICATION_DELEGATE_H_ + +class GURL; + +namespace content { +class WebContents; +} + +namespace signin { + +class ChromeRequestAdapter; +class ResponseAdapter; + +class HeaderModificationDelegate { + public: + HeaderModificationDelegate() = default; + + HeaderModificationDelegate(const HeaderModificationDelegate&) = delete; + HeaderModificationDelegate& operator=(const HeaderModificationDelegate&) = + delete; + + virtual ~HeaderModificationDelegate() = default; + + virtual bool ShouldInterceptNavigation(content::WebContents* contents) = 0; + virtual void ProcessRequest(ChromeRequestAdapter* request_adapter, + const GURL& redirect_url) = 0; + virtual void ProcessResponse(ResponseAdapter* response_adapter, + const GURL& redirect_url) = 0; +}; + +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_HEADER_MODIFICATION_DELEGATE_H_ diff --git a/chromium/chrome/browser/signin/header_modification_delegate_impl.cc b/chromium/chrome/browser/signin/header_modification_delegate_impl.cc new file mode 100644 index 00000000000..6aa2d10614f --- /dev/null +++ b/chromium/chrome/browser/signin/header_modification_delegate_impl.cc @@ -0,0 +1,154 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/header_modification_delegate_impl.h" + +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/content_settings/cookie_settings_factory.h" +#include "chrome/browser/extensions/api/identity/web_auth_flow.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/signin/chrome_signin_helper.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/sync/sync_service_factory.h" +#include "chrome/common/pref_names.h" +#include "components/prefs/pref_service.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/account_info.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/tribool.h" +#include "components/sync/base/pref_names.h" +#include "components/sync/driver/sync_service.h" +#include "content/public/browser/render_process_host.h" +#include "content/public/browser/site_instance.h" + +#if BUILDFLAG(ENABLE_EXTENSIONS) +#include "extensions/browser/guest_view/web_view/web_view_guest.h" +#include "extensions/browser/guest_view/web_view/web_view_renderer_state.h" +#endif + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "components/account_manager_core/pref_names.h" +#endif + +namespace signin { + +#if defined(OS_ANDROID) +HeaderModificationDelegateImpl::HeaderModificationDelegateImpl( + Profile* profile, + bool incognito_enabled) + : profile_(profile), + cookie_settings_(CookieSettingsFactory::GetForProfile(profile_)), + incognito_enabled_(incognito_enabled) {} +#else +HeaderModificationDelegateImpl::HeaderModificationDelegateImpl(Profile* profile) + : profile_(profile), + cookie_settings_(CookieSettingsFactory::GetForProfile(profile_)) {} +#endif + +HeaderModificationDelegateImpl::~HeaderModificationDelegateImpl() = default; + +bool HeaderModificationDelegateImpl::ShouldInterceptNavigation( + content::WebContents* contents) { + if (profile_->IsOffTheRecord()) + return false; + +#if BUILDFLAG(ENABLE_EXTENSIONS) + if (ShouldIgnoreGuestWebViewRequest(contents)) + return false; +#endif + + return true; +} + +void HeaderModificationDelegateImpl::ProcessRequest( + ChromeRequestAdapter* request_adapter, + const GURL& redirect_url) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + const PrefService* prefs = profile_->GetPrefs(); +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + syncer::SyncService* sync_service = + SyncServiceFactory::GetForProfile(profile_); +#endif + +#if BUILDFLAG(IS_CHROMEOS_ASH) + bool is_secondary_account_addition_allowed = true; + if (!prefs->GetBoolean( + ::account_manager::prefs::kSecondaryGoogleAccountSigninAllowed)) { + is_secondary_account_addition_allowed = false; + } +#endif + + ConsentLevel consent_level = ConsentLevel::kSync; +#if defined(OS_ANDROID) + consent_level = ConsentLevel::kSignin; +#endif + + IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile_); + CoreAccountInfo account = + identity_manager->GetPrimaryAccountInfo(consent_level); + signin::Tribool is_child_account = + // Defaults to kUnknown if the account is not found. + identity_manager->FindExtendedAccountInfo(account).is_child_account; + + int incognito_mode_availability = + prefs->GetInteger(prefs::kIncognitoModeAvailability); +#if defined(OS_ANDROID) + incognito_mode_availability = + incognito_enabled_ + ? incognito_mode_availability + : static_cast(IncognitoModePrefs::Availability::kDisabled); +#endif + + FixAccountConsistencyRequestHeader( + request_adapter, redirect_url, profile_->IsOffTheRecord(), + incognito_mode_availability, + AccountConsistencyModeManager::GetMethodForProfile(profile_), + account.gaia, is_child_account, +#if BUILDFLAG(IS_CHROMEOS_ASH) + is_secondary_account_addition_allowed, +#endif +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + sync_service && sync_service->IsSyncFeatureEnabled(), + prefs->GetString(prefs::kGoogleServicesSigninScopedDeviceId), +#endif + cookie_settings_.get()); +} + +void HeaderModificationDelegateImpl::ProcessResponse( + ResponseAdapter* response_adapter, + const GURL& redirect_url) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + ProcessAccountConsistencyResponseHeaders(response_adapter, redirect_url, + profile_->IsOffTheRecord()); +} + +#if BUILDFLAG(ENABLE_EXTENSIONS) +// static +bool HeaderModificationDelegateImpl::ShouldIgnoreGuestWebViewRequest( + content::WebContents* contents) { + if (!contents) + return true; + + if (extensions::WebViewRendererState::GetInstance()->IsGuest( + contents->GetMainFrame()->GetProcess()->GetID())) { + auto identity_api_config = + extensions::WebAuthFlow::GetWebViewPartitionConfig( + extensions::WebAuthFlow::GET_AUTH_TOKEN, + contents->GetBrowserContext()); + if (contents->GetSiteInstance()->GetStoragePartitionConfig() != + identity_api_config) + return true; + + // If the StoragePartitionConfig matches, but |contents| is not using a + // guest SiteInstance, then there is likely a serious bug. + CHECK(contents->GetSiteInstance()->IsGuest()); + } + return false; +} +#endif + +} // namespace signin diff --git a/chromium/chrome/browser/signin/header_modification_delegate_impl.h b/chromium/chrome/browser/signin/header_modification_delegate_impl.h new file mode 100644 index 00000000000..8bc1fd7fb2d --- /dev/null +++ b/chromium/chrome/browser/signin/header_modification_delegate_impl.h @@ -0,0 +1,68 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_SIGNIN_HEADER_MODIFICATION_DELEGATE_IMPL_H_ +#define CHROME_BROWSER_SIGNIN_HEADER_MODIFICATION_DELEGATE_IMPL_H_ + +#include "base/memory/raw_ptr.h" +#include "build/build_config.h" +#include "build/buildflag.h" +#include "chrome/browser/signin/header_modification_delegate.h" +#include "components/content_settings/core/browser/cookie_settings.h" +#include "content/public/browser/browser_thread.h" +#include "extensions/buildflags/buildflags.h" + +class Profile; + +namespace signin { + +// This class wraps the FixAccountConsistencyRequestHeader and +// ProcessAccountConsistencyResponseHeaders in the HeaderModificationDelegate +// interface. +class HeaderModificationDelegateImpl : public HeaderModificationDelegate { + public: +#if defined(OS_ANDROID) + explicit HeaderModificationDelegateImpl(Profile* profile, + bool incognito_enabled); +#else + explicit HeaderModificationDelegateImpl(Profile* profile); +#endif + + HeaderModificationDelegateImpl(const HeaderModificationDelegateImpl&) = + delete; + HeaderModificationDelegateImpl& operator=( + const HeaderModificationDelegateImpl&) = delete; + + ~HeaderModificationDelegateImpl() override; + + // HeaderModificationDelegate + bool ShouldInterceptNavigation(content::WebContents* contents) override; + void ProcessRequest(ChromeRequestAdapter* request_adapter, + const GURL& redirect_url) override; + void ProcessResponse(ResponseAdapter* response_adapter, + const GURL& redirect_url) override; + +#if BUILDFLAG(ENABLE_EXTENSIONS) + // Returns true if the request comes from a web view and should be ignored + // (i.e. not intercepted). + // Returns false if the request does not come from a web view. + // Requests coming from most guest web views are ignored. In particular the + // requests coming from the InlineLoginUI are not intercepted (see + // http://crbug.com/428396). Requests coming from the chrome identity + // extension consent flow are not ignored. + static bool ShouldIgnoreGuestWebViewRequest(content::WebContents* contents); +#endif + + private: + raw_ptr profile_; + scoped_refptr cookie_settings_; + +#if defined(OS_ANDROID) + bool incognito_enabled_; +#endif +}; + +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_HEADER_MODIFICATION_DELEGATE_IMPL_H_ diff --git a/chromium/chrome/browser/signin/identity_manager_factory.cc b/chromium/chrome/browser/signin/identity_manager_factory.cc new file mode 100644 index 00000000000..cac771bf1e2 --- /dev/null +++ b/chromium/chrome/browser/signin/identity_manager_factory.cc @@ -0,0 +1,171 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/identity_manager_factory.h" + +#include +#include + +#include "base/files/file_path.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/image_fetcher/image_decoder_impl.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/signin/chrome_signin_client_factory.h" +#include "chrome/browser/signin/identity_manager_provider.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/keyed_service/core/keyed_service.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/signin/public/base/signin_buildflags.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_manager_builder.h" +#include "components/signin/public/webdata/token_web_data.h" +#include "content/public/browser/network_service_instance.h" + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +#include "chrome/browser/content_settings/cookie_settings_factory.h" +#include "chrome/browser/web_data_service_factory.h" +#include "components/content_settings/core/browser/cookie_settings.h" +#include "components/keyed_service/core/service_access_type.h" +#include "components/signin/core/browser/cookie_settings_util.h" +#endif + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "chrome/browser/ash/profiles/profile_helper.h" +#include "chrome/browser/browser_process_platform_part.h" +#include "components/account_manager_core/chromeos/account_manager_facade_factory.h" +#endif + +#if BUILDFLAG(IS_CHROMEOS_LACROS) +#include "chrome/browser/lacros/account_manager/profile_account_manager.h" +#include "chrome/browser/lacros/account_manager/profile_account_manager_factory.h" +#include "components/account_manager_core/chromeos/account_manager_facade_factory.h" +#endif + +#if defined(OS_WIN) +#include "base/bind.h" +#include "chrome/browser/signin/signin_util_win.h" +#endif + +void IdentityManagerFactory::RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) { + signin::IdentityManager::RegisterProfilePrefs(registry); +} + +IdentityManagerFactory::IdentityManagerFactory() + : BrowserContextKeyedServiceFactory( + "IdentityManager", + BrowserContextDependencyManager::GetInstance()) { +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + DependsOn(WebDataServiceFactory::GetInstance()); +#endif +#if BUILDFLAG(IS_CHROMEOS_LACROS) + DependsOn(ProfileAccountManagerFactory::GetInstance()); +#endif + DependsOn(ChromeSigninClientFactory::GetInstance()); + signin::SetIdentityManagerProvider( + base::BindRepeating([](content::BrowserContext* context) { + return GetForProfile(Profile::FromBrowserContext(context)); + })); +} + +IdentityManagerFactory::~IdentityManagerFactory() { + signin::SetIdentityManagerProvider({}); +} + +// static +signin::IdentityManager* IdentityManagerFactory::GetForProfile( + Profile* profile) { + return static_cast( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +// static +signin::IdentityManager* IdentityManagerFactory::GetForProfileIfExists( + const Profile* profile) { + return static_cast( + GetInstance()->GetServiceForBrowserContext(const_cast(profile), + false)); +} + +// static +IdentityManagerFactory* IdentityManagerFactory::GetInstance() { + return base::Singleton::get(); +} + +// static +void IdentityManagerFactory::EnsureFactoryAndDependeeFactoriesBuilt() { + IdentityManagerFactory::GetInstance(); + ChromeSigninClientFactory::GetInstance(); +} + +void IdentityManagerFactory::AddObserver(Observer* observer) { + observer_list_.AddObserver(observer); +} + +void IdentityManagerFactory::RemoveObserver(Observer* observer) { + observer_list_.RemoveObserver(observer); +} + +KeyedService* IdentityManagerFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + + signin::IdentityManagerBuildParams params; + params.account_consistency = + AccountConsistencyModeManager::GetMethodForProfile(profile), + params.image_decoder = std::make_unique(); + params.local_state = g_browser_process->local_state(); + params.network_connection_tracker = content::GetNetworkConnectionTracker(); + params.pref_service = profile->GetPrefs(); + params.profile_path = profile->GetPath(); + params.signin_client = ChromeSigninClientFactory::GetForProfile(profile); + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + params.delete_signin_cookies_on_exit = + signin::SettingsDeleteSigninCookiesOnExit( + CookieSettingsFactory::GetForProfile(profile).get()); + params.token_web_data = WebDataServiceFactory::GetTokenWebDataForProfile( + profile, ServiceAccessType::EXPLICIT_ACCESS); +#endif + +#if BUILDFLAG(IS_CHROMEOS_ASH) + params.account_manager_facade = + GetAccountManagerFacade(profile->GetPath().value()); + params.is_regular_profile = + chromeos::ProfileHelper::IsRegularProfile(profile); +#endif + +#if BUILDFLAG(IS_CHROMEOS_LACROS) + // The system and (original profile of the) guest profiles are not regular. + const bool is_regular_profile = profile->IsRegularProfile(); + const bool use_profile_account_manager = + is_regular_profile && + // `ProfileManager` may be null in tests, and is required for account + // consistency. + g_browser_process->profile_manager(); + + params.account_manager_facade = + use_profile_account_manager + ? ProfileAccountManagerFactory::GetForProfile(profile) + : GetAccountManagerFacade(profile->GetPath().value()); + params.is_regular_profile = is_regular_profile; +#endif + +#if defined(OS_WIN) + params.reauth_callback = + base::BindRepeating(&signin_util::ReauthWithCredentialProviderIfPossible, + base::Unretained(profile)); +#endif + + std::unique_ptr identity_manager = + signin::BuildIdentityManager(¶ms); + + for (Observer& observer : observer_list_) + observer.IdentityManagerCreated(identity_manager.get()); + + return identity_manager.release(); +} diff --git a/chromium/chrome/browser/signin/identity_manager_factory.h b/chromium/chrome/browser/signin/identity_manager_factory.h new file mode 100644 index 00000000000..a2e0590da42 --- /dev/null +++ b/chromium/chrome/browser/signin/identity_manager_factory.h @@ -0,0 +1,67 @@ +// Copyright 2017 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 CHROME_BROWSER_SIGNIN_IDENTITY_MANAGER_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_IDENTITY_MANAGER_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "base/observer_list.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +namespace signin { +class IdentityManager; +} + +class Profile; + +// Singleton that owns all IdentityManager instances and associates them with +// Profiles. +class IdentityManagerFactory : public BrowserContextKeyedServiceFactory { + public: + class Observer : public base::CheckedObserver { + public: + // Called when a IdentityManager instance is created. + virtual void IdentityManagerCreated( + signin::IdentityManager* identity_manager) {} + + protected: + ~Observer() override {} + }; + + static signin::IdentityManager* GetForProfile(Profile* profile); + static signin::IdentityManager* GetForProfileIfExists(const Profile* profile); + + // Returns an instance of the IdentityManagerFactory singleton. + static IdentityManagerFactory* GetInstance(); + + IdentityManagerFactory(const IdentityManagerFactory&) = delete; + IdentityManagerFactory& operator=(const IdentityManagerFactory&) = delete; + + // Ensures that IdentityManagerFactory and the factories on which it depends + // are built. + static void EnsureFactoryAndDependeeFactoriesBuilt(); + + // Methods to register or remove observers of IdentityManager + // creation/shutdown. + void AddObserver(Observer* observer); + void RemoveObserver(Observer* observer); + + private: + friend struct base::DefaultSingletonTraits; + + IdentityManagerFactory(); + ~IdentityManagerFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const override; + void RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) override; + + // List of observers. Checks that list is empty on destruction. + base::ObserverList + observer_list_; +}; + +#endif // CHROME_BROWSER_SIGNIN_IDENTITY_MANAGER_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/identity_manager_provider.cc b/chromium/chrome/browser/signin/identity_manager_provider.cc new file mode 100644 index 00000000000..ca42e587adf --- /dev/null +++ b/chromium/chrome/browser/signin/identity_manager_provider.cc @@ -0,0 +1,38 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/identity_manager_provider.h" + +#include "base/check.h" +#include "base/no_destructor.h" + +namespace signin { + +namespace { + +IdentityManagerProvider& GetIdentityManagerProvider() { + static base::NoDestructor provider; + return *provider; +} + +} // namespace + +void SetIdentityManagerProvider(const IdentityManagerProvider& provider) { + IdentityManagerProvider& instance = GetIdentityManagerProvider(); + + // Exactly one of `provider` or `instance` should be non-null. + if (provider) + DCHECK(!instance); + else + DCHECK(instance); + + instance = provider; +} + +IdentityManager* GetIdentityManagerForBrowserContext( + content::BrowserContext* context) { + return GetIdentityManagerProvider().Run(context); +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/identity_manager_provider.h b/chromium/chrome/browser/signin/identity_manager_provider.h new file mode 100644 index 00000000000..9b6291d643b --- /dev/null +++ b/chromium/chrome/browser/signin/identity_manager_provider.h @@ -0,0 +1,32 @@ +// Copyright 2021 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 CHROME_BROWSER_SIGNIN_IDENTITY_MANAGER_PROVIDER_H_ +#define CHROME_BROWSER_SIGNIN_IDENTITY_MANAGER_PROVIDER_H_ + +#include "base/callback.h" + +namespace content { +class BrowserContext; +} + +namespace signin { + +class IdentityManager; + +using IdentityManagerProvider = + base::RepeatingCallback; + +// Called by IdentityManagerFactory to expose a way to retrieve the +// IdentityManager for a specific BrowserContext/Profile. This exists so that +// components which don't depend on //chrome/browser can still access the +// IdentityManager. +void SetIdentityManagerProvider(const IdentityManagerProvider& provider); + +IdentityManager* GetIdentityManagerForBrowserContext( + content::BrowserContext* context); + +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_IDENTITY_MANAGER_PROVIDER_H_ diff --git a/chromium/chrome/browser/signin/identity_services_provider_android.cc b/chromium/chrome/browser/signin/identity_services_provider_android.cc new file mode 100644 index 00000000000..cb49ede8c7d --- /dev/null +++ b/chromium/chrome/browser/signin/identity_services_provider_android.cc @@ -0,0 +1,39 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/android/jni_android.h" +#include "chrome/browser/profiles/profile_android.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/services/android/jni_headers/IdentityServicesProvider_jni.h" +#include "chrome/browser/signin/signin_manager_android_factory.h" +#include "components/signin/public/identity_manager/identity_manager.h" + +using base::android::JavaParamRef; +using base::android::ScopedJavaLocalRef; + +static ScopedJavaLocalRef +JNI_IdentityServicesProvider_GetIdentityManager( + JNIEnv* env, + const JavaParamRef& j_profile_android) { + Profile* profile = ProfileAndroid::FromProfileAndroid(j_profile_android); + return IdentityManagerFactory::GetForProfile(profile)->GetJavaObject(); +} + +static ScopedJavaLocalRef +JNI_IdentityServicesProvider_GetAccountTrackerService( + JNIEnv* env, + const JavaParamRef& j_profile_android) { + Profile* profile = ProfileAndroid::FromProfileAndroid(j_profile_android); + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile); + return identity_manager->LegacyGetAccountTrackerServiceJavaObject(); +} + +static ScopedJavaLocalRef +JNI_IdentityServicesProvider_GetSigninManager( + JNIEnv* env, + const JavaParamRef& j_profile_android) { + Profile* profile = ProfileAndroid::FromProfileAndroid(j_profile_android); + return SigninManagerAndroidFactory::GetJavaObjectForProfile(profile); +} diff --git a/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.cc b/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.cc new file mode 100644 index 00000000000..ff94a971878 --- /dev/null +++ b/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.cc @@ -0,0 +1,103 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h" + +#include "base/bind.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/signin/chrome_signin_client_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "ash/components/account_manager/account_manager_factory.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/browser_process_platform_part.h" +#include "components/account_manager_core/chromeos/account_manager_facade_factory.h" +#endif + +// static +std::unique_ptr IdentityTestEnvironmentProfileAdaptor:: + CreateProfileForIdentityTestEnvironment() { + return CreateProfileForIdentityTestEnvironment( + TestingProfile::TestingFactories()); +} + +// static +std::unique_ptr +IdentityTestEnvironmentProfileAdaptor::CreateProfileForIdentityTestEnvironment( + const TestingProfile::TestingFactories& input_factories) { + TestingProfile::Builder builder; + + for (auto& input_factory : input_factories) { + builder.AddTestingFactory(input_factory.first, input_factory.second); + } + + return CreateProfileForIdentityTestEnvironment(builder); +} + +// static +std::unique_ptr +IdentityTestEnvironmentProfileAdaptor::CreateProfileForIdentityTestEnvironment( + TestingProfile::Builder& builder, + signin::AccountConsistencyMethod account_consistency) { + for (auto& identity_factory : + GetIdentityTestEnvironmentFactories(account_consistency)) { + builder.AddTestingFactory(identity_factory.first, identity_factory.second); + } + + return builder.Build(); +} + +// static +void IdentityTestEnvironmentProfileAdaptor:: + SetIdentityTestEnvironmentFactoriesOnBrowserContext( + content::BrowserContext* context) { + for (const auto& factory_pair : GetIdentityTestEnvironmentFactories()) { + factory_pair.first->SetTestingFactory(context, factory_pair.second); + } +} + +// static +void IdentityTestEnvironmentProfileAdaptor:: + AppendIdentityTestEnvironmentFactories( + TestingProfile::TestingFactories* factories_to_append_to) { + TestingProfile::TestingFactories identity_factories = + GetIdentityTestEnvironmentFactories(); + factories_to_append_to->insert(factories_to_append_to->end(), + identity_factories.begin(), + identity_factories.end()); +} + +// static +TestingProfile::TestingFactories +IdentityTestEnvironmentProfileAdaptor::GetIdentityTestEnvironmentFactories( + signin::AccountConsistencyMethod account_consistency) { + return {{IdentityManagerFactory::GetInstance(), + base::BindRepeating(&BuildIdentityManagerForTests, + account_consistency)}}; +} + +// static +std::unique_ptr +IdentityTestEnvironmentProfileAdaptor::BuildIdentityManagerForTests( + signin::AccountConsistencyMethod account_consistency, + content::BrowserContext* context) { + Profile* profile = Profile::FromBrowserContext(context); +#if BUILDFLAG(IS_CHROMEOS_ASH) + return signin::IdentityTestEnvironment::BuildIdentityManagerForTests( + ChromeSigninClientFactory::GetForProfile(profile), profile->GetPrefs(), + profile->GetPath(), + g_browser_process->platform_part()->GetAccountManagerFactory(), + GetAccountManagerFacade(profile->GetPath().value())); +#else + return signin::IdentityTestEnvironment::BuildIdentityManagerForTests( + ChromeSigninClientFactory::GetForProfile(profile), profile->GetPrefs(), + profile->GetPath(), account_consistency); +#endif +} + +IdentityTestEnvironmentProfileAdaptor::IdentityTestEnvironmentProfileAdaptor( + Profile* profile) + : identity_test_env_(IdentityManagerFactory::GetForProfile(profile), + ChromeSigninClientFactory::GetForProfile(profile)) {} diff --git a/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.h b/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.h new file mode 100644 index 00000000000..ef477efd9d3 --- /dev/null +++ b/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.h @@ -0,0 +1,101 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_SIGNIN_IDENTITY_TEST_ENVIRONMENT_PROFILE_ADAPTOR_H_ +#define CHROME_BROWSER_SIGNIN_IDENTITY_TEST_ENVIRONMENT_PROFILE_ADAPTOR_H_ + +#include "chrome/test/base/testing_profile.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "components/sync_preferences/testing_pref_service_syncable.h" + +// Adaptor that supports signin::IdentityTestEnvironment's usage in testing +// contexts where the relevant fake objects must be injected via the +// BrowserContextKeyedServiceFactory infrastructure as the production code +// accesses IdentityManager via that infrastructure. Before using this +// class, please consider whether you can change the production code in question +// to take in the relevant dependencies directly rather than obtaining them from +// the Profile; this is both cleaner in general and allows for direct usage of +// signin::IdentityTestEnvironment in the test. +class IdentityTestEnvironmentProfileAdaptor { + public: + // Creates and returns a TestingProfile that has been configured with the set + // of testing factories that IdentityTestEnvironment requires. + static std::unique_ptr + CreateProfileForIdentityTestEnvironment(); + + // Like the above, but additionally configures the returned Profile with + // |input_factories|. + static std::unique_ptr + CreateProfileForIdentityTestEnvironment( + const TestingProfile::TestingFactories& input_factories); + + // Creates and returns a TestingProfile that has been configured with the + // given |builder| and the set of testing factories that + // IdentityTestEnvironment requires. + // See the above variant for comments on common parameters. + static std::unique_ptr + CreateProfileForIdentityTestEnvironment( + TestingProfile::Builder& builder, + signin::AccountConsistencyMethod account_consistency = + signin::AccountConsistencyMethod::kDisabled); + + // Sets the testing factories that signin::IdentityTestEnvironment + // requires explicitly on a Profile that is passed to it. + // See the above variant for comments on common parameters. + static void SetIdentityTestEnvironmentFactoriesOnBrowserContext( + content::BrowserContext* browser_context); + + // Appends the set of testing factories that signin::IdentityTestEnvironment + // requires to |factories_to_append_to|, which should be the set of testing + // factories supplied to TestingProfile (via one of the various mechanisms for + // doing so). Prefer the above API if possible, as it is less fragile. This + // API is primarily for use in tests that do not create the TestingProfile + // internally but rather simply supply the set of TestingFactories to some + // external facility (e.g., a superclass). + // See CreateProfileForIdentityTestEnvironment() for comments on common + // parameters. + static void AppendIdentityTestEnvironmentFactories( + TestingProfile::TestingFactories* factories_to_append_to); + + // Returns the set of testing factories that signin::IdentityTestEnvironment + // requires, which can be useful to configure profiles for services that do + // not require any other testing factory than the ones specified in here. + static TestingProfile::TestingFactories GetIdentityTestEnvironmentFactories( + signin::AccountConsistencyMethod account_consistency = + signin::AccountConsistencyMethod::kDisabled); + + // Constructs an adaptor that associates an IdentityTestEnvironment instance + // with |profile| via the relevant backing objects. Note that + // |profile| must have been configured with the IdentityTestEnvironment + // testing factories, either because it was created via + // CreateProfileForIdentityTestEnvironment() or because + // AppendIdentityTestEnvironmentFactories() was invoked on the set of + // factories supplied to it. + // |profile| must outlive this object. + explicit IdentityTestEnvironmentProfileAdaptor(Profile* profile); + + IdentityTestEnvironmentProfileAdaptor( + const IdentityTestEnvironmentProfileAdaptor&) = delete; + IdentityTestEnvironmentProfileAdaptor& operator=( + const IdentityTestEnvironmentProfileAdaptor&) = delete; + + ~IdentityTestEnvironmentProfileAdaptor() {} + + // Returns the IdentityTestEnvironment associated with this object (and + // implicitly with the Profile passed to this object's constructor). + signin::IdentityTestEnvironment* identity_test_env() { + return &identity_test_env_; + } + + private: + // Testing factory that creates an IdentityManager + // with a FakeProfileOAuth2TokenService. + static std::unique_ptr BuildIdentityManagerForTests( + signin::AccountConsistencyMethod account_consistency, + content::BrowserContext* context); + + signin::IdentityTestEnvironment identity_test_env_; +}; + +#endif // CHROME_BROWSER_SIGNIN_IDENTITY_TEST_ENVIRONMENT_PROFILE_ADAPTOR_H_ diff --git a/chromium/chrome/browser/signin/investigator_dependency_provider.cc b/chromium/chrome/browser/signin/investigator_dependency_provider.cc new file mode 100644 index 00000000000..601806d2669 --- /dev/null +++ b/chromium/chrome/browser/signin/investigator_dependency_provider.cc @@ -0,0 +1,14 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/investigator_dependency_provider.h" + +InvestigatorDependencyProvider::InvestigatorDependencyProvider(Profile* profile) + : profile_(profile) {} + +InvestigatorDependencyProvider::~InvestigatorDependencyProvider() {} + +PrefService* InvestigatorDependencyProvider::GetPrefs() { + return profile_->GetPrefs(); +} diff --git a/chromium/chrome/browser/signin/investigator_dependency_provider.h b/chromium/chrome/browser/signin/investigator_dependency_provider.h new file mode 100644 index 00000000000..199f7bb07a5 --- /dev/null +++ b/chromium/chrome/browser/signin/investigator_dependency_provider.h @@ -0,0 +1,33 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_SIGNIN_INVESTIGATOR_DEPENDENCY_PROVIDER_H_ +#define CHROME_BROWSER_SIGNIN_INVESTIGATOR_DEPENDENCY_PROVIDER_H_ + +#include "base/memory/raw_ptr.h" +#include "chrome/browser/profiles/profile.h" +#include "components/prefs/pref_service.h" +#include "components/signin/core/browser/signin_investigator.h" + +// This version should work for anything with a profile object, like desktop and +// Android. +class InvestigatorDependencyProvider + : public SigninInvestigator::DependencyProvider { + public: + explicit InvestigatorDependencyProvider(Profile* profile); + + InvestigatorDependencyProvider(const InvestigatorDependencyProvider&) = + delete; + InvestigatorDependencyProvider& operator=( + const InvestigatorDependencyProvider&) = delete; + + ~InvestigatorDependencyProvider() override; + PrefService* GetPrefs() override; + + private: + // Non-owning pointer. + raw_ptr profile_; +}; + +#endif // CHROME_BROWSER_SIGNIN_INVESTIGATOR_DEPENDENCY_PROVIDER_H_ diff --git a/chromium/chrome/browser/signin/logout_tab_helper.cc b/chromium/chrome/browser/signin/logout_tab_helper.cc new file mode 100644 index 00000000000..db560b21a69 --- /dev/null +++ b/chromium/chrome/browser/signin/logout_tab_helper.cc @@ -0,0 +1,34 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/logout_tab_helper.h" + +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "components/signin/public/base/signin_metrics.h" +#include "components/signin/public/identity_manager/accounts_mutator.h" +#include "components/signin/public/identity_manager/identity_manager.h" + +WEB_CONTENTS_USER_DATA_KEY_IMPL(LogoutTabHelper); + +LogoutTabHelper::LogoutTabHelper(content::WebContents* web_contents) + : content::WebContentsUserData(*web_contents), + content::WebContentsObserver(web_contents) {} + +LogoutTabHelper::~LogoutTabHelper() = default; + +void LogoutTabHelper::PrimaryPageChanged(content::Page& page) { + if (page.GetMainDocument().IsErrorDocument()) { + // Failed to load the logout page, fallback to local signout. + Profile* profile = + Profile::FromBrowserContext(web_contents()->GetBrowserContext()); + IdentityManagerFactory::GetForProfile(profile) + ->GetAccountsMutator() + ->RemoveAllAccounts(signin_metrics::SourceForRefreshTokenOperation:: + kLogoutTabHelper_PrimaryPageChanged); + } + + // Delete this. + web_contents()->RemoveUserData(UserDataKey()); +} diff --git a/chromium/chrome/browser/signin/logout_tab_helper.h b/chromium/chrome/browser/signin/logout_tab_helper.h new file mode 100644 index 00000000000..4f2974c4568 --- /dev/null +++ b/chromium/chrome/browser/signin/logout_tab_helper.h @@ -0,0 +1,36 @@ +// Copyright 2020 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 CHROME_BROWSER_SIGNIN_LOGOUT_TAB_HELPER_H_ +#define CHROME_BROWSER_SIGNIN_LOGOUT_TAB_HELPER_H_ + +#include "content/public/browser/web_contents_observer.h" +#include "content/public/browser/web_contents_user_data.h" + +// Tab helper used for logout tabs. Monitors if the logout tab loaded correctly +// and fallbacks to local signout in case of failure. +// Only the first navigation is monitored. Even though the logout page sometimes +// redirects to the SAML provider through javascript, that second navigation is +// not monitored. The logout is considered successful if the first navigation +// succeeds, because the signout headers which cause the tokens to be revoked +// are there. +class LogoutTabHelper : public content::WebContentsUserData, + public content::WebContentsObserver { + public: + ~LogoutTabHelper() override; + + LogoutTabHelper(const LogoutTabHelper&) = delete; + LogoutTabHelper& operator=(const LogoutTabHelper&) = delete; + + private: + friend class content::WebContentsUserData; + explicit LogoutTabHelper(content::WebContents* web_contents); + + // content::WebContentsObserver: + void PrimaryPageChanged(content::Page& page) override; + + WEB_CONTENTS_USER_DATA_KEY_DECL(); +}; + +#endif // CHROME_BROWSER_SIGNIN_LOGOUT_TAB_HELPER_H_ diff --git a/chromium/chrome/browser/signin/logout_tab_helper_unittest.cc b/chromium/chrome/browser/signin/logout_tab_helper_unittest.cc new file mode 100644 index 00000000000..7717ade215c --- /dev/null +++ b/chromium/chrome/browser/signin/logout_tab_helper_unittest.cc @@ -0,0 +1,24 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/logout_tab_helper.h" + +#include "chrome/test/base/chrome_render_view_host_test_harness.h" +#include "content/public/test/navigation_simulator.h" +#include "google_apis/gaia/gaia_urls.h" + +class LogoutTabHelperTest : public ChromeRenderViewHostTestHarness {}; + +TEST_F(LogoutTabHelperTest, SelfDeleteInPrimaryPageChanged) { + LogoutTabHelper::CreateForWebContents(web_contents()); + + EXPECT_NE(nullptr, LogoutTabHelper::FromWebContents(web_contents())); + + // Load the logout page. + content::NavigationSimulator::NavigateAndCommitFromBrowser( + web_contents(), GaiaUrls::GetInstance()->service_logout_url()); + + // The helper was deleted in PrimaryPageChanged. + EXPECT_EQ(nullptr, LogoutTabHelper::FromWebContents(web_contents())); +} diff --git a/chromium/chrome/browser/signin/mirror_browsertest.cc b/chromium/chrome/browser/signin/mirror_browsertest.cc new file mode 100644 index 00000000000..abc65d011e6 --- /dev/null +++ b/chromium/chrome/browser/signin/mirror_browsertest.cc @@ -0,0 +1,277 @@ +// Copyright 2019 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 +#include +#include +#include + +#include "base/base_switches.h" +#include "base/bind.h" +#include "base/callback.h" +#include "base/callback_helpers.h" +#include "base/command_line.h" +#include "base/path_service.h" +#include "base/run_loop.h" +#include "base/task/post_task.h" +#include "base/test/bind.h" +#include "build/build_config.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/chrome_content_browser_client.h" +#include "chrome/browser/extensions/api/identity/web_auth_flow.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/signin_util.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/common/chrome_paths.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/ui_test_utils.h" +#include "components/google/core/common/google_switches.h" +#include "components/network_session_configurator/common/network_switches.h" +#include "components/prefs/pref_service.h" +#include "components/signin/core/browser/dice_header_helper.h" +#include "components/signin/core/browser/signin_header_helper.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "content/public/common/content_client.h" +#include "content/public/test/browser_test.h" +#include "google_apis/gaia/gaia_urls.h" +#include "net/dns/mock_host_resolver.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "net/test/embedded_test_server/http_request.h" +#include "net/test/embedded_test_server/request_handler_util.h" +#include "third_party/blink/public/common/loader/url_loader_throttle.h" + +namespace { + +// A delegate to insert a user generated X-Chrome-Connected header +// to a specifict URL. +class HeaderModifyingThrottle : public blink::URLLoaderThrottle { + public: + HeaderModifyingThrottle() = default; + + HeaderModifyingThrottle(const HeaderModifyingThrottle&) = delete; + HeaderModifyingThrottle& operator=(const HeaderModifyingThrottle&) = delete; + + ~HeaderModifyingThrottle() override = default; + + void WillStartRequest(network::ResourceRequest* request, + bool* defer) override { + request->headers.SetHeader(signin::kChromeConnectedHeader, "User Data"); + } +}; + +class ThrottleContentBrowserClient : public ChromeContentBrowserClient { + public: + explicit ThrottleContentBrowserClient(const GURL& watch_url) + : watch_url_(watch_url) {} + + ThrottleContentBrowserClient(const ThrottleContentBrowserClient&) = delete; + ThrottleContentBrowserClient& operator=(const ThrottleContentBrowserClient&) = + delete; + + ~ThrottleContentBrowserClient() override = default; + + // ContentBrowserClient overrides: + std::vector> + CreateURLLoaderThrottles( + const network::ResourceRequest& request, + content::BrowserContext* browser_context, + const base::RepeatingCallback& wc_getter, + content::NavigationUIData* navigation_ui_data, + int frame_tree_node_id) override { + std::vector> throttles; + if (request.url == watch_url_) + throttles.push_back(std::make_unique()); + return throttles; + } + + private: + const GURL watch_url_; +}; + +// Subclass of DiceManageAccountBrowserTest with Mirror enabled. +class MirrorBrowserTest : public InProcessBrowserTest { + protected: + void RunExtensionConsentTest(extensions::WebAuthFlow::Partition partition, + bool expects_header) { + net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS); + https_server.AddDefaultHandlers(GetChromeTestDataDir()); + const std::string kAuthPath = "/auth"; + net::test_server::HttpRequest::HeaderMap headers; + base::RunLoop run_loop; + https_server.RegisterRequestMonitor(base::BindLambdaForTesting( + [&](const net::test_server::HttpRequest& request) { + if (request.GetURL().path() != kAuthPath) + return; + + headers = request.headers; + run_loop.Quit(); + })); + ASSERT_TRUE(https_server.Start()); + + auto web_auth_flow = std::make_unique( + nullptr, browser()->profile(), + https_server.GetURL("google.com", kAuthPath), + extensions::WebAuthFlow::INTERACTIVE, partition); + + web_auth_flow->Start(); + run_loop.Run(); + EXPECT_EQ(!!headers.count(signin::kChromeConnectedHeader), expects_header); + + web_auth_flow.release()->DetachDelegateAndDelete(); + base::RunLoop().RunUntilIdle(); + } + + private: + void SetUpOnMainThread() override { + // The test makes requests to google.com and other domains which we want to + // redirect to the test server. + host_resolver()->AddRule("*", "127.0.0.1"); + } + + void SetUpCommandLine(base::CommandLine* command_line) override { + // HTTPS server only serves a valid cert for localhost, so this is needed to + // load pages from "www.google.com" without an interstitial. + command_line->AppendSwitch(switches::kIgnoreCertificateErrors); + + // The production code only allows known ports (80 for http and 443 for + // https), but the test server runs on a random port. + command_line->AppendSwitch(switches::kIgnoreGooglePortNumbers); + } +}; + +// Verify the following items: +// 1- X-Chrome-Connected is appended on Google domains if account +// consistency is enabled and access is secure. +// 2- The header is stripped in case a request is redirected from a Gooogle +// domain to non-google domain. +// 3- The header is NOT stripped in case it is added directly by the page +// and not because it was on a secure Google domain. +// This is a regression test for crbug.com/588492. +IN_PROC_BROWSER_TEST_F(MirrorBrowserTest, MirrorRequestHeader) { + browser()->profile()->GetPrefs()->SetString(prefs::kGoogleServicesAccountId, + "account_id"); + + base::Lock lock; + // Map from the path of the URLs that test server sees to the request header. + // This is the path, and not URL, because the requests use different domains + // which the mock HostResolver converts to 127.0.0.1. + std::map header_map; + embedded_test_server()->RegisterRequestMonitor(base::BindLambdaForTesting( + [&](const net::test_server::HttpRequest& request) { + base::AutoLock auto_lock(lock); + header_map[request.GetURL().path()] = request.headers; + })); + ASSERT_TRUE(embedded_test_server()->Start()); + + net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS); + https_server.AddDefaultHandlers(GetChromeTestDataDir()); + https_server.RegisterRequestMonitor(base::BindLambdaForTesting( + [&](const net::test_server::HttpRequest& request) { + base::AutoLock auto_lock(lock); + header_map[request.GetURL().path()] = request.headers; + })); + ASSERT_TRUE(https_server.Start()); + + base::FilePath root_http; + base::PathService::Get(chrome::DIR_TEST_DATA, &root_http); + root_http = root_http.AppendASCII("mirror_request_header"); + + struct TestCase { + GURL original_url; // The URL from which the request begins. + // The path to which navigation is redirected. + std::string redirected_to_path; + bool inject_header; // Should X-Chrome-Connected header be injected to the + // original request. + bool original_url_expects_header; // Expectation: The header should be + // visible in original URL. + bool redirected_to_url_expects_header; // Expectation: The header should be + // visible in redirected URL. + }; + + std::vector all_tests; + + // Neither should have the header. + // Note we need to replace the port of the redirect's URL. + base::StringPairs replacement_text; + replacement_text.push_back(std::make_pair( + "{{PORT}}", base::NumberToString(embedded_test_server()->port()))); + std::string replacement_path = net::test_server::GetFilePathWithReplacements( + "/mirror_request_header/http.www.google.com.html", replacement_text); + all_tests.push_back( + {embedded_test_server()->GetURL("www.google.com", replacement_path), + "/simple.html", false, false, false}); + + // First one adds the header and transfers it to the second. + replacement_path = net::test_server::GetFilePathWithReplacements( + "/mirror_request_header/http.www.header_adder.com.html", + replacement_text); + all_tests.push_back( + {embedded_test_server()->GetURL("www.header_adder.com", replacement_path), + "/simple.html", true, true, true}); + + // First one should have the header, but not transfered to second one. + replacement_text.clear(); + replacement_text.push_back( + std::make_pair("{{PORT}}", base::NumberToString(https_server.port()))); + replacement_path = net::test_server::GetFilePathWithReplacements( + "/mirror_request_header/https.www.google.com.html", replacement_text); + all_tests.push_back({https_server.GetURL("www.google.com", replacement_path), + "/simple.html", false, true, false}); + + for (const auto& test_case : all_tests) { + SCOPED_TRACE(test_case.original_url); + + // If test case requires adding header for the first url add a throttle. + ThrottleContentBrowserClient browser_client(test_case.original_url); + content::ContentBrowserClient* old_browser_client = nullptr; + if (test_case.inject_header) + old_browser_client = content::SetBrowserClientForTesting(&browser_client); + + // Navigate to first url. + ASSERT_TRUE( + ui_test_utils::NavigateToURL(browser(), test_case.original_url)); + + if (test_case.inject_header) + content::SetBrowserClientForTesting(old_browser_client); + + base::AutoLock auto_lock(lock); + + // Check if header exists and X-Chrome-Connected is correctly provided. + ASSERT_EQ(1u, header_map.count(test_case.original_url.path())); + if (test_case.original_url_expects_header) { + ASSERT_TRUE(header_map[test_case.original_url.path()].count( + signin::kChromeConnectedHeader)); + } else { + ASSERT_FALSE(header_map[test_case.original_url.path()].count( + signin::kChromeConnectedHeader)); + } + + ASSERT_EQ(1u, header_map.count(test_case.redirected_to_path)); + if (test_case.redirected_to_url_expects_header) { + ASSERT_TRUE(header_map[test_case.redirected_to_path].count( + signin::kChromeConnectedHeader)); + } else { + ASSERT_FALSE(header_map[test_case.redirected_to_path].count( + signin::kChromeConnectedHeader)); + } + + header_map.clear(); + } +} + +// Verifies that requests originated from chrome.identity.launchWebAuthFlow() +// API don't have Mirror headers attached. +// This is a regression test for crbug.com/1077504. +IN_PROC_BROWSER_TEST_F(MirrorBrowserTest, + NoMirrorExtensionConsent_LaunchWebAuthFlow) { + RunExtensionConsentTest(extensions::WebAuthFlow::LAUNCH_WEB_AUTH_FLOW, false); +} + +// Verifies that requests originated from chrome.identity.getAuthToken() +// API have Mirror headers attached. +IN_PROC_BROWSER_TEST_F(MirrorBrowserTest, MirrorExtensionConsent_GetAuthToken) { + RunExtensionConsentTest(extensions::WebAuthFlow::GET_AUTH_TOKEN, true); +} + +} // namespace diff --git a/chromium/chrome/browser/signin/process_dice_header_delegate_impl.cc b/chromium/chrome/browser/signin/process_dice_header_delegate_impl.cc new file mode 100644 index 00000000000..5669e179b48 --- /dev/null +++ b/chromium/chrome/browser/signin/process_dice_header_delegate_impl.cc @@ -0,0 +1,146 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/process_dice_header_delegate_impl.h" + +#include + +#include "base/callback.h" +#include "base/logging.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/dice_tab_helper.h" +#include "chrome/browser/signin/dice_web_signin_interceptor.h" +#include "chrome/browser/signin/dice_web_signin_interceptor_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/ui/webui/signin/signin_ui_error.h" +#include "chrome/common/url_constants.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "content/public/browser/navigation_controller.h" +#include "content/public/browser/web_contents.h" +#include "url/gurl.h" + +namespace { + +void RedirectToNtp(content::WebContents* contents) { + VLOG(1) << "RedirectToNtp"; + contents->GetController().LoadURL( + GURL(chrome::kChromeUINewTabURL), content::Referrer(), + ui::PAGE_TRANSITION_AUTO_TOPLEVEL, std::string()); +} + +// Helper function similar to DiceTabHelper::FromWebContents(), but also handles +// the case where |contents| is nullptr. +DiceTabHelper* GetDiceTabHelperFromWebContents(content::WebContents* contents) { + if (!contents) + return nullptr; + return DiceTabHelper::FromWebContents(contents); +} + +} // namespace + +ProcessDiceHeaderDelegateImpl::ProcessDiceHeaderDelegateImpl( + content::WebContents* web_contents, + EnableSyncCallback enable_sync_callback, + ShowSigninErrorCallback show_signin_error_callback) + : web_contents_(web_contents->GetWeakPtr()), + profile_(Profile::FromBrowserContext(web_contents->GetBrowserContext())), + enable_sync_callback_(std::move(enable_sync_callback)), + show_signin_error_callback_(std::move(show_signin_error_callback)) { + DCHECK(profile_); + + DiceTabHelper* tab_helper = DiceTabHelper::FromWebContents(web_contents); + if (tab_helper) { + is_sync_signin_tab_ = tab_helper->IsSyncSigninInProgress(); + redirect_url_ = tab_helper->redirect_url(); + access_point_ = tab_helper->signin_access_point(); + promo_action_ = tab_helper->signin_promo_action(); + reason_ = tab_helper->signin_reason(); + } +} + +ProcessDiceHeaderDelegateImpl::~ProcessDiceHeaderDelegateImpl() = default; + +bool ProcessDiceHeaderDelegateImpl::ShouldEnableSync() { + if (IdentityManagerFactory::GetForProfile(profile_)->HasPrimaryAccount( + signin::ConsentLevel::kSync)) { + VLOG(1) << "Do not start sync after web sign-in [already authenticated]."; + return false; + } + + if (!is_sync_signin_tab_) { + VLOG(1) + << "Do not start sync after web sign-in [not a Chrome sign-in tab]."; + return false; + } + + return true; +} + +void ProcessDiceHeaderDelegateImpl::HandleTokenExchangeSuccess( + CoreAccountId account_id, + bool is_new_account) { + // is_sync_signin_tab_ tells whether the current signin is happening in a tab + // that was opened from a "Enable Sync" Chrome UI. Usually this is indeed a + // sync signin, but it is not always the case: the user may abandon the sync + // signin and do a simple web signin in the same tab instead. + DiceWebSigninInterceptorFactory::GetForProfile(profile_) + ->MaybeInterceptWebSignin(web_contents_.get(), account_id, is_new_account, + is_sync_signin_tab_); +} + +void ProcessDiceHeaderDelegateImpl::EnableSync( + const CoreAccountId& account_id) { + DiceTabHelper* tab_helper = + GetDiceTabHelperFromWebContents(web_contents_.get()); + if (tab_helper) + tab_helper->OnSyncSigninFlowComplete(); + + if (!ShouldEnableSync()) { + // No special treatment is needed if the user is not enabling sync. + return; + } + + content::WebContents* web_contents = web_contents_.get(); + VLOG(1) << "Start sync after web sign-in."; + std::move(enable_sync_callback_) + .Run(profile_.get(), access_point_, promo_action_, reason_, web_contents, + account_id); + + if (!web_contents) + return; + + // After signing in to Chrome, the user should be redirected to the NTP, + // unless specified otherwise. + if (redirect_url_.is_empty()) { + RedirectToNtp(web_contents); + return; + } + + DCHECK(redirect_url_.is_valid()); + web_contents->GetController().LoadURL(redirect_url_, content::Referrer(), + ui::PAGE_TRANSITION_AUTO_TOPLEVEL, + std::string()); +} + +void ProcessDiceHeaderDelegateImpl::HandleTokenExchangeFailure( + const std::string& email, + const GoogleServiceAuthError& error) { + DCHECK_NE(GoogleServiceAuthError::NONE, error.state()); + DiceTabHelper* tab_helper = + GetDiceTabHelperFromWebContents(web_contents_.get()); + if (tab_helper) + tab_helper->OnSyncSigninFlowComplete(); + + bool should_enable_sync = ShouldEnableSync(); + + content::WebContents* web_contents = web_contents_.get(); + if (should_enable_sync && web_contents) + RedirectToNtp(web_contents); + + // Show the error even if the WebContents was closed, because the user may be + // signed out of the web. + std::move(show_signin_error_callback_) + .Run(profile_.get(), web_contents, + SigninUIError::FromGoogleServiceAuthError(email, error)); +} diff --git a/chromium/chrome/browser/signin/process_dice_header_delegate_impl.h b/chromium/chrome/browser/signin/process_dice_header_delegate_impl.h new file mode 100644 index 00000000000..cd7116700c6 --- /dev/null +++ b/chromium/chrome/browser/signin/process_dice_header_delegate_impl.h @@ -0,0 +1,77 @@ +// Copyright 2017 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 CHROME_BROWSER_SIGNIN_PROCESS_DICE_HEADER_DELEGATE_IMPL_H_ +#define CHROME_BROWSER_SIGNIN_PROCESS_DICE_HEADER_DELEGATE_IMPL_H_ + +#include "base/memory/raw_ptr.h" +#include "chrome/browser/signin/dice_response_handler.h" + +#include +#include + +#include "base/callback_forward.h" +#include "base/memory/weak_ptr.h" +#include "components/signin/public/base/signin_metrics.h" + +namespace content { +class WebContents; +} + +class Profile; +class SigninUIError; + +class ProcessDiceHeaderDelegateImpl : public ProcessDiceHeaderDelegate { + public: + // Callback starting Sync. + using EnableSyncCallback = + base::OnceCallback; + + // Callback showing a signin error UI. + using ShowSigninErrorCallback = base::OnceCallback< + void(Profile*, content::WebContents*, const SigninUIError&)>; + + // |is_sync_signin_tab| is true if a sync signin flow has been started in that + // tab. + ProcessDiceHeaderDelegateImpl( + content::WebContents* web_contents, + EnableSyncCallback enable_sync_callback, + ShowSigninErrorCallback show_signin_error_callback); + + ProcessDiceHeaderDelegateImpl(const ProcessDiceHeaderDelegateImpl&) = delete; + ProcessDiceHeaderDelegateImpl& operator=( + const ProcessDiceHeaderDelegateImpl&) = delete; + + ~ProcessDiceHeaderDelegateImpl() override; + + // ProcessDiceHeaderDelegate: + void HandleTokenExchangeSuccess(CoreAccountId account_id, + bool is_new_account) override; + void EnableSync(const CoreAccountId& account_id) override; + void HandleTokenExchangeFailure(const std::string& email, + const GoogleServiceAuthError& error) override; + + private: + // Returns true if sync should be enabled after the user signs in. + bool ShouldEnableSync(); + + const base::WeakPtr web_contents_; + raw_ptr profile_; + EnableSyncCallback enable_sync_callback_; + ShowSigninErrorCallback show_signin_error_callback_; + bool is_sync_signin_tab_ = false; + signin_metrics::AccessPoint access_point_ = + signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN; + signin_metrics::PromoAction promo_action_ = + signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO; + signin_metrics::Reason reason_ = signin_metrics::Reason::kUnknownReason; + GURL redirect_url_; +}; + +#endif // CHROME_BROWSER_SIGNIN_PROCESS_DICE_HEADER_DELEGATE_IMPL_H_ diff --git a/chromium/chrome/browser/signin/process_dice_header_delegate_impl_unittest.cc b/chromium/chrome/browser/signin/process_dice_header_delegate_impl_unittest.cc new file mode 100644 index 00000000000..60c842301f9 --- /dev/null +++ b/chromium/chrome/browser/signin/process_dice_header_delegate_impl_unittest.cc @@ -0,0 +1,375 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/process_dice_header_delegate_impl.h" + +#include +#include +#include + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/strings/utf_string_conversions.h" +#include "chrome/browser/signin/dice_tab_helper.h" +#include "chrome/browser/signin/dice_web_signin_interceptor.h" +#include "chrome/browser/signin/dice_web_signin_interceptor_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h" +#include "chrome/browser/ui/webui/signin/signin_ui_error.h" +#include "chrome/common/url_constants.h" +#include "chrome/test/base/chrome_render_view_host_test_harness.h" +#include "components/signin/public/base/account_consistency_method.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "content/public/browser/web_contents.h" +#include "content/public/test/navigation_simulator.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using signin_metrics::Reason; + +namespace { + +signin_metrics::AccessPoint kTestAccessPoint = + signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_BUBBLE; + +signin_metrics::PromoAction kTestPromoAction = + signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO; + +// Dummy delegate that declines all interceptions. +class TestDiceWebSigninInterceptorDelegate + : public DiceWebSigninInterceptor::Delegate { + public: + ~TestDiceWebSigninInterceptorDelegate() override = default; + std::unique_ptr + ShowSigninInterceptionBubble( + content::WebContents* web_contents, + const BubbleParameters& bubble_parameters, + base::OnceCallback callback) override { + std::move(callback).Run(SigninInterceptionResult::kDeclined); + return nullptr; + } + + void ShowProfileCustomizationBubble(Browser* browser) override {} +}; + +class MockDiceWebSigninInterceptor : public DiceWebSigninInterceptor { + public: + explicit MockDiceWebSigninInterceptor(Profile* profile) + : DiceWebSigninInterceptor( + profile, + std::make_unique()) {} + ~MockDiceWebSigninInterceptor() override = default; + + MOCK_METHOD(void, + MaybeInterceptWebSignin, + (content::WebContents * web_contents, + CoreAccountId account_id, + bool is_new_account, + bool is_sync_signin), + (override)); +}; + +std::unique_ptr CreateMockDiceWebSigninInterceptor( + content::BrowserContext* context) { + return std::make_unique( + Profile::FromBrowserContext(context)); +} + +class ProcessDiceHeaderDelegateImplTest + : public ChromeRenderViewHostTestHarness { + public: + ProcessDiceHeaderDelegateImplTest() + : enable_sync_called_(false), + show_error_called_(false), + account_id_("12345"), + email_("foo@bar.com"), + auth_error_(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS) {} + + ~ProcessDiceHeaderDelegateImplTest() override {} + + void AddAccount(bool is_primary) { + if (!identity_test_environment_profile_adaptor_) + InitializeIdentityTestEnvironment(); + if (is_primary) { + identity_test_environment_profile_adaptor_->identity_test_env() + ->SetPrimaryAccount(email_, signin::ConsentLevel::kSync); + } else { + identity_test_environment_profile_adaptor_->identity_test_env() + ->MakeAccountAvailable(email_); + } + } + + void InitializeIdentityTestEnvironment() { + DCHECK(profile()); + identity_test_environment_profile_adaptor_ = + std::make_unique(profile()); + } + + // Creates a ProcessDiceHeaderDelegateImpl instance. + std::unique_ptr + CreateDelegateAndNavigateToSignin( + bool is_sync_signin_tab, + Reason reason = Reason::kSigninPrimaryAccount) { + signin_reason_ = reason; + if (!identity_test_environment_profile_adaptor_) + InitializeIdentityTestEnvironment(); + // Load the signin page. + std::unique_ptr simulator = + content::NavigationSimulator::CreateRendererInitiated(signin_url_, + main_rfh()); + simulator->Start(); + if (is_sync_signin_tab) { + DiceTabHelper::CreateForWebContents(web_contents()); + DiceTabHelper* dice_tab_helper = + DiceTabHelper::FromWebContents(web_contents()); + dice_tab_helper->InitializeSigninFlow(signin_url_, kTestAccessPoint, + signin_reason_, kTestPromoAction, + GURL::EmptyGURL()); + } + simulator->Commit(); + DCHECK_EQ(signin_url_, web_contents()->GetVisibleURL()); + return std::make_unique( + web_contents(), + base::BindOnce(&ProcessDiceHeaderDelegateImplTest::StartSyncCallback, + base::Unretained(this)), + base::BindOnce( + &ProcessDiceHeaderDelegateImplTest::ShowSigninErrorCallback, + base::Unretained(this))); + } + + // ChromeRenderViewHostTestHarness: + TestingProfile::TestingFactories GetTestingFactories() const override { + TestingProfile::TestingFactories factories = { + {DiceWebSigninInterceptorFactory::GetInstance(), + base::BindRepeating(&CreateMockDiceWebSigninInterceptor)}}; + IdentityTestEnvironmentProfileAdaptor:: + AppendIdentityTestEnvironmentFactories(&factories); + return factories; + } + + void TearDown() override { + identity_test_environment_profile_adaptor_.reset(); + ChromeRenderViewHostTestHarness::TearDown(); + } + + // Callback for the ProcessDiceHeaderDelegateImpl. + void StartSyncCallback(Profile* profile, + signin_metrics::AccessPoint access_point, + signin_metrics::PromoAction promo_action, + signin_metrics::Reason reason, + content::WebContents* contents, + const CoreAccountId& account_id) { + EXPECT_EQ(profile, this->profile()); + EXPECT_EQ(access_point, kTestAccessPoint); + EXPECT_EQ(promo_action, kTestPromoAction); + EXPECT_EQ(reason, signin_reason_); + EXPECT_EQ(web_contents(), contents); + EXPECT_EQ(account_id_, account_id); + enable_sync_called_ = true; + } + + // Callback for the ProcessDiceHeaderDelegateImpl. + void ShowSigninErrorCallback(Profile* profile, + content::WebContents* contents, + const SigninUIError& error) { + EXPECT_EQ(profile, this->profile()); + EXPECT_EQ(web_contents(), contents); + EXPECT_EQ(base::UTF8ToUTF16(auth_error_.ToString()), error.message()); + EXPECT_EQ(base::UTF8ToUTF16(email_), error.email()); + show_error_called_ = true; + } + + MockDiceWebSigninInterceptor* mock_interceptor() { + return static_cast( + DiceWebSigninInterceptorFactory::GetForProfile(profile())); + } + + std::unique_ptr + identity_test_environment_profile_adaptor_; + + const GURL signin_url_ = GURL("https://accounts.google.com"); + bool enable_sync_called_; + bool show_error_called_; + CoreAccountId account_id_; + std::string email_; + GoogleServiceAuthError auth_error_; + Reason signin_reason_ = Reason::kSigninPrimaryAccount; +}; + +// Check that sync is enabled if the tab is closed during signin. +TEST_F(ProcessDiceHeaderDelegateImplTest, CloseTabWhileStartingSync) { + std::unique_ptr delegate = + CreateDelegateAndNavigateToSignin(true); + + // Close the tab. + DeleteContents(); + + // Check expectations. + delegate->EnableSync(account_id_); + EXPECT_TRUE(enable_sync_called_); + EXPECT_FALSE(show_error_called_); +} + +// Check that the error is still shown if the tab is closed before the error is +// received. +TEST_F(ProcessDiceHeaderDelegateImplTest, CloseTabWhileFailingSignin) { + std::unique_ptr delegate = + CreateDelegateAndNavigateToSignin(true); + + // Close the tab. + DeleteContents(); + + // Check expectations. + delegate->HandleTokenExchangeFailure(email_, auth_error_); + EXPECT_FALSE(enable_sync_called_); + EXPECT_TRUE(show_error_called_); +} + +struct TestConfiguration { + // Test setup. + bool signed_in; // User was already signed in at the start of the flow. + bool signin_tab; // The tab is marked as a Sync signin tab. + + // Test expectations. + bool callback_called; // The relevant callback was called. + bool show_ntp; // The NTP was shown. +}; + +TestConfiguration kEnableSyncTestCases[] = { + // clang-format off + // signed_in | signin_tab | callback_called | show_ntp + { false, false, false, false}, + { false, true, true, true}, + { true, false, false, false}, + { true, true, false, false}, + // clang-format on +}; + +// Parameterized version of ProcessDiceHeaderDelegateImplTest. +class ProcessDiceHeaderDelegateImplTestEnableSync + : public ProcessDiceHeaderDelegateImplTest, + public ::testing::WithParamInterface {}; + +// Test the EnableSync() method in all configurations. +TEST_P(ProcessDiceHeaderDelegateImplTestEnableSync, EnableSync) { + if (GetParam().signed_in) + AddAccount(/*is_primary=*/true); + std::unique_ptr delegate = + CreateDelegateAndNavigateToSignin(GetParam().signin_tab); + delegate->EnableSync(account_id_); + EXPECT_EQ(GetParam().callback_called, enable_sync_called_); + GURL expected_url = + GetParam().show_ntp ? GURL(chrome::kChromeUINewTabURL) : signin_url_; + EXPECT_EQ(expected_url, web_contents()->GetVisibleURL()); + EXPECT_FALSE(show_error_called_); + // Check that the sync signin flow is complete. + if (GetParam().signin_tab) { + DiceTabHelper* dice_tab_helper = + DiceTabHelper::FromWebContents(web_contents()); + ASSERT_TRUE(dice_tab_helper); + EXPECT_FALSE(dice_tab_helper->IsSyncSigninInProgress()); + } +} + +INSTANTIATE_TEST_SUITE_P(All, + ProcessDiceHeaderDelegateImplTestEnableSync, + ::testing::ValuesIn(kEnableSyncTestCases)); + +TestConfiguration kHandleTokenExchangeFailureTestCases[] = { + // clang-format off + // signed_in | signin_tab | callback_called | show_ntp + { false, false, true, false}, + { false, true, true, true}, + { true, false, true, false}, + { true, true, true, false}, + // clang-format on +}; + +// Parameterized version of ProcessDiceHeaderDelegateImplTest. +class ProcessDiceHeaderDelegateImplTestHandleTokenExchangeFailure + : public ProcessDiceHeaderDelegateImplTest, + public ::testing::WithParamInterface {}; + +// Test the HandleTokenExchangeFailure() method in all configurations. +TEST_P(ProcessDiceHeaderDelegateImplTestHandleTokenExchangeFailure, + HandleTokenExchangeFailure) { + if (GetParam().signed_in) + AddAccount(/*is_primary=*/true); + std::unique_ptr delegate = + CreateDelegateAndNavigateToSignin(GetParam().signin_tab); + delegate->HandleTokenExchangeFailure(email_, auth_error_); + EXPECT_FALSE(enable_sync_called_); + EXPECT_EQ(GetParam().callback_called, show_error_called_); + GURL expected_url = + GetParam().show_ntp ? GURL(chrome::kChromeUINewTabURL) : signin_url_; + EXPECT_EQ(expected_url, web_contents()->GetVisibleURL()); + // Check that the sync signin flow is complete. + if (GetParam().signin_tab) { + DiceTabHelper* dice_tab_helper = + DiceTabHelper::FromWebContents(web_contents()); + ASSERT_TRUE(dice_tab_helper); + EXPECT_FALSE(dice_tab_helper->IsSyncSigninInProgress()); + } +} + +INSTANTIATE_TEST_SUITE_P( + All, + ProcessDiceHeaderDelegateImplTestHandleTokenExchangeFailure, + ::testing::ValuesIn(kHandleTokenExchangeFailureTestCases)); + +struct TokenExchangeSuccessConfiguration { + bool is_reauth; // User was already signed in with the account. + bool signin_tab; // A DiceTabHelper is attached to the tab. + Reason reason; + bool sync_signin; // Expected value for the MaybeInterceptWebSigin call. +}; + +TokenExchangeSuccessConfiguration kHandleTokenExchangeSuccessTestCases[] = { + // clang-format off + // is_reauth | signin_tab | reason    | sync_signin + { false, false, Reason::kSigninPrimaryAccount, false }, + { false, true, Reason::kSigninPrimaryAccount, true }, + { false, true, Reason::kAddSecondaryAccount, false }, + { true, false, Reason::kSigninPrimaryAccount, false }, + { true, true, Reason::kSigninPrimaryAccount, true }, + // clang-format on +}; + +// Parameterized version of ProcessDiceHeaderDelegateImplTest. +class ProcessDiceHeaderDelegateImplTestHandleTokenExchangeSuccess + : public ProcessDiceHeaderDelegateImplTest, + public ::testing::WithParamInterface { +}; + +// Test the HandleTokenExchangeSuccess() method in all configurations. +TEST_P(ProcessDiceHeaderDelegateImplTestHandleTokenExchangeSuccess, + HandleTokenExchangeSuccess) { + if (GetParam().is_reauth) + AddAccount(/*is_primary=*/false); + std::unique_ptr delegate = + CreateDelegateAndNavigateToSignin(GetParam().signin_tab, + GetParam().reason); + EXPECT_CALL( + *mock_interceptor(), + MaybeInterceptWebSignin(web_contents(), account_id_, + !GetParam().is_reauth, GetParam().sync_signin)); + delegate->HandleTokenExchangeSuccess(account_id_, !GetParam().is_reauth); + + // Check that the sync signin flow is complete. + if (GetParam().signin_tab) { + DiceTabHelper* dice_tab_helper = + DiceTabHelper::FromWebContents(web_contents()); + ASSERT_TRUE(dice_tab_helper); + EXPECT_EQ(GetParam().sync_signin, + dice_tab_helper->IsSyncSigninInProgress()); + } +} + +INSTANTIATE_TEST_SUITE_P( + All, + ProcessDiceHeaderDelegateImplTestHandleTokenExchangeSuccess, + ::testing::ValuesIn(kHandleTokenExchangeSuccessTestCases)); + +} // namespace diff --git a/chromium/chrome/browser/signin/reauth_result.h b/chromium/chrome/browser/signin/reauth_result.h new file mode 100644 index 00000000000..9aa85bed81c --- /dev/null +++ b/chromium/chrome/browser/signin/reauth_result.h @@ -0,0 +1,38 @@ +// Copyright 2020 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 CHROME_BROWSER_SIGNIN_REAUTH_RESULT_H_ +#define CHROME_BROWSER_SIGNIN_REAUTH_RESULT_H_ + +namespace signin { + +// Indicates the result of the Gaia Reauth flow. +// Needs to be kept in sync with "SigninReauthResult" in enums.xml. +// These values are persisted to logs. Entries should not be renumbered and +// numeric values should never be reused. +enum class ReauthResult { + // The user was successfully re-authenticated. + kSuccess = 0, + + // The user account is not signed in. + kAccountNotSignedIn = 1, + + // The user dismissed the reauth prompt. + kDismissedByUser = 2, + + // The reauth page failed to load. + kLoadFailed = 3, + + // A caller canceled the reauth flow. + kCancelled = 4, + + // An unexpected response was received from Gaia. + kUnexpectedResponse = 5, + + kMaxValue = kUnexpectedResponse, +}; + +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_REAUTH_RESULT_H_ diff --git a/chromium/chrome/browser/signin/reauth_tab_helper.cc b/chromium/chrome/browser/signin/reauth_tab_helper.cc new file mode 100644 index 00000000000..e8bd1d8b1c8 --- /dev/null +++ b/chromium/chrome/browser/signin/reauth_tab_helper.cc @@ -0,0 +1,96 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/reauth_tab_helper.h" +#include "base/memory/ptr_util.h" +#include "chrome/browser/signin/reauth_result.h" +#include "content/public/browser/navigation_handle.h" +#include "net/http/http_status_code.h" +#include "url/origin.h" + +namespace signin { + +namespace { + +bool IsExpectedResponseCode(int response_code) { + return response_code == net::HTTP_OK || response_code == net::HTTP_NO_CONTENT; +} + +} // namespace + +// static +void ReauthTabHelper::CreateForWebContents(content::WebContents* web_contents, + const GURL& reauth_url, + ReauthCallback callback) { + DCHECK(web_contents); + if (!FromWebContents(web_contents)) { + web_contents->SetUserData( + UserDataKey(), base::WrapUnique(new ReauthTabHelper( + web_contents, reauth_url, std::move(callback)))); + } else { + std::move(callback).Run(signin::ReauthResult::kCancelled); + } +} + +ReauthTabHelper::~ReauthTabHelper() = default; + +void ReauthTabHelper::CompleteReauth(signin::ReauthResult result) { + if (callback_) + std::move(callback_).Run(result); +} + +void ReauthTabHelper::DidFinishNavigation( + content::NavigationHandle* navigation_handle) { + if (!navigation_handle->IsInPrimaryMainFrame()) + return; + + is_within_reauth_origin_ &= + url::IsSameOriginWith(reauth_url_, navigation_handle->GetURL()); + + if (navigation_handle->IsErrorPage()) { + has_last_committed_error_page_ = true; + return; + } + + has_last_committed_error_page_ = false; + + GURL::Replacements replacements; + replacements.ClearQuery(); + GURL url_without_query = + navigation_handle->GetURL().ReplaceComponents(replacements); + if (url_without_query != reauth_url_) + return; + + if (!navigation_handle->GetResponseHeaders() || + !IsExpectedResponseCode( + navigation_handle->GetResponseHeaders()->response_code())) { + CompleteReauth(signin::ReauthResult::kUnexpectedResponse); + } + + CompleteReauth(signin::ReauthResult::kSuccess); +} + +void ReauthTabHelper::WebContentsDestroyed() { + CompleteReauth(signin::ReauthResult::kDismissedByUser); +} + +bool ReauthTabHelper::is_within_reauth_origin() { + return is_within_reauth_origin_; +} + +bool ReauthTabHelper::has_last_committed_error_page() { + return has_last_committed_error_page_; +} + +ReauthTabHelper::ReauthTabHelper(content::WebContents* web_contents, + const GURL& reauth_url, + ReauthCallback callback) + : content::WebContentsUserData(*web_contents), + content::WebContentsObserver(web_contents), + reauth_url_(reauth_url), + callback_(std::move(callback)) {} + +WEB_CONTENTS_USER_DATA_KEY_IMPL(ReauthTabHelper); + +} // namespace signin diff --git a/chromium/chrome/browser/signin/reauth_tab_helper.h b/chromium/chrome/browser/signin/reauth_tab_helper.h new file mode 100644 index 00000000000..f830c24caec --- /dev/null +++ b/chromium/chrome/browser/signin/reauth_tab_helper.h @@ -0,0 +1,66 @@ +// Copyright 2020 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 CHROME_BROWSER_SIGNIN_REAUTH_TAB_HELPER_H_ +#define CHROME_BROWSER_SIGNIN_REAUTH_TAB_HELPER_H_ + +#include "base/callback.h" +#include "content/public/browser/web_contents_observer.h" +#include "content/public/browser/web_contents_user_data.h" +#include "url/gurl.h" + +namespace signin { + +enum class ReauthResult; + +// Tab helper class observing navigations within the reauth flow and notifying +// a caller about a flow result. +class ReauthTabHelper : public content::WebContentsUserData, + public content::WebContentsObserver { + public: + using ReauthCallback = base::OnceCallback; + + // Creates a new ReauthTabHelper and attaches it to |web_contents|. If an + // instance is already attached, no replacement happens, just notifies the + // caller by invoking |callback| with signin::ReauthResult::kCancelled. + // Initializes a helper with: + // - |callback| to be called when the reauth flow is complete. + // - |reauth_url| that should be the final destination of the reauth flow. + static void CreateForWebContents(content::WebContents* web_contents, + const GURL& reauth_url, + ReauthCallback callback); + + ReauthTabHelper(const ReauthTabHelper&) = delete; + ReauthTabHelper& operator=(const ReauthTabHelper&) = delete; + + ~ReauthTabHelper() override; + + // If |callback_| is not null, calls |callback_| with |result|. + void CompleteReauth(signin::ReauthResult result); + + // content::WebContentsObserver + void DidFinishNavigation( + content::NavigationHandle* navigation_handle) override; + void WebContentsDestroyed() override; + + bool is_within_reauth_origin(); + bool has_last_committed_error_page(); + + private: + friend class content::WebContentsUserData; + explicit ReauthTabHelper(content::WebContents* web_contents, + const GURL& reauth_url, + ReauthCallback callback); + + const GURL reauth_url_; + ReauthCallback callback_; + bool is_within_reauth_origin_ = true; + bool has_last_committed_error_page_ = false; + + WEB_CONTENTS_USER_DATA_KEY_DECL(); +}; + +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_REAUTH_TAB_HELPER_H_ diff --git a/chromium/chrome/browser/signin/reauth_tab_helper_unittest.cc b/chromium/chrome/browser/signin/reauth_tab_helper_unittest.cc new file mode 100644 index 00000000000..a17078cb067 --- /dev/null +++ b/chromium/chrome/browser/signin/reauth_tab_helper_unittest.cc @@ -0,0 +1,184 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/reauth_tab_helper.h" + +#include "base/memory/raw_ptr.h" +#include "base/test/mock_callback.h" +#include "chrome/browser/signin/reauth_result.h" +#include "chrome/test/base/chrome_render_view_host_test_harness.h" +#include "content/public/test/navigation_simulator.h" +#include "content/public/test/web_contents_tester.h" +#include "net/base/net_errors.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/public/common/features.h" + +namespace signin { + +class ReauthTabHelperTest : public ChromeRenderViewHostTestHarness { + public: + ReauthTabHelperTest() + : reauth_url_("https://my-identity_provider.com/reauth") {} + + void SetUp() override { + ChromeRenderViewHostTestHarness::SetUp(); + + ReauthTabHelper::CreateForWebContents(web_contents(), reauth_url(), + mock_callback_.Get()); + tab_helper_ = ReauthTabHelper::FromWebContents(web_contents()); + } + + ReauthTabHelper* tab_helper() { return tab_helper_; } + + base::MockOnceCallback* mock_callback() { + return &mock_callback_; + } + + const GURL& reauth_url() { return reauth_url_; } + + private: + raw_ptr tab_helper_ = nullptr; + base::MockOnceCallback mock_callback_; + const GURL reauth_url_; +}; + +// Tests a direct call to CompleteReauth(). +TEST_F(ReauthTabHelperTest, CompleteReauth) { + signin::ReauthResult result = signin::ReauthResult::kSuccess; + EXPECT_CALL(*mock_callback(), Run(result)); + tab_helper()->CompleteReauth(result); +} + +// Tests a successful navigation to the reauth URL. +TEST_F(ReauthTabHelperTest, NavigateToReauthURL) { + auto simulator = content::NavigationSimulator::CreateBrowserInitiated( + reauth_url(), web_contents()); + simulator->Start(); + EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess)); + simulator->Commit(); +} + +// Tests the reauth flow when the reauth URL has query parameters. +TEST_F(ReauthTabHelperTest, NavigateToReauthURLWithQuery) { + auto simulator = content::NavigationSimulator::CreateBrowserInitiated( + reauth_url().Resolve("?rapt=35be36ae"), web_contents()); + simulator->Start(); + EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess)); + simulator->Commit(); +} + +// Tests the reauth flow with multiple navigations within the same origin. +TEST_F(ReauthTabHelperTest, MultipleNavigationReauth) { + auto simulator = content::NavigationSimulator::CreateBrowserInitiated( + reauth_url(), web_contents()); + simulator->Start(); + simulator->Redirect( + reauth_url().DeprecatedGetOriginAsURL().Resolve("/login")); + simulator->Commit(); + + auto simulator2 = content::NavigationSimulator::CreateRendererInitiated( + reauth_url(), main_rfh()); + simulator2->Start(); + EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess)); + simulator2->Commit(); +} + +// Tests the reauth flow with multiple navigations across two different origins. +// TODO(https://crbug.com/1045515): update this test once navigations outside of +// reauth_url() are blocked. +TEST_F(ReauthTabHelperTest, MultipleNavigationReauthThroughExternalOrigin) { + auto simulator = content::NavigationSimulator::CreateBrowserInitiated( + reauth_url(), web_contents()); + simulator->Start(); + simulator->Redirect(GURL("https://other-identity-provider.com/login")); + simulator->Commit(); + + auto simulator2 = content::NavigationSimulator::CreateRendererInitiated( + reauth_url(), main_rfh()); + simulator2->Start(); + EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess)); + simulator2->Commit(); +} + +// Tests a failed navigation to the reauth URL, followed by a successful +// navigation. +TEST_F(ReauthTabHelperTest, NavigationToReauthURLFailed) { + auto simulator = content::NavigationSimulator::CreateBrowserInitiated( + reauth_url(), web_contents()); + simulator->Start(); + simulator->Fail(net::ERR_TIMED_OUT); + simulator->CommitErrorPage(); + EXPECT_TRUE(tab_helper()->has_last_committed_error_page()); + // Check that the navigation still counts as within the same origin. + EXPECT_TRUE(tab_helper()->is_within_reauth_origin()); + + auto simulator2 = content::NavigationSimulator::CreateRendererInitiated( + reauth_url(), main_rfh()); + simulator2->Start(); + EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess)); + simulator2->Commit(); + EXPECT_FALSE(tab_helper()->has_last_committed_error_page()); +} + +// Tests a failed navigation redirecting to an external origin, followed by a +// successful navigation. +TEST_F(ReauthTabHelperTest, NavigationToExternalOriginFailed) { + auto simulator = content::NavigationSimulator::CreateBrowserInitiated( + reauth_url(), web_contents()); + simulator->Start(); + simulator->Redirect(GURL("https://other-identity-provider.com/login")); + simulator->Fail(net::ERR_TIMED_OUT); + simulator->CommitErrorPage(); + EXPECT_TRUE(tab_helper()->has_last_committed_error_page()); + // Check that the navigation doesn't count as within the same origin. + EXPECT_FALSE(tab_helper()->is_within_reauth_origin()); + + auto simulator2 = content::NavigationSimulator::CreateRendererInitiated( + reauth_url(), main_rfh()); + simulator2->Start(); + EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess)); + simulator2->Commit(); + EXPECT_FALSE(tab_helper()->has_last_committed_error_page()); + EXPECT_FALSE(tab_helper()->is_within_reauth_origin()); +} + +// Tests the WebContents deletion. +TEST_F(ReauthTabHelperTest, WebContentsDestroyed) { + EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kDismissedByUser)); + DeleteContents(); +} + +class ReauthTabHelperPrerenderTest : public ReauthTabHelperTest { + public: + ReauthTabHelperPrerenderTest() { + feature_list_.InitWithFeatures( + {blink::features::kPrerender2}, + // Disable the memory requirement of Prerender2 so the test can run on + // any bot. + {blink::features::kPrerender2MemoryControls}); + } + + private: + base::test::ScopedFeatureList feature_list_; +}; + +TEST_F(ReauthTabHelperPrerenderTest, + PrerenderDoesNotAffectLastCommittedErrorPage) { + content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(), + reauth_url()); + EXPECT_FALSE(tab_helper()->has_last_committed_error_page()); + + // Fail prerendering navigation. + const GURL prerender_url = reauth_url().Resolve("?prerendering"); + auto simulator = content::WebContentsTester::For(web_contents()) + ->AddPrerenderAndStartNavigation(prerender_url); + simulator->Fail(net::ERR_TIMED_OUT); + simulator->CommitErrorPage(); + + // has_last_committed_error_page_ is not updated by preredering. + EXPECT_FALSE(tab_helper()->has_last_committed_error_page()); +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/reauth_util.cc b/chromium/chrome/browser/signin/reauth_util.cc new file mode 100644 index 00000000000..b770da50fb0 --- /dev/null +++ b/chromium/chrome/browser/signin/reauth_util.cc @@ -0,0 +1,40 @@ +// Copyright 2020 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 + +#include "base/strings/string_number_conversions.h" +#include "chrome/browser/signin/reauth_util.h" +#include "chrome/common/webui_url_constants.h" +#include "net/base/url_util.h" + +namespace signin { + +GURL GetReauthConfirmationURL(signin_metrics::ReauthAccessPoint access_point) { + GURL url = GURL(chrome::kChromeUISigninReauthURL); + url = net::AppendQueryParameter( + url, "access_point", + base::NumberToString(static_cast(access_point))); + return url; +} + +signin_metrics::ReauthAccessPoint GetReauthAccessPointForReauthConfirmationURL( + const GURL& url) { + std::string value; + if (!net::GetValueForKeyInQuery(url, "access_point", &value)) + return signin_metrics::ReauthAccessPoint::kUnknown; + + int access_point = -1; + base::StringToInt(value, &access_point); + if (access_point <= + static_cast(signin_metrics::ReauthAccessPoint::kUnknown) || + access_point > + static_cast(signin_metrics::ReauthAccessPoint::kMaxValue)) { + return signin_metrics::ReauthAccessPoint::kUnknown; + } + + return static_cast(access_point); +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/reauth_util.h b/chromium/chrome/browser/signin/reauth_util.h new file mode 100644 index 00000000000..52150ee53f1 --- /dev/null +++ b/chromium/chrome/browser/signin/reauth_util.h @@ -0,0 +1,24 @@ +// Copyright 2020 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 CHROME_BROWSER_SIGNIN_REAUTH_UTIL_H_ +#define CHROME_BROWSER_SIGNIN_REAUTH_UTIL_H_ + +#include "components/signin/public/base/signin_metrics.h" +#include "url/gurl.h" + +namespace signin { + +// Returns a URL to display in the reauth confirmation dialog. The dialog was +// triggered by |access_point|. +GURL GetReauthConfirmationURL(signin_metrics::ReauthAccessPoint access_point); + +// Returns ReauthAccessPoint encoded in the query of the reauth confirmation +// URL. +signin_metrics::ReauthAccessPoint GetReauthAccessPointForReauthConfirmationURL( + const GURL& url); + +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_REAUTH_UTIL_H_ diff --git a/chromium/chrome/browser/signin/reauth_util_unittest.cc b/chromium/chrome/browser/signin/reauth_util_unittest.cc new file mode 100644 index 00000000000..44fb952465d --- /dev/null +++ b/chromium/chrome/browser/signin/reauth_util_unittest.cc @@ -0,0 +1,33 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/reauth_util.h" + +#include "chrome/common/webui_url_constants.h" +#include "components/signin/public/base/signin_metrics.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace signin { + +class ReauthUtilURLTest : public ::testing::TestWithParam {}; + +TEST_P(ReauthUtilURLTest, GetAndParseReauthConfirmationURL) { + auto access_point = + static_cast(GetParam()); + GURL url = GetReauthConfirmationURL(access_point); + ASSERT_TRUE(url.is_valid()); + EXPECT_EQ(url.host(), chrome::kChromeUISigninReauthHost); + signin_metrics::ReauthAccessPoint get_access_point = + GetReauthAccessPointForReauthConfirmationURL(url); + EXPECT_EQ(get_access_point, access_point); +} + +INSTANTIATE_TEST_CASE_P( + AllAccessPoints, + ReauthUtilURLTest, + ::testing::Range( + static_cast(signin_metrics::ReauthAccessPoint::kUnknown), + static_cast(signin_metrics::ReauthAccessPoint::kMaxValue) + 1)); + +} // namespace signin diff --git a/chromium/chrome/browser/signin/remove_local_account_browsertest.cc b/chromium/chrome/browser/signin/remove_local_account_browsertest.cc new file mode 100644 index 00000000000..259f3d2857e --- /dev/null +++ b/chromium/chrome/browser/signin/remove_local_account_browsertest.cc @@ -0,0 +1,143 @@ +// Copyright 2021 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 + +#include "base/run_loop.h" +#include "base/test/bind.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_tabstrip.h" +#include "chrome/test/base/mixin_based_in_process_browser_test.h" +#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_test_utils.h" +#include "components/signin/public/identity_manager/test_identity_manager_observer.h" +#include "content/public/test/browser_test.h" +#include "google_apis/gaia/fake_gaia.h" +#include "google_apis/gaia/gaia_switches.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "net/test/embedded_test_server/http_response.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "chrome/browser/ash/login/test/network_portal_detector_mixin.h" +#endif + +namespace { + +using testing::Contains; +using testing::Not; + +MATCHER_P(ListedAccountMatchesGaiaId, gaia_id, "") { + return arg.gaia_id == std::string(gaia_id); +} + +const char kTestGaiaId[] = "123"; + +class RemoveLocalAccountTest : public MixinBasedInProcessBrowserTest { + protected: + RemoveLocalAccountTest() + : embedded_test_server_(net::EmbeddedTestServer::TYPE_HTTPS) { + embedded_test_server_.RegisterRequestHandler(base::BindRepeating( + &FakeGaia::HandleRequest, base::Unretained(&fake_gaia_))); + } + + ~RemoveLocalAccountTest() override = default; + + signin::IdentityManager* identity_manager() { + return IdentityManagerFactory::GetForProfile(browser()->profile()); + } + + signin::AccountsInCookieJarInfo WaitUntilAccountsInCookieUpdated() { + signin::TestIdentityManagerObserver observer(identity_manager()); + base::RunLoop run_loop; + observer.SetOnAccountsInCookieUpdatedCallback(run_loop.QuitClosure()); + run_loop.Run(); + return observer.AccountsInfoFromAccountsInCookieUpdatedCallback(); + } + + // MixinBasedInProcessBrowserTest: + void SetUpCommandLine(base::CommandLine* command_line) override { + MixinBasedInProcessBrowserTest::SetUpCommandLine(command_line); + ASSERT_TRUE(embedded_test_server_.InitializeAndListen()); + const GURL base_url = embedded_test_server_.base_url(); + command_line->AppendSwitchASCII(switches::kGaiaUrl, base_url.spec()); + } + + void SetUpOnMainThread() override { + MixinBasedInProcessBrowserTest::SetUpOnMainThread(); + fake_gaia_.Initialize(); + + FakeGaia::MergeSessionParams params; + params.signed_out_gaia_ids.push_back(kTestGaiaId); + fake_gaia_.UpdateMergeSessionParams(params); + + embedded_test_server_.StartAcceptingConnections(); + +#if BUILDFLAG(IS_CHROMEOS_ASH) + // ChromeSigninClient uses chromeos::DelayNetworkCall() which requires + // simulating being online. + network_portal_detector_.SimulateDefaultNetworkState( + ash::NetworkPortalDetector::CAPTIVE_PORTAL_STATUS_ONLINE); +#endif + } + + FakeGaia fake_gaia_; + net::EmbeddedTestServer embedded_test_server_; + +#if BUILDFLAG(IS_CHROMEOS_ASH) + ash::NetworkPortalDetectorMixin network_portal_detector_{&mixin_host_}; +#endif +}; + +IN_PROC_BROWSER_TEST_F(RemoveLocalAccountTest, ShouldNotifyObservers) { + // To enforce an initial ListAccounts fetch and the corresponding notification + // to observers, make the current list as stale. This is done for the purpose + // of documenting assertions on the AccountsInCookieJarInfo passed to + // observers during notification. + signin::SetFreshnessOfAccountsInGaiaCookie(identity_manager(), + /*accounts_are_fresh=*/false); + + ASSERT_FALSE(identity_manager()->GetAccountsInCookieJar().accounts_are_fresh); + const signin::AccountsInCookieJarInfo + cookie_jar_info_in_initial_notification = + WaitUntilAccountsInCookieUpdated(); + ASSERT_TRUE(cookie_jar_info_in_initial_notification.accounts_are_fresh); + ASSERT_THAT(cookie_jar_info_in_initial_notification.signed_out_accounts, + Contains(ListedAccountMatchesGaiaId(kTestGaiaId))); + + const signin::AccountsInCookieJarInfo initial_cookie_jar_info = + identity_manager()->GetAccountsInCookieJar(); + ASSERT_TRUE(initial_cookie_jar_info.accounts_are_fresh); + ASSERT_THAT(initial_cookie_jar_info.signed_out_accounts, + Contains(ListedAccountMatchesGaiaId(kTestGaiaId))); + + // Open a FakeGaia page that issues the desired HTTP response header with + // Google-Accounts-RemoveLocalAccount. + chrome::AddTabAt(browser(), + fake_gaia_.GetDummyRemoveLocalAccountURL(kTestGaiaId), + /*index=*/0, + /*foreground=*/true); + + // Wait until observers are notified with OnAccountsInCookieUpdated(). + const signin::AccountsInCookieJarInfo + cookie_jar_info_in_updated_notification = + WaitUntilAccountsInCookieUpdated(); + + EXPECT_TRUE(cookie_jar_info_in_updated_notification.accounts_are_fresh); + EXPECT_THAT(cookie_jar_info_in_updated_notification.signed_out_accounts, + Not(Contains(ListedAccountMatchesGaiaId(kTestGaiaId)))); + + const signin::AccountsInCookieJarInfo updated_cookie_jar_info = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(updated_cookie_jar_info.accounts_are_fresh); + EXPECT_THAT(updated_cookie_jar_info.signed_out_accounts, + Not(Contains(ListedAccountMatchesGaiaId(kTestGaiaId)))); +} + +} // namespace diff --git a/chromium/chrome/browser/signin/services/DIR_METADATA b/chromium/chrome/browser/signin/services/DIR_METADATA new file mode 100644 index 00000000000..7a2580a646c --- /dev/null +++ b/chromium/chrome/browser/signin/services/DIR_METADATA @@ -0,0 +1 @@ +os: ANDROID diff --git a/chromium/chrome/browser/signin/services/OWNERS b/chromium/chrome/browser/signin/services/OWNERS new file mode 100644 index 00000000000..1c49383244f --- /dev/null +++ b/chromium/chrome/browser/signin/services/OWNERS @@ -0,0 +1,2 @@ +bsazonov@chromium.org +aliceywang@chromium.org diff --git a/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/ProfileDataCacheUnitTest.java b/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/ProfileDataCacheUnitTest.java new file mode 100644 index 00000000000..f64eb974942 --- /dev/null +++ b/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/ProfileDataCacheUnitTest.java @@ -0,0 +1,120 @@ +// Copyright 2021 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. + +package org.chromium.chrome.browser.signin.services; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.quality.Strictness; +import org.robolectric.RuntimeEnvironment; + +import org.chromium.base.test.BaseRobolectricTestRunner; +import org.chromium.base.test.util.JniMocker; +import org.chromium.chrome.R; +import org.chromium.chrome.test.util.browser.signin.AccountManagerTestRule; +import org.chromium.components.signin.base.AccountInfo; +import org.chromium.components.signin.base.CoreAccountId; +import org.chromium.components.signin.identitymanager.AccountInfoServiceProvider; +import org.chromium.components.signin.identitymanager.AccountTrackerService; +import org.chromium.components.signin.identitymanager.IdentityManager; +import org.chromium.components.signin.identitymanager.IdentityManagerJni; + +/** + * Unit tests for {@link ProfileDataCache} + */ +@RunWith(BaseRobolectricTestRunner.class) +public class ProfileDataCacheUnitTest { + private static final long NATIVE_IDENTITY_MANAGER = 10001L; + private static final String ACCOUNT_EMAIL = "test@gmail.com"; + + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS); + + @Rule + public final JniMocker mocker = new JniMocker(); + + @Rule + public final AccountManagerTestRule mAccountManagerTestRule = new AccountManagerTestRule(); + + @Mock + private AccountTrackerService mAccountTrackerServiceMock; + + @Mock + private IdentityManager.Natives mIdentityManagerNativeMock; + + @Mock + private ProfileDataCache.Observer mObserverMock; + + private final IdentityManager mIdentityManager = + IdentityManager.create(NATIVE_IDENTITY_MANAGER, null /* OAuth2TokenService */); + + private ProfileDataCache mProfileDataCache; + + @Before + public void setUp() { + mocker.mock(IdentityManagerJni.TEST_HOOKS, mIdentityManagerNativeMock); + mProfileDataCache = ProfileDataCache.createWithDefaultImageSizeAndNoBadge( + RuntimeEnvironment.application.getApplicationContext()); + + // Add an observer for IdentityManager::onExtendedAccountInfoUpdated. + mAccountManagerTestRule.observeIdentityManager(mIdentityManager); + } + + @After + public void tearDown() { + AccountInfoServiceProvider.resetForTests(); + } + + @Test + public void accountInfoIsUpdatedWithOnlyFullName() { + final String fullName = "full name1"; + final AccountInfo accountInfo = new AccountInfo(new CoreAccountId("gaia-id-test"), + ACCOUNT_EMAIL, "gaia-id-test", fullName, null, null); + mProfileDataCache.addObserver(mObserverMock); + Assert.assertFalse(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL)); + Assert.assertNull(mProfileDataCache.getProfileDataOrDefault(ACCOUNT_EMAIL).getFullName()); + + mIdentityManager.onExtendedAccountInfoUpdated(accountInfo); + + Assert.assertTrue(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL)); + Assert.assertEquals( + fullName, mProfileDataCache.getProfileDataOrDefault(ACCOUNT_EMAIL).getFullName()); + } + + @Test + public void accountInfoIsUpdatedWithOnlyGivenName() { + final String givenName = "given name1"; + final AccountInfo accountInfo = new AccountInfo(new CoreAccountId("gaia-id-test"), + ACCOUNT_EMAIL, "gaia-id-test", null, givenName, null); + mProfileDataCache.addObserver(mObserverMock); + Assert.assertFalse(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL)); + Assert.assertNull(mProfileDataCache.getProfileDataOrDefault(ACCOUNT_EMAIL).getGivenName()); + + mIdentityManager.onExtendedAccountInfoUpdated(accountInfo); + + Assert.assertTrue(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL)); + Assert.assertEquals( + givenName, mProfileDataCache.getProfileDataOrDefault(ACCOUNT_EMAIL).getGivenName()); + } + + @Test + public void accountInfoIsUpdatedWithOnlyBadgeConfig() { + mProfileDataCache.setBadge(R.drawable.ic_sync_badge_error_20dp); + final AccountInfo accountInfo = new AccountInfo( + new CoreAccountId("gaia-id-test"), ACCOUNT_EMAIL, "gaia-id-test", null, null, null); + mProfileDataCache.addObserver(mObserverMock); + Assert.assertFalse(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL)); + + mIdentityManager.onExtendedAccountInfoUpdated(accountInfo); + + Assert.assertTrue(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL)); + } +} diff --git a/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/WebSigninBridgeTest.java b/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/WebSigninBridgeTest.java new file mode 100644 index 00000000000..8acec65e1e9 --- /dev/null +++ b/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/WebSigninBridgeTest.java @@ -0,0 +1,89 @@ +// Copyright 2020 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. + +package org.chromium.chrome.browser.signin.services; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import org.chromium.base.test.BaseRobolectricTestRunner; +import org.chromium.base.test.util.JniMocker; +import org.chromium.chrome.browser.profiles.Profile; +import org.chromium.components.signin.base.CoreAccountInfo; +import org.chromium.components.signin.base.GoogleServiceAuthError; +import org.chromium.components.signin.base.GoogleServiceAuthError.State; + +/** + * Unit tests for {@link WebSigninBridge}. + */ +@RunWith(BaseRobolectricTestRunner.class) +public class WebSigninBridgeTest { + private static final CoreAccountInfo CORE_ACCOUNT_INFO = + CoreAccountInfo.createFromEmailAndGaiaId("user@domain.com", "gaia-id-user"); + private static final long NATIVE_WEB_SIGNIN_BRIDGE = 1000L; + + @Rule + public final JniMocker mocker = new JniMocker(); + + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Mock + private WebSigninBridge.Natives mNativeMock; + + @Mock + private Profile mProfileMock; + + @Mock + private WebSigninBridge.Listener mListenerMock; + + private final WebSigninBridge.Factory mFactory = new WebSigninBridge.Factory(); + + @Before + public void setUp() { + mocker.mock(WebSigninBridgeJni.TEST_HOOKS, mNativeMock); + when(mNativeMock.create(mProfileMock, CORE_ACCOUNT_INFO, mListenerMock)) + .thenReturn(NATIVE_WEB_SIGNIN_BRIDGE); + } + + @Test + public void testFactoryCreate() { + WebSigninBridge webSigninBridge = + mFactory.create(mProfileMock, CORE_ACCOUNT_INFO, mListenerMock); + Assert.assertNotNull("Factory#create should not return null!", webSigninBridge); + verify(mNativeMock).create(mProfileMock, CORE_ACCOUNT_INFO, mListenerMock); + } + + @Test + public void testDestroy() { + mFactory.create(mProfileMock, CORE_ACCOUNT_INFO, mListenerMock).destroy(); + verify(mNativeMock).destroy(NATIVE_WEB_SIGNIN_BRIDGE); + } + + @Test + public void testOnSigninSucceed() { + WebSigninBridge.onSigninSucceeded(mListenerMock); + verify(mListenerMock).onSigninSucceeded(); + verify(mListenerMock, never()).onSigninFailed(any()); + } + + @Test + public void testOnSigninFailed() { + final GoogleServiceAuthError error = new GoogleServiceAuthError(State.CONNECTION_FAILED); + WebSigninBridge.onSigninFailed(mListenerMock, error); + verify(mListenerMock).onSigninFailed(error); + verify(mListenerMock, never()).onSigninSucceeded(); + } +} diff --git a/chromium/chrome/browser/signin/signin_error_controller_factory.cc b/chromium/chrome/browser/signin/signin_error_controller_factory.cc new file mode 100644 index 00000000000..ebbd0612ba9 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_error_controller_factory.cc @@ -0,0 +1,48 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/signin_error_controller_factory.h" + +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" + +SigninErrorControllerFactory::SigninErrorControllerFactory() + : BrowserContextKeyedServiceFactory( + "SigninErrorController", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(IdentityManagerFactory::GetInstance()); +} + +SigninErrorControllerFactory::~SigninErrorControllerFactory() {} + +// static +SigninErrorController* SigninErrorControllerFactory::GetForProfile( + Profile* profile) { + return static_cast( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +// static +SigninErrorControllerFactory* SigninErrorControllerFactory::GetInstance() { + return base::Singleton::get(); +} + +KeyedService* SigninErrorControllerFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + SigninErrorController::AccountMode account_mode = +#if BUILDFLAG(IS_CHROMEOS_ASH) + SigninErrorController::AccountMode::ANY_ACCOUNT; +#else + AccountConsistencyModeManager::IsMirrorEnabledForProfile(profile) + ? SigninErrorController::AccountMode::ANY_ACCOUNT + : SigninErrorController::AccountMode::PRIMARY_ACCOUNT; +#endif + return new SigninErrorController( + account_mode, IdentityManagerFactory::GetForProfile(profile)); +} diff --git a/chromium/chrome/browser/signin/signin_error_controller_factory.h b/chromium/chrome/browser/signin/signin_error_controller_factory.h new file mode 100644 index 00000000000..0d87f49799e --- /dev/null +++ b/chromium/chrome/browser/signin/signin_error_controller_factory.h @@ -0,0 +1,37 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_ERROR_CONTROLLER_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_ERROR_CONTROLLER_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" +#include "components/signin/core/browser/signin_error_controller.h" + +class Profile; + +// Singleton that owns all SigninErrorControllers and associates them with +// Profiles. +class SigninErrorControllerFactory : public BrowserContextKeyedServiceFactory { + public: + // Returns the instance of SigninErrorController associated with this profile + // (creating one if none exists). Returns NULL if this profile cannot have an + // SigninClient (for example, if |profile| is incognito). + static SigninErrorController* GetForProfile(Profile* profile); + + // Returns an instance of the factory singleton. + static SigninErrorControllerFactory* GetInstance(); + + private: + friend struct base::DefaultSingletonTraits; + + SigninErrorControllerFactory(); + ~SigninErrorControllerFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_ERROR_CONTROLLER_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/signin_features.cc b/chromium/chrome/browser/signin/signin_features.cc new file mode 100644 index 00000000000..b1932809890 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_features.cc @@ -0,0 +1,16 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/signin_features.h" + +// Enables the client-side processing of the HTTP response header +// Google-Accounts-RemoveLocalAccount. +const base::Feature kProcessGaiaRemoveLocalAccountHeader{ + "ProcessGaiaRemoveLocalAccountHeader", base::FEATURE_ENABLED_BY_DEFAULT}; + +// Allows policies to be loaded on a managed account without activating sync. +// Uses enterprise confirmation dialog for managed accounts signin outside of +// the profile picker. +const base::Feature kAccountPoliciesLoadedWithoutSync{ + "AccountPoliciesLoadedWithoutSync", base::FEATURE_DISABLED_BY_DEFAULT}; diff --git a/chromium/chrome/browser/signin/signin_features.h b/chromium/chrome/browser/signin/signin_features.h new file mode 100644 index 00000000000..47c61c21949 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_features.h @@ -0,0 +1,15 @@ +// Copyright 2020 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 CHROME_BROWSER_SIGNIN_SIGNIN_FEATURES_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_FEATURES_H_ + +#include "base/feature_list.h" +#include "build/chromeos_buildflags.h" + +extern const base::Feature kProcessGaiaRemoveLocalAccountHeader; + +extern const base::Feature kAccountPoliciesLoadedWithoutSync; + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_FEATURES_H_ diff --git a/chromium/chrome/browser/signin/signin_global_error.cc b/chromium/chrome/browser/signin/signin_global_error.cc new file mode 100644 index 00000000000..d02d7ad0158 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_global_error.cc @@ -0,0 +1,170 @@ +// Copyright (c) 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 "chrome/browser/signin/signin_global_error.h" + +#include "base/logging.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/app/chrome_command_ids.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/ui/browser_commands.h" +#include "chrome/browser/ui/browser_window.h" +#include "chrome/browser/ui/chrome_pages.h" +#include "chrome/browser/ui/global_error/global_error_service.h" +#include "chrome/browser/ui/global_error/global_error_service_factory.h" +#include "chrome/browser/ui/singleton_tabs.h" +#include "chrome/browser/ui/webui/signin/login_ui_service.h" +#include "chrome/browser/ui/webui/signin/login_ui_service_factory.h" +#include "chrome/common/url_constants.h" +#include "chrome/grit/chromium_strings.h" +#include "chrome/grit/generated_resources.h" +#include "components/signin/public/base/signin_metrics.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "net/base/url_util.h" +#include "ui/base/l10n/l10n_util.h" + +#if !defined(OS_ANDROID) +#include "chrome/browser/signin/signin_promo.h" +#endif + +SigninGlobalError::SigninGlobalError( + SigninErrorController* error_controller, + Profile* profile) + : profile_(profile), + error_controller_(error_controller) { + error_controller_->AddObserver(this); +} + +SigninGlobalError::~SigninGlobalError() { + DCHECK(!error_controller_) + << "SigninGlobalError::Shutdown() was not called"; +} + +bool SigninGlobalError::HasError() { + return HasMenuItem(); +} + +void SigninGlobalError::Shutdown() { + error_controller_->RemoveObserver(this); + error_controller_ = nullptr; +} + +bool SigninGlobalError::HasMenuItem() { + return error_controller_->HasError(); +} + +int SigninGlobalError::MenuItemCommandID() { + return IDC_SHOW_SIGNIN_ERROR; +} + +std::u16string SigninGlobalError::MenuItemLabel() { + // Notify the user if there's an auth error the user should know about. + if (error_controller_->HasError()) + return l10n_util::GetStringUTF16(IDS_SYNC_SIGN_IN_ERROR_WRENCH_MENU_ITEM); + return std::u16string(); +} + +void SigninGlobalError::ExecuteMenuItem(Browser* browser) { +#if BUILDFLAG(IS_CHROMEOS_ASH) + if (error_controller_->auth_error().state() != + GoogleServiceAuthError::NONE) { + DVLOG(1) << "Signing out the user to fix a sync error."; + // TODO(beng): seems like this could just call chrome::AttemptUserExit(). + chrome::ExecuteCommand(browser, IDC_EXIT); + return; + } +#endif + + // Global errors don't show up in the wrench menu on mobile. +#if !defined(OS_ANDROID) + LoginUIService* login_ui = LoginUIServiceFactory::GetForProfile(profile_); + if (login_ui->current_login_ui()) { + login_ui->current_login_ui()->FocusUI(); + return; + } + + browser->window()->ShowAvatarBubbleFromAvatarButton( + BrowserWindow::AVATAR_BUBBLE_MODE_REAUTH, + signin_metrics::AccessPoint::ACCESS_POINT_MENU, false); +#endif +} + +bool SigninGlobalError::HasBubbleView() { + return !GetBubbleViewMessages().empty(); +} + +std::u16string SigninGlobalError::GetBubbleViewTitle() { + return l10n_util::GetStringUTF16(IDS_SIGNIN_ERROR_BUBBLE_VIEW_TITLE); +} + +std::vector SigninGlobalError::GetBubbleViewMessages() { + std::vector messages; + + // If the user isn't signed in, no need to display an error bubble. + auto* identity_manager = + IdentityManagerFactory::GetForProfileIfExists(profile_); + if (identity_manager && + !identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) { + return messages; + } + + if (!error_controller_->HasError()) + return messages; + + switch (error_controller_->auth_error().state()) { + // TODO(rogerta): use account id in error messages. + + // User credentials are invalid (bad acct, etc). + case GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS: + case GoogleServiceAuthError::SERVICE_ERROR: + messages.push_back(l10n_util::GetStringUTF16( + IDS_SYNC_SIGN_IN_ERROR_BUBBLE_VIEW_MESSAGE)); + break; + + // Sync service is not available for this account's domain. + case GoogleServiceAuthError::SERVICE_UNAVAILABLE: + messages.push_back(l10n_util::GetStringUTF16( + IDS_SYNC_UNAVAILABLE_ERROR_BUBBLE_VIEW_MESSAGE)); + break; + + // Generic message for "other" errors. + default: + messages.push_back(l10n_util::GetStringUTF16( + IDS_SYNC_OTHER_SIGN_IN_ERROR_BUBBLE_VIEW_MESSAGE)); + } + return messages; +} + +std::u16string SigninGlobalError::GetBubbleViewAcceptButtonLabel() { + // If the auth service is unavailable, don't give the user the option to try + // signing in again. + if (error_controller_->auth_error().state() == + GoogleServiceAuthError::SERVICE_UNAVAILABLE) { + return l10n_util::GetStringUTF16( + IDS_SYNC_UNAVAILABLE_ERROR_BUBBLE_VIEW_ACCEPT); + } else { + return l10n_util::GetStringUTF16(IDS_SYNC_SIGN_IN_ERROR_BUBBLE_VIEW_ACCEPT); + } +} + +std::u16string SigninGlobalError::GetBubbleViewCancelButtonLabel() { + return std::u16string(); +} + +void SigninGlobalError::OnBubbleViewDidClose(Browser* browser) { +} + +void SigninGlobalError::BubbleViewAcceptButtonPressed(Browser* browser) { + ExecuteMenuItem(browser); +} + +void SigninGlobalError::BubbleViewCancelButtonPressed(Browser* browser) { + NOTREACHED(); +} + +void SigninGlobalError::OnErrorChanged() { + GlobalErrorServiceFactory::GetForProfile(profile_)->NotifyErrorsChanged(); +} diff --git a/chromium/chrome/browser/signin/signin_global_error.h b/chromium/chrome/browser/signin/signin_global_error.h new file mode 100644 index 00000000000..50e110ea0bc --- /dev/null +++ b/chromium/chrome/browser/signin/signin_global_error.h @@ -0,0 +1,64 @@ +// Copyright (c) 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 CHROME_BROWSER_SIGNIN_SIGNIN_GLOBAL_ERROR_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_GLOBAL_ERROR_H_ + +#include +#include "base/gtest_prod_util.h" +#include "base/memory/raw_ptr.h" +#include "chrome/browser/ui/global_error/global_error.h" +#include "components/keyed_service/core/keyed_service.h" +#include "components/signin/core/browser/signin_error_controller.h" + +class Profile; + +// Shows auth errors on the wrench menu using a bubble view and a menu item. +class SigninGlobalError : public GlobalErrorWithStandardBubble, + public SigninErrorController::Observer, + public KeyedService { + public: + SigninGlobalError(SigninErrorController* error_controller, + Profile* profile); + + SigninGlobalError(const SigninGlobalError&) = delete; + SigninGlobalError& operator=(const SigninGlobalError&) = delete; + + ~SigninGlobalError() override; + + // Returns true if there is an authentication error. + bool HasError(); + + private: + FRIEND_TEST_ALL_PREFIXES(SigninGlobalErrorTest, Basic); + FRIEND_TEST_ALL_PREFIXES(SigninGlobalErrorTest, AuthStatusEnumerateAllErrors); + + // KeyedService: + void Shutdown() override; + + // GlobalErrorWithStandardBubble: + bool HasMenuItem() override; + int MenuItemCommandID() override; + std::u16string MenuItemLabel() override; + void ExecuteMenuItem(Browser* browser) override; + bool HasBubbleView() override; + std::u16string GetBubbleViewTitle() override; + std::vector GetBubbleViewMessages() override; + std::u16string GetBubbleViewAcceptButtonLabel() override; + std::u16string GetBubbleViewCancelButtonLabel() override; + void OnBubbleViewDidClose(Browser* browser) override; + void BubbleViewAcceptButtonPressed(Browser* browser) override; + void BubbleViewCancelButtonPressed(Browser* browser) override; + + // SigninErrorController::Observer: + void OnErrorChanged() override; + + // The Profile this service belongs to. + raw_ptr profile_; + + // The SigninErrorController that provides auth status. + raw_ptr error_controller_; +}; + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_GLOBAL_ERROR_H_ diff --git a/chromium/chrome/browser/signin/signin_global_error_factory.cc b/chromium/chrome/browser/signin/signin_global_error_factory.cc new file mode 100644 index 00000000000..304d6a59cce --- /dev/null +++ b/chromium/chrome/browser/signin/signin_global_error_factory.cc @@ -0,0 +1,46 @@ +// 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 "chrome/browser/signin/signin_global_error_factory.h" + +#include "build/chromeos_buildflags.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/signin_error_controller_factory.h" +#include "chrome/browser/signin/signin_global_error.h" +#include "chrome/browser/ui/global_error/global_error_service_factory.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" + +SigninGlobalErrorFactory::SigninGlobalErrorFactory() + : BrowserContextKeyedServiceFactory( + "SigninGlobalError", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(SigninErrorControllerFactory::GetInstance()); + DependsOn(GlobalErrorServiceFactory::GetInstance()); +} + +SigninGlobalErrorFactory::~SigninGlobalErrorFactory() {} + +// static +SigninGlobalError* SigninGlobalErrorFactory::GetForProfile( + Profile* profile) { + return static_cast( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +// static +SigninGlobalErrorFactory* SigninGlobalErrorFactory::GetInstance() { + return base::Singleton::get(); +} + +KeyedService* SigninGlobalErrorFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { +#if BUILDFLAG(IS_CHROMEOS_ASH) + return nullptr; +#endif + + Profile* profile = static_cast(context); + return new SigninGlobalError( + SigninErrorControllerFactory::GetForProfile(profile), profile); +} diff --git a/chromium/chrome/browser/signin/signin_global_error_factory.h b/chromium/chrome/browser/signin/signin_global_error_factory.h new file mode 100644 index 00000000000..d13b4f6df61 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_global_error_factory.h @@ -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. + +#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_GLOBAL_ERROR_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_GLOBAL_ERROR_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +class SigninGlobalError; +class Profile; + +// Singleton that owns all SigninGlobalErrors and associates them with +// Profiles. Listens for the Profile's destruction notification and cleans up +// the associated SigninGlobalError. +class SigninGlobalErrorFactory : public BrowserContextKeyedServiceFactory { + public: + // Returns the instance of SigninGlobalError associated with this + // profile, creating one if none exists. In Ash, this will return NULL. + static SigninGlobalError* GetForProfile(Profile* profile); + + // Returns an instance of the SigninGlobalErrorFactory singleton. + static SigninGlobalErrorFactory* GetInstance(); + + SigninGlobalErrorFactory(const SigninGlobalErrorFactory&) = delete; + SigninGlobalErrorFactory& operator=(const SigninGlobalErrorFactory&) = delete; + + private: + friend struct base::DefaultSingletonTraits; + + SigninGlobalErrorFactory(); + ~SigninGlobalErrorFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_GLOBAL_ERROR_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/signin_global_error_unittest.cc b/chromium/chrome/browser/signin/signin_global_error_unittest.cc new file mode 100644 index 00000000000..8d4f5f66eb6 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_global_error_unittest.cc @@ -0,0 +1,166 @@ +// Copyright (c) 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 "chrome/browser/signin/signin_global_error.h" + +#include + +#include +#include + +#include "base/bind.h" +#include "base/cxx17_backports.h" +#include "base/memory/raw_ptr.h" +#include "base/strings/utf_string_conversions.h" +#include "base/test/metrics/histogram_tester.h" +#include "chrome/browser/profiles/profile_attributes_entry.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/profiles/profile_metrics.h" +#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h" +#include "chrome/browser/signin/signin_error_controller_factory.h" +#include "chrome/browser/signin/signin_global_error_factory.h" +#include "chrome/browser/ui/global_error/global_error_service.h" +#include "chrome/browser/ui/global_error/global_error_service_factory.h" +#include "chrome/common/pref_names.h" +#include "chrome/test/base/testing_browser_process.h" +#include "chrome/test/base/testing_profile.h" +#include "chrome/test/base/testing_profile_manager.h" +#include "components/prefs/pref_service.h" +#include "components/signin/core/browser/signin_error_controller.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "components/signin/public/identity_manager/identity_test_utils.h" +#include "components/sync_preferences/pref_service_syncable.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gtest/include/gtest/gtest.h" + +static const char kTestEmail[] = "testuser@test.com"; +static const char16_t kTestEmail16[] = u"testuser@test.com"; + +class SigninGlobalErrorTest : public testing::Test { + public: + SigninGlobalErrorTest() : + profile_manager_(TestingBrowserProcess::GetGlobal()) {} + + void SetUp() override { + ASSERT_TRUE(profile_manager_.SetUp()); + + // Create a signed-in profile. + TestingProfile::TestingFactories testing_factories = + IdentityTestEnvironmentProfileAdaptor:: + GetIdentityTestEnvironmentFactories(); + + profile_ = profile_manager_.CreateTestingProfile( + "Person 1", std::unique_ptr(), + u"Person 1", 0, std::string(), std::move(testing_factories)); + + identity_test_env_profile_adaptor_ = + std::make_unique(profile()); + + AccountInfo account_info = + identity_test_env_profile_adaptor_->identity_test_env() + ->MakePrimaryAccountAvailable(kTestEmail, + signin::ConsentLevel::kSync); + ProfileAttributesEntry* entry = + profile_manager_.profile_attributes_storage() + ->GetProfileAttributesWithPath(profile()->GetPath()); + ASSERT_NE(entry, nullptr); + + entry->SetAuthInfo(account_info.gaia, kTestEmail16, + /*is_consented_primary_account=*/true); + + global_error_ = SigninGlobalErrorFactory::GetForProfile(profile()); + error_controller_ = SigninErrorControllerFactory::GetForProfile(profile()); + } + + TestingProfile* profile() { return profile_; } + TestingProfileManager* testing_profile_manager() { + return &profile_manager_; + } + + SigninGlobalError* global_error() { return global_error_; } + SigninErrorController* error_controller() { return error_controller_; } + + void SetAuthError(GoogleServiceAuthError::State state) { + signin::IdentityTestEnvironment* identity_test_env = + identity_test_env_profile_adaptor_->identity_test_env(); + CoreAccountId primary_account_id = + identity_test_env->identity_manager()->GetPrimaryAccountId( + signin::ConsentLevel::kSync); + + signin::UpdatePersistentErrorOfRefreshTokenForAccount( + identity_test_env->identity_manager(), primary_account_id, + GoogleServiceAuthError(state)); + } + + private: + content::BrowserTaskEnvironment task_environment_; + TestingProfileManager profile_manager_; + raw_ptr profile_; + + std::unique_ptr + identity_test_env_profile_adaptor_; + + raw_ptr global_error_; + raw_ptr error_controller_; +}; + +TEST_F(SigninGlobalErrorTest, Basic) { + ASSERT_FALSE(global_error()->HasMenuItem()); + + SetAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); + EXPECT_TRUE(global_error()->HasMenuItem()); + + SetAuthError(GoogleServiceAuthError::NONE); + EXPECT_FALSE(global_error()->HasMenuItem()); +} + +// Verify that SigninGlobalError ignores certain errors. +TEST_F(SigninGlobalErrorTest, AuthStatusEnumerateAllErrors) { + typedef struct { + GoogleServiceAuthError::State error_state; + bool is_error; + } ErrorTableEntry; + + ErrorTableEntry table[] = { + {GoogleServiceAuthError::NONE, false}, + {GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS, true}, + {GoogleServiceAuthError::USER_NOT_SIGNED_UP, true}, + {GoogleServiceAuthError::CONNECTION_FAILED, false}, + {GoogleServiceAuthError::SERVICE_UNAVAILABLE, false}, + {GoogleServiceAuthError::REQUEST_CANCELED, false}, + {GoogleServiceAuthError::UNEXPECTED_SERVICE_RESPONSE, true}, + {GoogleServiceAuthError::SERVICE_ERROR, true}, + }; + static_assert( + base::size(table) == GoogleServiceAuthError::NUM_STATES - + GoogleServiceAuthError::kDeprecatedStateCount, + "table size should match number of auth error types"); + + // Mark the profile with an active timestamp so profile_metrics logs it. + testing_profile_manager()->UpdateLastUser(profile()); + + for (ErrorTableEntry entry : table) { + SetAuthError(GoogleServiceAuthError::NONE); + + base::HistogramTester histogram_tester; + SetAuthError(entry.error_state); + + EXPECT_EQ(global_error()->HasMenuItem(), entry.is_error); + EXPECT_EQ(global_error()->MenuItemLabel().empty(), !entry.is_error); + EXPECT_EQ(global_error()->GetBubbleViewMessages().empty(), !entry.is_error); + EXPECT_FALSE(global_error()->GetBubbleViewTitle().empty()); + EXPECT_FALSE(global_error()->GetBubbleViewAcceptButtonLabel().empty()); + EXPECT_TRUE(global_error()->GetBubbleViewCancelButtonLabel().empty()); + + ProfileMetrics::LogNumberOfProfiles(&testing_profile_manager() + ->profile_manager() + ->GetProfileAttributesStorage()); + + if (entry.is_error) { + histogram_tester.ExpectBucketCount("Signin.AuthError", entry.error_state, + 1); + } + } +} diff --git a/chromium/chrome/browser/signin/signin_manager.cc b/chromium/chrome/browser/signin/signin_manager.cc new file mode 100644 index 00000000000..9cc73e0e49e --- /dev/null +++ b/chromium/chrome/browser/signin/signin_manager.cc @@ -0,0 +1,200 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/signin_manager.h" + +#include "components/prefs/pref_service.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/primary_account_mutator.h" + +SigninManager::SigninManager(PrefService* prefs, + signin::IdentityManager* identity_manager) + : prefs_(prefs), identity_manager_(identity_manager) { + signin_allowed_.Init( + prefs::kSigninAllowed, prefs_, + base::BindRepeating(&SigninManager::OnSigninAllowedPrefChanged, + base::Unretained(this))); + + UpdateUnconsentedPrimaryAccount(); + identity_manager_->AddObserver(this); +} + +SigninManager::~SigninManager() { + identity_manager_->RemoveObserver(this); +} + +void SigninManager::UpdateUnconsentedPrimaryAccount() { + // Only update the unconsented primary account only after accounts are loaded. + if (!identity_manager_->AreRefreshTokensLoaded()) { + return; + } + + absl::optional account = + ComputeUnconsentedPrimaryAccountInfo(); + + DCHECK(!account || !account->IsEmpty()); + if (account) { + if (identity_manager_->GetPrimaryAccountInfo( + signin::ConsentLevel::kSignin) != account) { + DCHECK( + !identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync)); + identity_manager_->GetPrimaryAccountMutator()->SetPrimaryAccount( + account->account_id, signin::ConsentLevel::kSignin); + } + } else if (identity_manager_->HasPrimaryAccount( + signin::ConsentLevel::kSignin)) { + DCHECK(!identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync)); + identity_manager_->GetPrimaryAccountMutator()->ClearPrimaryAccount( + signin_metrics::USER_DELETED_ACCOUNT_COOKIES, + signin_metrics::SignoutDelete::kIgnoreMetric); + } +} + +absl::optional +SigninManager::ComputeUnconsentedPrimaryAccountInfo() const { + DCHECK(identity_manager_->AreRefreshTokensLoaded()); + + // UPA is equal to the primary account with sync consent if it exists. + if (identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync)) { + return identity_manager_->GetPrimaryAccountInfo( + signin::ConsentLevel::kSync); + } + + // Clearing the primary sync account when sign-in is not allowed is handled + // by PrimaryAccountPolicyManager. That flow is extremely hard to follow + // especially for the case when the user is syncing with a managed account + // as in that case the whole profile needs to be deleted. + // + // It was considered simpler to keep the logic to update the unconsented + // primary account in a single place. + if (!signin_allowed_.GetValue()) + return absl::nullopt; + + signin::AccountsInCookieJarInfo cookie_info = + identity_manager_->GetAccountsInCookieJar(); + + std::vector cookie_accounts = + cookie_info.signed_in_accounts; + + // Fresh cookies and loaded tokens are needed to compute the UPA. + if (cookie_info.accounts_are_fresh) { + // Cookies are fresh and tokens are loaded, UPA is the first account + // in cookies if it exists and has a refresh token. + if (cookie_accounts.empty()) { + // Cookies are empty, the UPA is empty. + return absl::nullopt; + } + + AccountInfo account_info = + identity_manager_->FindExtendedAccountInfoByAccountId( + cookie_accounts[0].id); + + // Verify the first account in cookies has a refresh token that is valid. + bool error_state = + account_info.IsEmpty() || + identity_manager_->HasAccountWithRefreshTokenInPersistentErrorState( + account_info.account_id); + + return error_state ? absl::nullopt + : absl::make_optional(account_info); + } + + if (!identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin)) + return absl::nullopt; + + // If cookies or tokens are not loaded, it is not possible to fully compute + // the unconsented primary account. However, if the current unconsented + // primary account is no longer valid, it has to be removed. + CoreAccountId current_account = + identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSignin); + + if (!identity_manager_->HasAccountWithRefreshToken(current_account)) { + // Tokens are loaded, but the current UPA doesn't have a refresh token. + // Clear the current UPA. + return absl::nullopt; + } + + if (cookie_info.accounts_are_fresh) { + if (cookie_accounts.empty() || cookie_accounts[0].id != current_account) { + // The current UPA is not the first in fresh cookies. It needs to be + // cleared. + return absl::nullopt; + } + } + + // No indication that the current UPA is invalid, return current UPA. + return identity_manager_->GetPrimaryAccountInfo( + signin::ConsentLevel::kSignin); +} + +// signin::IdentityManager::Observer implementation. +void SigninManager::OnPrimaryAccountChanged( + const signin::PrimaryAccountChangeEvent& event_details) { + // This is needed for the case where the user chooses to start syncing + // with an account that is different from the unconsented primary account + // (not the first in cookies) but then cancels. In that case, the tokens stay + // the same. In all the other cases, either the token will be revoked which + // will trigger an update for the unconsented primary account or the + // primary account stays the same but the sync consent is revoked. + if (event_details.GetEventTypeFor(signin::ConsentLevel::kSync) != + signin::PrimaryAccountChangeEvent::Type::kCleared) { + return; + } + + // It is important to update the primary account after all observers process + // the current OnPrimaryAccountChanged() as all observers should see the same + // value for the unconsented primary account. Schedule the potential update + // on the next run loop. + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(&SigninManager::UpdateUnconsentedPrimaryAccount, + weak_ptr_factory_.GetWeakPtr())); +} + +void SigninManager::OnRefreshTokenUpdatedForAccount( + const CoreAccountInfo& account_info) { + UpdateUnconsentedPrimaryAccount(); +} + +void SigninManager::OnRefreshTokenRemovedForAccount( + const CoreAccountId& account_id) { + UpdateUnconsentedPrimaryAccount(); +} + +void SigninManager::OnRefreshTokensLoaded() { + UpdateUnconsentedPrimaryAccount(); +} + +void SigninManager::OnAccountsInCookieUpdated( + const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info, + const GoogleServiceAuthError& error) { + UpdateUnconsentedPrimaryAccount(); +} + +void SigninManager::OnAccountsCookieDeletedByUserAction() { + UpdateUnconsentedPrimaryAccount(); +} + +void SigninManager::OnErrorStateOfRefreshTokenUpdatedForAccount( + const CoreAccountInfo& account_info, + const GoogleServiceAuthError& error) { + CoreAccountInfo current_account = + identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin); + + bool should_update = false; + if (error == GoogleServiceAuthError::AuthErrorNone()) { + should_update = current_account.IsEmpty(); + } else { + // In error state, update if the account in error is the current UPA. + should_update = (account_info == current_account); + } + + if (should_update) + UpdateUnconsentedPrimaryAccount(); +} + +void SigninManager::OnSigninAllowedPrefChanged() { + UpdateUnconsentedPrimaryAccount(); +} diff --git a/chromium/chrome/browser/signin/signin_manager.h b/chromium/chrome/browser/signin/signin_manager.h new file mode 100644 index 00000000000..2feaf8dc737 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_manager.h @@ -0,0 +1,71 @@ +// Copyright 2020 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 CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_H_ + +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "components/keyed_service/core/keyed_service.h" +#include "components/prefs/pref_member.h" +#include "components/signin/public/identity_manager/identity_manager.h" + +class PrefService; + +class SigninManager : public KeyedService, + public signin::IdentityManager::Observer { + public: + SigninManager(PrefService* prefs, signin::IdentityManager* identity_manger); + SigninManager(const SigninManager&) = delete; + SigninManager& operator=(const SigninManager&) = delete; + + ~SigninManager() override; + + private: + // Updates the cached version of unconsented primary account and notifies the + // observers if there is any change. + void UpdateUnconsentedPrimaryAccount(); + + // Computes and returns the unconsented primary account (UPA). + // - If a primary account with sync consent exists, the UPA is equal to it. + // - The UPA is the first account in cookies and must have a refresh token. + // For the UPA to be computed, it needs fresh cookies and tokens to be loaded. + // - If tokens are not loaded or cookies are not fresh, the UPA can't be + // computed but if one already exists it might be invalid. That can happen if + // cookies are fresh but are empty or the first account is different than the + // current UPA, the other cases are if tokens are not loaded but the current + // UPA's refresh token has been rekoved or tokens are loaded but the current + // UPA does not have a refresh token. If the UPA is invalid, it needs to be + // cleared, |absl::nullopt| is returned. If it is still valid, returns the + // valid UPA. + absl::optional ComputeUnconsentedPrimaryAccountInfo() const; + + // signin::IdentityManager::Observer implementation. + void OnPrimaryAccountChanged( + const signin::PrimaryAccountChangeEvent& event_details) override; + void OnRefreshTokenUpdatedForAccount( + const CoreAccountInfo& account_info) override; + void OnRefreshTokenRemovedForAccount( + const CoreAccountId& account_id) override; + void OnRefreshTokensLoaded() override; + void OnAccountsInCookieUpdated( + const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info, + const GoogleServiceAuthError& error) override; + void OnAccountsCookieDeletedByUserAction() override; + void OnErrorStateOfRefreshTokenUpdatedForAccount( + const CoreAccountInfo& account_info, + const GoogleServiceAuthError& error) override; + + void OnSigninAllowedPrefChanged(); + + raw_ptr prefs_; + raw_ptr identity_manager_; + + // Helper object to listen for changes to the signin allowed preference. + BooleanPrefMember signin_allowed_; + + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_H_ diff --git a/chromium/chrome/browser/signin/signin_manager_android_factory.cc b/chromium/chrome/browser/signin/signin_manager_android_factory.cc new file mode 100644 index 00000000000..08d545c346a --- /dev/null +++ b/chromium/chrome/browser/signin/signin_manager_android_factory.cc @@ -0,0 +1,41 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/signin_manager_android_factory.h" + +#include "chrome/browser/android/signin/signin_manager_android.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" + +SigninManagerAndroidFactory::SigninManagerAndroidFactory() + : BrowserContextKeyedServiceFactory( + "SigninManagerAndroid", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(IdentityManagerFactory::GetInstance()); +} + +SigninManagerAndroidFactory::~SigninManagerAndroidFactory() {} + +// static +base::android::ScopedJavaLocalRef +SigninManagerAndroidFactory::GetJavaObjectForProfile(Profile* profile) { + return static_cast( + GetInstance()->GetServiceForBrowserContext(profile, true)) + ->GetJavaObject(); +} + +// static +SigninManagerAndroidFactory* SigninManagerAndroidFactory::GetInstance() { + static base::NoDestructor instance; + return instance.get(); +} + +KeyedService* SigninManagerAndroidFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + auto* identity_manager = IdentityManagerFactory::GetForProfile(profile); + + return new SigninManagerAndroid(profile, identity_manager); +} diff --git a/chromium/chrome/browser/signin/signin_manager_android_factory.h b/chromium/chrome/browser/signin/signin_manager_android_factory.h new file mode 100644 index 00000000000..5eabc40186a --- /dev/null +++ b/chromium/chrome/browser/signin/signin_manager_android_factory.h @@ -0,0 +1,32 @@ +// Copyright 2019 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 CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_ANDROID_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_ANDROID_FACTORY_H_ + +#include "base/android/scoped_java_ref.h" +#include "base/no_destructor.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +class Profile; + +class SigninManagerAndroidFactory : public BrowserContextKeyedServiceFactory { + public: + static base::android::ScopedJavaLocalRef GetJavaObjectForProfile( + Profile* profile); + + // Returns an instance of the SigninManagerAndroidFactory singleton. + static SigninManagerAndroidFactory* GetInstance(); + + private: + friend class base::NoDestructor; + SigninManagerAndroidFactory(); + + ~SigninManagerAndroidFactory() override; + + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* context) const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_ANDROID_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/signin_manager_factory.cc b/chromium/chrome/browser/signin/signin_manager_factory.cc new file mode 100644 index 00000000000..788dbee7f54 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_manager_factory.cc @@ -0,0 +1,48 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/signin_manager_factory.h" + +#include "base/logging.h" +#include "build/build_config.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/signin_features.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" + +// static +SigninManagerFactory* SigninManagerFactory::GetInstance() { + return base::Singleton::get(); +} + +// static +SigninManager* SigninManagerFactory::GetForProfile(Profile* profile) { + DCHECK(profile); + return static_cast( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +SigninManagerFactory::SigninManagerFactory() + : BrowserContextKeyedServiceFactory( + "SigninManager", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(IdentityManagerFactory::GetInstance()); +} + +SigninManagerFactory::~SigninManagerFactory() = default; + +KeyedService* SigninManagerFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + return new SigninManager(profile->GetPrefs(), + IdentityManagerFactory::GetForProfile(profile)); +} + +bool SigninManagerFactory::ServiceIsCreatedWithBrowserContext() const { + return true; +} + +bool SigninManagerFactory::ServiceIsNULLWhileTesting() const { + return true; +} diff --git a/chromium/chrome/browser/signin/signin_manager_factory.h b/chromium/chrome/browser/signin/signin_manager_factory.h new file mode 100644 index 00000000000..d0ca640e22d --- /dev/null +++ b/chromium/chrome/browser/signin/signin_manager_factory.h @@ -0,0 +1,33 @@ +// Copyright 2020 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 CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/signin_manager.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +class SigninManagerFactory : public BrowserContextKeyedServiceFactory { + public: + // Returns an instance of the factory singleton. + static SigninManagerFactory* GetInstance(); + + static SigninManager* GetForProfile(Profile* profile); + + private: + friend struct base::DefaultSingletonTraits; + + SigninManagerFactory(); + ~SigninManagerFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* context) const override; + bool ServiceIsCreatedWithBrowserContext() const override; + bool ServiceIsNULLWhileTesting() const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/signin_manager_unittest.cc b/chromium/chrome/browser/signin/signin_manager_unittest.cc new file mode 100644 index 00000000000..e2bb33af9f0 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_manager_unittest.cc @@ -0,0 +1,421 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#include "chrome/browser/signin/signin_manager.h" + +#include "base/memory/raw_ptr.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "components/signin/public/identity_manager/identity_test_utils.h" +#include "components/sync_preferences/testing_pref_service_syncable.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ::testing::_; +using ::testing::Mock; + +namespace signin { +namespace { +const char kTestEmail[] = "me@gmail.com"; +const char kTestEmail2[] = "me2@gmail.com"; + +class FakeIdentityManagerObserver : public IdentityManager::Observer { + public: + explicit FakeIdentityManagerObserver(IdentityManager* identity_manager) + : identity_manager_(identity_manager) {} + ~FakeIdentityManagerObserver() override = default; + + void OnPrimaryAccountChanged( + const PrimaryAccountChangeEvent& event) override { + auto current_state = event.GetCurrentState(); + EXPECT_EQ( + current_state.primary_account, + identity_manager_->GetPrimaryAccountInfo(current_state.consent_level)); + events_.push_back(event); + } + + const std::vector& events() const { + return events_; + } + + void Reset() { events_.clear(); } + + private: + raw_ptr identity_manager_; + std::vector events_; +}; +} // namespace + +class SigninManagerTest : public testing::Test { + public: + SigninManagerTest() + : identity_test_env_(/*test_url_loader_factory=*/nullptr, + /*pref_service=*/&prefs_, + signin::AccountConsistencyMethod::kDice, + /*test_signin_client=*/nullptr), + observer_(identity_test_env_.identity_manager()) {} + + SigninManagerTest(const SigninManagerTest&) = delete; + SigninManagerTest& operator=(const SigninManagerTest&) = delete; + + void SetUp() override { + testing::Test::SetUp(); + RecreateSigninManager(); + identity_manager()->AddObserver(&observer_); + } + + void TearDown() override { identity_manager()->RemoveObserver(&observer_); } + + void RecreateSigninManager() { + signin_manger_ = + std::make_unique(&prefs_, identity_manager()); + } + + AccountInfo GetAccountInfo(const std::string& email) { + AccountInfo account_info; + account_info.gaia = GetTestGaiaIdForEmail(email); + account_info.account_id = + identity_manager()->PickAccountIdForAccount(account_info.gaia, email); + account_info.email = email; + return account_info; + } + + void ExpectUnconsentedPrimaryAccountSetEvent( + const CoreAccountInfo& expected_primary_account) { + EXPECT_EQ(1U, observer().events().size()); + auto event = observer().events()[0]; + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kSet, + event.GetEventTypeFor(ConsentLevel::kSignin)); + EXPECT_TRUE(event.GetPreviousState().primary_account.IsEmpty()); + EXPECT_EQ(expected_primary_account, + event.GetCurrentState().primary_account); + observer().Reset(); + } + + void ExpectUnconsentedPrimaryAccountClearedEvent( + const CoreAccountInfo& expected_cleared_account) { + EXPECT_EQ(1U, observer().events().size()); + auto event = observer().events()[0]; + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared, + event.GetEventTypeFor(ConsentLevel::kSignin)); + EXPECT_EQ(expected_cleared_account, + event.GetPreviousState().primary_account); + EXPECT_TRUE(event.GetCurrentState().primary_account.IsEmpty()); + observer().Reset(); + } + + void ExpectSyncPrimaryAccountSetEvent( + const CoreAccountInfo& expected_primary_account) { + EXPECT_EQ(1U, observer().events().size()); + auto event = observer().events()[0]; + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kSet, + event.GetEventTypeFor(ConsentLevel::kSignin)); + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kSet, + event.GetEventTypeFor(ConsentLevel::kSync)); + EXPECT_TRUE(event.GetPreviousState().primary_account.IsEmpty()); + EXPECT_EQ(expected_primary_account, + event.GetCurrentState().primary_account); + observer().Reset(); + } + + IdentityManager* identity_manager() { + return identity_test_env_.identity_manager(); + } + + IdentityTestEnvironment* identity_test_env() { return &identity_test_env_; } + + AccountInfo MakeAccountAvailableWithCookies(const std::string& email) { + AccountInfo account = GetAccountInfo(kTestEmail); + identity_test_env_.MakeAccountAvailableWithCookies(account.email, + account.gaia); + EXPECT_FALSE(account.IsEmpty()); + EXPECT_TRUE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin)); + EXPECT_EQ(account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); + EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSync)); + return account; + } + + AccountInfo MakeSyncAccountAvailableWithCookies(const std::string& email) { + AccountInfo account = identity_test_env_.MakePrimaryAccountAvailable( + email, signin::ConsentLevel::kSync); + identity_test_env_.SetCookieAccounts({{account.email, account.gaia}}); + EXPECT_EQ(account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); + EXPECT_EQ(account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSync)); + EXPECT_TRUE(identity_manager()->HasPrimaryAccountWithRefreshToken( + signin::ConsentLevel::kSync)); + return account; + } + + FakeIdentityManagerObserver& observer() { return observer_; } + + sync_preferences::TestingPrefServiceSyncable prefs_; + content::BrowserTaskEnvironment task_environment_; + IdentityTestEnvironment identity_test_env_; + std::unique_ptr signin_manger_; + FakeIdentityManagerObserver observer_; +}; + +TEST_F( + SigninManagerTest, + UnconsentedPrimaryAccountUpdatedOnItsAccountRefreshTokenUpdateWithValidTokenWhenNoSyncConsent) { + // Add an unconsented primary account, incl. proper cookies. + AccountInfo account = MakeAccountAvailableWithCookies(kTestEmail); + ExpectUnconsentedPrimaryAccountSetEvent(account); + EXPECT_EQ(account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); +} + +TEST_F( + SigninManagerTest, + UnconsentedPrimaryAccountUpdatedOnItsAccountRefreshTokenUpdateWithInvalidTokenWhenNoSyncConsent) { + // Prerequisite: add an unconsented primary account, incl. proper cookies. + AccountInfo account = MakeAccountAvailableWithCookies(kTestEmail); + ExpectUnconsentedPrimaryAccountSetEvent(account); + + // Invalid token. + SetInvalidRefreshTokenForAccount(identity_manager(), account.account_id); + ExpectUnconsentedPrimaryAccountClearedEvent(account); + EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin)); + + // Update with a valid token. + SetRefreshTokenForAccount(identity_manager(), account.account_id, ""); + ExpectUnconsentedPrimaryAccountSetEvent(account); + EXPECT_EQ(identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin), + account); +} + +TEST_F( + SigninManagerTest, + UnconsentedPrimaryAccountRemovedOnItsAccountRefreshTokenRemovalWhenNoSyncConsent) { + // Prerequisite: Add an unconsented primary account, incl. proper cookies. + AccountInfo account = MakeAccountAvailableWithCookies(kTestEmail); + ExpectUnconsentedPrimaryAccountSetEvent(account); + + // With no refresh token, there is no unconsented primary account any more. + identity_test_env()->RemoveRefreshTokenForAccount(account.account_id); + ExpectUnconsentedPrimaryAccountClearedEvent(account); + EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin)); +} + +TEST_F(SigninManagerTest, UnconsentedPrimaryAccountNotChangedOnSignout) { + // Set a primary account at sync consent level. + AccountInfo account = MakeSyncAccountAvailableWithCookies(kTestEmail); + EXPECT_EQ(account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); + EXPECT_EQ(account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSync)); + EXPECT_TRUE(identity_manager()->HasPrimaryAccountWithRefreshToken( + signin::ConsentLevel::kSync)); + + // Verify the primary account changed event. + ExpectSyncPrimaryAccountSetEvent(account); + + // Tests that sync primary account is cleared, but unconsented account is not. + identity_test_env()->RevokeSyncConsent(); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); + EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSync)); + + EXPECT_EQ(1U, observer().events().size()); + auto event = observer().events()[0]; + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kNone, + event.GetEventTypeFor(ConsentLevel::kSignin)); + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared, + event.GetEventTypeFor(ConsentLevel::kSync)); + EXPECT_EQ(account, event.GetPreviousState().primary_account); + EXPECT_EQ(account, event.GetCurrentState().primary_account); +} + +TEST_F(SigninManagerTest, + UnconsentedPrimaryAccountTokenRevokedWithStaleCookies) { + // Prerequisite: add an unconsented primary account, incl. proper cookies. + AccountInfo account = MakeAccountAvailableWithCookies(kTestEmail); + ExpectUnconsentedPrimaryAccountSetEvent(account); + + // Make the cookies stale and remove the account. + // Removing the refresh token for the unconsented primary account is + // sufficient to clear it. + identity_test_env()->SetFreshnessOfAccountsInGaiaCookie(false); + identity_test_env()->RemoveRefreshTokenForAccount(account.account_id); + ASSERT_FALSE(identity_manager()->GetAccountsInCookieJar().accounts_are_fresh); + + // Unconsented account was removed. + EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin)); + ExpectUnconsentedPrimaryAccountClearedEvent(account); +} + +TEST_F(SigninManagerTest, + UnconsentedPrimaryAccountTokenRevokedWithStaleCookiesMultipleAccounts) { + // Add two accounts with cookies. + AccountInfo main_account = + identity_test_env()->MakeAccountAvailable(kTestEmail); + AccountInfo secondary_account = + identity_test_env()->MakeAccountAvailable(kTestEmail2); + identity_test_env()->SetCookieAccounts( + {{main_account.email, main_account.gaia}, + {secondary_account.email, secondary_account.gaia}}); + + EXPECT_TRUE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin)); + EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSync)); + EXPECT_EQ(main_account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); + ExpectUnconsentedPrimaryAccountSetEvent(main_account); + + // Make the cookies stale and remove the main account. + identity_test_env()->SetFreshnessOfAccountsInGaiaCookie(false); + identity_test_env()->RemoveRefreshTokenForAccount(main_account.account_id); + ASSERT_FALSE(identity_manager()->GetAccountsInCookieJar().accounts_are_fresh); + + // Unconsented account was removed. + EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin)); + ExpectUnconsentedPrimaryAccountClearedEvent(main_account); +} + +TEST_F(SigninManagerTest, UnconsentedPrimaryAccountDuringLoad) { + // Pre-requisite: Add two accounts with cookies. + AccountInfo main_account = + identity_test_env()->MakeAccountAvailable(kTestEmail); + AccountInfo secondary_account = + identity_test_env()->MakeAccountAvailable(kTestEmail2); + identity_test_env()->SetCookieAccounts( + {{main_account.email, main_account.gaia}, + {secondary_account.email, secondary_account.gaia}}); + ASSERT_EQ(main_account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); + ASSERT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSync)); + ExpectUnconsentedPrimaryAccountSetEvent(main_account); + + // Set the token service in "loading" mode. + identity_test_env()->ResetToAccountsNotYetLoadedFromDiskState(); + RecreateSigninManager(); + + // Unconsented primary account is available while tokens are not loaded. + EXPECT_EQ(main_account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); + EXPECT_TRUE(observer().events().empty()); + + // Revoking an unrelated token doesn't change the unconsented primary account. + identity_test_env()->RemoveRefreshTokenForAccount( + secondary_account.account_id); + EXPECT_EQ(main_account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); + EXPECT_TRUE(observer().events().empty()); + + // Revoking the token of the unconsented primary account while the tokens + // are still loading does not change the unconsented primary account. + identity_test_env()->RemoveRefreshTokenForAccount(main_account.account_id); + EXPECT_EQ(main_account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); + EXPECT_TRUE(observer().events().empty()); + + // Finish the token load should clear the primary account as the token of the + // primary account was revoked. + identity_test_env()->ReloadAccountsFromDisk(); + EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin)); + ExpectUnconsentedPrimaryAccountClearedEvent(main_account); +} + +TEST_F(SigninManagerTest, + UnconsentedPrimaryAccountUpdatedOnSyncConsentRevoked) { + AccountInfo first_account = + identity_test_env()->MakeAccountAvailable(kTestEmail); + AccountInfo second_account = + identity_test_env()->MakeAccountAvailable(kTestEmail2); + identity_test_env()->SetCookieAccounts( + {{first_account.email, first_account.gaia}, + {second_account.email, second_account.gaia}}); + ASSERT_EQ(first_account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); + ExpectUnconsentedPrimaryAccountSetEvent(first_account); + + // Set the sync primary account to the second account in cookies. + // The unconsented primary account should be updated. + identity_test_env()->SetPrimaryAccount(second_account.email, + signin::ConsentLevel::kSync); + EXPECT_EQ(second_account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSync)); + EXPECT_EQ(1U, observer().events().size()); + auto event = observer().events()[0]; + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kSet, + event.GetEventTypeFor(ConsentLevel::kSync)); + EXPECT_EQ(first_account, event.GetPreviousState().primary_account); + EXPECT_EQ(second_account, event.GetCurrentState().primary_account); + observer().Reset(); + + // Clear primary account but do not delete the account. The unconsented + // primary account should be updated to be the first account in cookies. + identity_test_env()->RevokeSyncConsent(); + base::RunLoop().RunUntilIdle(); + + // Primary account is cleared, but unconsented account is not. + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); + EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSync)); + EXPECT_TRUE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin)); + EXPECT_EQ(first_account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); + + EXPECT_EQ(2U, observer().events().size()); + event = observer().events()[0]; + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared, + event.GetEventTypeFor(ConsentLevel::kSync)); + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kNone, + event.GetEventTypeFor(ConsentLevel::kSignin)); + EXPECT_EQ(second_account, event.GetPreviousState().primary_account); + EXPECT_EQ(second_account, event.GetCurrentState().primary_account); + + event = observer().events()[1]; + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kNone, + event.GetEventTypeFor(ConsentLevel::kSync)); + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kSet, + event.GetEventTypeFor(ConsentLevel::kSignin)); + EXPECT_EQ(second_account, event.GetPreviousState().primary_account); + EXPECT_EQ(first_account, event.GetCurrentState().primary_account); +} + +TEST_F(SigninManagerTest, ClearPrimaryAccountAndSignOut) { + AccountInfo account = MakeSyncAccountAvailableWithCookies(kTestEmail); + ExpectSyncPrimaryAccountSetEvent(account); + + identity_test_env()->ClearPrimaryAccount(); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(1U, observer().events().size()); + auto event = observer().events()[0]; + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared, + event.GetEventTypeFor(ConsentLevel::kSignin)); + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared, + event.GetEventTypeFor(ConsentLevel::kSync)); + EXPECT_EQ(account, event.GetPreviousState().primary_account); + EXPECT_TRUE(event.GetCurrentState().primary_account.IsEmpty()); +} + +TEST_F(SigninManagerTest, + UnconsentedPrimaryAccountClearedWhenSigninDisallowed) { + // Prerequisite: add an unconsented primary account. + AccountInfo account = MakeAccountAvailableWithCookies(kTestEmail); + ExpectUnconsentedPrimaryAccountSetEvent(account); + + prefs_.SetBoolean(prefs::kSigninAllowed, false); + base::RunLoop().RunUntilIdle(); + + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin)); + + EXPECT_EQ(1U, observer().events().size()); + auto event = observer().events()[0]; + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared, + event.GetEventTypeFor(ConsentLevel::kSignin)); + EXPECT_EQ(account, event.GetPreviousState().primary_account); + EXPECT_TRUE(event.GetCurrentState().primary_account.IsEmpty()); +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/signin_profile_attributes_updater.cc b/chromium/chrome/browser/signin/signin_profile_attributes_updater.cc new file mode 100644 index 00000000000..1f0c6a9ae36 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_profile_attributes_updater.cc @@ -0,0 +1,72 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/signin_profile_attributes_updater.h" + +#include + +#include "base/strings/utf_string_conversions.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/signin/signin_util.h" +#include "chrome/common/pref_names.h" +#include "components/signin/public/base/consent_level.h" +#include "components/signin/public/identity_manager/account_info.h" +#include "google_apis/gaia/gaia_auth_util.h" + +SigninProfileAttributesUpdater::SigninProfileAttributesUpdater( + signin::IdentityManager* identity_manager, + ProfileAttributesStorage* profile_attributes_storage, + const base::FilePath& profile_path, + PrefService* prefs) + : identity_manager_(identity_manager), + profile_attributes_storage_(profile_attributes_storage), + profile_path_(profile_path), + prefs_(prefs) { + DCHECK(identity_manager_); + DCHECK(profile_attributes_storage_); + identity_manager_observation_.Observe(identity_manager_.get()); + + UpdateProfileAttributes(); +} + +SigninProfileAttributesUpdater::~SigninProfileAttributesUpdater() = default; + +void SigninProfileAttributesUpdater::Shutdown() { + identity_manager_observation_.Reset(); +} + +void SigninProfileAttributesUpdater::UpdateProfileAttributes() { + ProfileAttributesEntry* entry = + profile_attributes_storage_->GetProfileAttributesWithPath(profile_path_); + if (!entry) { + return; + } + + CoreAccountInfo account_info = + identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin); + + bool clear_profile = account_info.IsEmpty(); + + if (account_info.gaia != entry->GetGAIAId() || + !gaia::AreEmailsSame(account_info.email, + base::UTF16ToUTF8(entry->GetUserName()))) { + // Reset prefs. Note: this will also update the |ProfileAttributesEntry|. + prefs_->ClearPref(prefs::kProfileUsingDefaultAvatar); + prefs_->ClearPref(prefs::kProfileUsingGAIAAvatar); + } + + if (clear_profile) { + entry->SetAuthInfo(std::string(), std::u16string(), + /*is_consented_primary_account=*/false); + } else { + entry->SetAuthInfo( + account_info.gaia, base::UTF8ToUTF16(account_info.email), + identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync)); + } +} + +void SigninProfileAttributesUpdater::OnPrimaryAccountChanged( + const signin::PrimaryAccountChangeEvent& event) { + UpdateProfileAttributes(); +} diff --git a/chromium/chrome/browser/signin/signin_profile_attributes_updater.h b/chromium/chrome/browser/signin/signin_profile_attributes_updater.h new file mode 100644 index 00000000000..8e189e8e370 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_profile_attributes_updater.h @@ -0,0 +1,56 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_H_ + +#include "base/files/file_path.h" +#include "base/memory/raw_ptr.h" +#include "base/scoped_observation.h" +#include "components/keyed_service/core/keyed_service.h" +#include "components/prefs/pref_service.h" +#include "components/signin/public/identity_manager/identity_manager.h" + +class ProfileAttributesStorage; + +// This class listens to various signin events and updates the signin-related +// fields of ProfileAttributes. +class SigninProfileAttributesUpdater + : public KeyedService, + public signin::IdentityManager::Observer { + public: + SigninProfileAttributesUpdater( + signin::IdentityManager* identity_manager, + ProfileAttributesStorage* profile_attributes_storage, + const base::FilePath& profile_path, + PrefService* prefs); + + SigninProfileAttributesUpdater(const SigninProfileAttributesUpdater&) = + delete; + SigninProfileAttributesUpdater& operator=( + const SigninProfileAttributesUpdater&) = delete; + + ~SigninProfileAttributesUpdater() override; + + private: + // KeyedService: + void Shutdown() override; + + // Updates the profile attributes on signin and signout events. + void UpdateProfileAttributes(); + + // IdentityManager::Observer: + void OnPrimaryAccountChanged( + const signin::PrimaryAccountChangeEvent& event) override; + + raw_ptr identity_manager_; + raw_ptr profile_attributes_storage_; + const base::FilePath profile_path_; + raw_ptr prefs_; + base::ScopedObservation + identity_manager_observation_{this}; +}; + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_H_ diff --git a/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.cc b/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.cc new file mode 100644 index 00000000000..86c62a2ce6e --- /dev/null +++ b/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.cc @@ -0,0 +1,53 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/signin_profile_attributes_updater_factory.h" + +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/signin_profile_attributes_updater.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" + +// static +SigninProfileAttributesUpdater* +SigninProfileAttributesUpdaterFactory::GetForProfile(Profile* profile) { + return static_cast( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +// static +SigninProfileAttributesUpdaterFactory* +SigninProfileAttributesUpdaterFactory::GetInstance() { + return base::Singleton::get(); +} + +SigninProfileAttributesUpdaterFactory::SigninProfileAttributesUpdaterFactory() + : BrowserContextKeyedServiceFactory( + "SigninProfileAttributesUpdater", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(IdentityManagerFactory::GetInstance()); +} + +SigninProfileAttributesUpdaterFactory:: + ~SigninProfileAttributesUpdaterFactory() {} + +KeyedService* SigninProfileAttributesUpdaterFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + // Some tests don't have a ProfileManager, disable this service. + if (!g_browser_process->profile_manager()) + return nullptr; + + return new SigninProfileAttributesUpdater( + IdentityManagerFactory::GetForProfile(profile), + &g_browser_process->profile_manager()->GetProfileAttributesStorage(), + profile->GetPath(), profile->GetPrefs()); +} + +bool SigninProfileAttributesUpdaterFactory::ServiceIsCreatedWithBrowserContext() + const { + return true; +} diff --git a/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.h b/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.h new file mode 100644 index 00000000000..78f990abaf8 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.h @@ -0,0 +1,42 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +class Profile; +class SigninProfileAttributesUpdater; + +class SigninProfileAttributesUpdaterFactory + : public BrowserContextKeyedServiceFactory { + public: + // Returns nullptr if this profile cannot have a + // SigninProfileAttributesUpdater (for example, if |profile| is incognito). + static SigninProfileAttributesUpdater* GetForProfile(Profile* profile); + + // Returns an instance of the factory singleton. + static SigninProfileAttributesUpdaterFactory* GetInstance(); + + SigninProfileAttributesUpdaterFactory( + const SigninProfileAttributesUpdaterFactory&) = delete; + SigninProfileAttributesUpdaterFactory& operator=( + const SigninProfileAttributesUpdaterFactory&) = delete; + + private: + friend struct base::DefaultSingletonTraits< + SigninProfileAttributesUpdaterFactory>; + + SigninProfileAttributesUpdaterFactory(); + ~SigninProfileAttributesUpdaterFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const override; + bool ServiceIsCreatedWithBrowserContext() const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/signin_profile_attributes_updater_unittest.cc b/chromium/chrome/browser/signin/signin_profile_attributes_updater_unittest.cc new file mode 100644 index 00000000000..01430a350b0 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_profile_attributes_updater_unittest.cc @@ -0,0 +1,224 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/signin_profile_attributes_updater.h" + +#include "base/bind.h" +#include "base/files/file_path.h" +#include "base/memory/raw_ptr.h" +#include "base/strings/utf_string_conversions.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/profiles/profile_attributes_entry.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/signin/signin_util.h" +#include "chrome/common/pref_names.h" +#include "chrome/test/base/testing_browser_process.h" +#include "chrome/test/base/testing_profile.h" +#include "chrome/test/base/testing_profile_manager.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "components/signin/public/identity_manager/identity_test_utils.h" +#include "components/sync_preferences/pref_service_syncable.h" +#include "content/public/test/browser_task_environment.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { +#if !BUILDFLAG(IS_CHROMEOS_ASH) +const char kEmail[] = "example@email.com"; + +void CheckProfilePrefsReset(PrefService* pref_service, + bool expected_using_default_name) { + EXPECT_TRUE(pref_service->GetBoolean(prefs::kProfileUsingDefaultAvatar)); + EXPECT_FALSE(pref_service->GetBoolean(prefs::kProfileUsingGAIAAvatar)); + EXPECT_EQ(expected_using_default_name, + pref_service->GetBoolean(prefs::kProfileUsingDefaultName)); +} + +void CheckProfilePrefsSet(PrefService* pref_service, + bool expected_is_using_default_name) { + EXPECT_FALSE(pref_service->GetBoolean(prefs::kProfileUsingDefaultAvatar)); + EXPECT_TRUE(pref_service->GetBoolean(prefs::kProfileUsingGAIAAvatar)); + EXPECT_EQ(expected_is_using_default_name, + pref_service->GetBoolean(prefs::kProfileUsingDefaultName)); +} + +// Set the prefs to nondefault values. +void SetProfilePrefs(PrefService* pref_service) { + pref_service->SetBoolean(prefs::kProfileUsingDefaultAvatar, false); + pref_service->SetBoolean(prefs::kProfileUsingGAIAAvatar, true); + pref_service->SetBoolean(prefs::kProfileUsingDefaultName, false); + + CheckProfilePrefsSet(pref_service, false); +} +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) +} // namespace + +class SigninProfileAttributesUpdaterTest : public testing::Test { + public: + SigninProfileAttributesUpdaterTest() + : profile_manager_(TestingBrowserProcess::GetGlobal()) {} + + // Recreates |signin_profile_attributes_updater_|. Useful for tests that want + // to set up the updater with specific preconditions. + void RecreateSigninProfileAttributesUpdater() { + signin_profile_attributes_updater_ = + std::make_unique( + identity_test_env_.identity_manager(), + profile_manager_.profile_attributes_storage(), profile_->GetPath(), + profile_->GetPrefs()); + } + + void SetUp() override { + testing::Test::SetUp(); + + ASSERT_TRUE(profile_manager_.SetUp()); + std::string name = "profile_name"; + profile_ = profile_manager_.CreateTestingProfile( + name, /*prefs=*/nullptr, base::UTF8ToUTF16(name), 0, std::string(), + TestingProfile::TestingFactories()); + + RecreateSigninProfileAttributesUpdater(); + } + + content::BrowserTaskEnvironment task_environment_; + TestingProfileManager profile_manager_; + raw_ptr profile_; + signin::IdentityTestEnvironment identity_test_env_; + std::unique_ptr + signin_profile_attributes_updater_; +}; + +#if !BUILDFLAG(IS_CHROMEOS_ASH) +// Tests that the browser state info is updated on signin and signout. +// ChromeOS does not support signout. +TEST_F(SigninProfileAttributesUpdaterTest, SigninSignout) { + ProfileAttributesEntry* entry = + profile_manager_.profile_attributes_storage() + ->GetProfileAttributesWithPath(profile_->GetPath()); + ASSERT_NE(entry, nullptr); + ASSERT_EQ(entry->GetSigninState(), SigninState::kNotSignedIn); + EXPECT_FALSE(entry->IsSigninRequired()); + + // Signin. + identity_test_env_.MakePrimaryAccountAvailable(kEmail, + signin::ConsentLevel::kSync); + EXPECT_TRUE(entry->IsAuthenticated()); + EXPECT_EQ(signin::GetTestGaiaIdForEmail(kEmail), entry->GetGAIAId()); + EXPECT_EQ(kEmail, base::UTF16ToUTF8(entry->GetUserName())); + + // Signout. + identity_test_env_.ClearPrimaryAccount(); + EXPECT_EQ(entry->GetSigninState(), SigninState::kNotSignedIn); + EXPECT_FALSE(entry->IsSigninRequired()); +} +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) + +#if !BUILDFLAG(IS_CHROMEOS_ASH) +TEST_F(SigninProfileAttributesUpdaterTest, SigninSignoutResetsProfilePrefs) { + PrefService* pref_service = profile_->GetPrefs(); + ProfileAttributesEntry* entry = + profile_manager_.profile_attributes_storage() + ->GetProfileAttributesWithPath(profile_->GetPath()); + ASSERT_NE(entry, nullptr); + + // Set profile prefs. + CheckProfilePrefsReset(pref_service, true); +#if !defined(OS_ANDROID) + SetProfilePrefs(pref_service); + + // Set UPA should reset profile prefs. + AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable( + "email1@example.com", signin::ConsentLevel::kSignin); + EXPECT_FALSE(entry->IsAuthenticated()); + CheckProfilePrefsReset(pref_service, false); + SetProfilePrefs(pref_service); + // Signout should reset profile prefs. + identity_test_env_.ClearPrimaryAccount(); + CheckProfilePrefsReset(pref_service, false); +#endif // !defined(OS_ANDROID) + + SetProfilePrefs(pref_service); + // Set primary account should reset profile prefs. + AccountInfo primary_account = identity_test_env_.MakePrimaryAccountAvailable( + "primary@example.com", signin::ConsentLevel::kSync); + CheckProfilePrefsReset(pref_service, false); + SetProfilePrefs(pref_service); + // Disabling sync should reset profile prefs. + identity_test_env_.ClearPrimaryAccount(); + CheckProfilePrefsReset(pref_service, false); +} + +#if !defined(OS_ANDROID) +TEST_F(SigninProfileAttributesUpdaterTest, + EnablingSyncWithUPAAccountShouldNotResetProfilePrefs) { + PrefService* pref_service = profile_->GetPrefs(); + ProfileAttributesEntry* entry = + profile_manager_.profile_attributes_storage() + ->GetProfileAttributesWithPath(profile_->GetPath()); + ASSERT_NE(entry, nullptr); + // Set UPA. + AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable( + "email1@example.com", signin::ConsentLevel::kSignin); + EXPECT_FALSE(entry->IsAuthenticated()); + SetProfilePrefs(pref_service); + // Set primary account to be the same as the UPA. + // Given it is the same account, profile prefs should keep the same state. + identity_test_env_.SetPrimaryAccount(account_info.email, + signin::ConsentLevel::kSync); + EXPECT_TRUE(entry->IsAuthenticated()); + CheckProfilePrefsSet(pref_service, false); + identity_test_env_.ClearPrimaryAccount(); + CheckProfilePrefsReset(pref_service, false); +} + +TEST_F(SigninProfileAttributesUpdaterTest, + EnablingSyncWithDifferentAccountThanUPAResetsProfilePrefs) { + PrefService* pref_service = profile_->GetPrefs(); + ProfileAttributesEntry* entry = + profile_manager_.profile_attributes_storage() + ->GetProfileAttributesWithPath(profile_->GetPath()); + ASSERT_NE(entry, nullptr); + AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable( + "email1@example.com", signin::ConsentLevel::kSignin); + EXPECT_FALSE(entry->IsAuthenticated()); + SetProfilePrefs(pref_service); + // Set primary account to a different account than the UPA. + AccountInfo primary_account = identity_test_env_.MakePrimaryAccountAvailable( + "primary@example.com", signin::ConsentLevel::kSync); + EXPECT_TRUE(entry->IsAuthenticated()); + CheckProfilePrefsReset(pref_service, false); +} +#endif // !defined(OS_ANDROID) + +class SigninProfileAttributesUpdaterWithForceSigninTest + : public SigninProfileAttributesUpdaterTest { + public: + SigninProfileAttributesUpdaterWithForceSigninTest() + : forced_signin_setter_(true) {} + + private: + signin_util::ScopedForceSigninSetterForTesting forced_signin_setter_; +}; + +TEST_F(SigninProfileAttributesUpdaterWithForceSigninTest, IsSigninRequired) { + ProfileAttributesEntry* entry = + profile_manager_.profile_attributes_storage() + ->GetProfileAttributesWithPath(profile_->GetPath()); + ASSERT_NE(entry, nullptr); + EXPECT_FALSE(entry->IsAuthenticated()); + EXPECT_TRUE(entry->IsSigninRequired()); + + AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable( + kEmail, signin::ConsentLevel::kSync); + + EXPECT_TRUE(entry->IsAuthenticated()); + EXPECT_EQ(signin::GetTestGaiaIdForEmail(kEmail), entry->GetGAIAId()); + EXPECT_EQ(kEmail, base::UTF16ToUTF8(entry->GetUserName())); + + identity_test_env_.ClearPrimaryAccount(); + EXPECT_EQ(entry->GetSigninState(), SigninState::kNotSignedIn); + EXPECT_TRUE(entry->IsSigninRequired()); +} +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) diff --git a/chromium/chrome/browser/signin/signin_promo.cc b/chromium/chrome/browser/signin/signin_promo.cc new file mode 100644 index 00000000000..9315cb8a924 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_promo.cc @@ -0,0 +1,143 @@ +// 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 "chrome/browser/signin/signin_promo.h" + +#include "base/strings/string_number_conversions.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/google/google_brand.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/signin/signin_promo_util.h" +#include "chrome/common/pref_names.h" +#include "chrome/common/url_constants.h" +#include "components/google/core/common/google_util.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/prefs/pref_service.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/storage_partition_config.h" +#include "extensions/browser/guest_view/web_view/web_view_guest.h" +#include "google_apis/gaia/gaia_urls.h" +#include "net/base/url_util.h" +#include "url/gurl.h" + +#if defined(OS_WIN) +#include "base/win/windows_version.h" +#endif + +namespace signin { + +const char kSignInPromoQueryKeyAccessPoint[] = "access_point"; +const char kSignInPromoQueryKeyAutoClose[] = "auto_close"; +const char kSignInPromoQueryKeyForceKeepData[] = "force_keep_data"; +const char kSignInPromoQueryKeyReason[] = "reason"; + +#if !BUILDFLAG(IS_CHROMEOS_ASH) +GURL GetEmbeddedPromoURL(signin_metrics::AccessPoint access_point, + signin_metrics::Reason reason, + bool auto_close) { + CHECK_LT(static_cast(access_point), + static_cast(signin_metrics::AccessPoint::ACCESS_POINT_MAX)); + CHECK_NE(static_cast(access_point), + static_cast(signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN)); + CHECK_LE(static_cast(reason), + static_cast(signin_metrics::Reason::kMaxValue)); + CHECK_NE(static_cast(reason), + static_cast(signin_metrics::Reason::kUnknownReason)); + + GURL url(chrome::kChromeUIChromeSigninURL); + url = net::AppendQueryParameter( + url, signin::kSignInPromoQueryKeyAccessPoint, + base::NumberToString(static_cast(access_point))); + url = + net::AppendQueryParameter(url, signin::kSignInPromoQueryKeyReason, + base::NumberToString(static_cast(reason))); + if (auto_close) { + url = net::AppendQueryParameter(url, signin::kSignInPromoQueryKeyAutoClose, + "1"); + } + return url; +} + +GURL GetEmbeddedReauthURLWithEmail(signin_metrics::AccessPoint access_point, + signin_metrics::Reason reason, + const std::string& email) { + GURL url = GetEmbeddedPromoURL(access_point, reason, /*auto_close=*/true); + url = net::AppendQueryParameter(url, "email", email); + url = net::AppendQueryParameter(url, "validateEmail", "1"); + return net::AppendQueryParameter(url, "readOnlyEmail", "1"); +} +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) + +GURL GetChromeSyncURLForDice(const std::string& email, + const std::string& continue_url) { + GURL url = GaiaUrls::GetInstance()->signin_chrome_sync_dice(); + if (!email.empty()) + url = net::AppendQueryParameter(url, "email_hint", email); + if (!continue_url.empty()) + url = net::AppendQueryParameter(url, "continue", continue_url); + return url; +} + +GURL GetAddAccountURLForDice(const std::string& email, + const std::string& continue_url) { + GURL url = GaiaUrls::GetInstance()->add_account_url(); + if (!email.empty()) + url = net::AppendQueryParameter(url, "Email", email); + if (!continue_url.empty()) + url = net::AppendQueryParameter(url, "continue", continue_url); + return url; +} + +content::StoragePartition* GetSigninPartition( + content::BrowserContext* browser_context) { + const auto signin_partition_config = content::StoragePartitionConfig::Create( + browser_context, "chrome-signin", /* partition_name= */ "", + /* in_memory= */ true); + return browser_context->GetStoragePartition(signin_partition_config); +} + +signin_metrics::AccessPoint GetAccessPointForEmbeddedPromoURL(const GURL& url) { + std::string value; + if (!net::GetValueForKeyInQuery(url, kSignInPromoQueryKeyAccessPoint, + &value)) { + return signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN; + } + + int access_point = -1; + base::StringToInt(value, &access_point); + if (access_point < + static_cast( + signin_metrics::AccessPoint::ACCESS_POINT_START_PAGE) || + access_point >= + static_cast(signin_metrics::AccessPoint::ACCESS_POINT_MAX)) { + return signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN; + } + + return static_cast(access_point); +} + +signin_metrics::Reason GetSigninReasonForEmbeddedPromoURL(const GURL& url) { + std::string value; + if (!net::GetValueForKeyInQuery(url, kSignInPromoQueryKeyReason, &value)) + return signin_metrics::Reason::kUnknownReason; + + int reason = -1; + base::StringToInt(value, &reason); + if (reason < + static_cast(signin_metrics::Reason::kSigninPrimaryAccount) || + reason > static_cast(signin_metrics::Reason::kMaxValue)) { + return signin_metrics::Reason::kUnknownReason; + } + + return static_cast(reason); +} + +void RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) { + registry->RegisterIntegerPref(prefs::kDiceSigninUserMenuPromoCount, 0); +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/signin_promo.h b/chromium/chrome/browser/signin/signin_promo.h new file mode 100644 index 00000000000..36cbc05b7c4 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_promo.h @@ -0,0 +1,83 @@ +// 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 CHROME_BROWSER_SIGNIN_SIGNIN_PROMO_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_PROMO_H_ + +#include + +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "components/signin/public/base/signin_metrics.h" + +class GURL; + +namespace content { +class BrowserContext; +class StoragePartition; +} // namespace content + +namespace user_prefs { +class PrefRegistrySyncable; +} + +// Utility functions for sign in promos. +namespace signin { + +extern const char kSignInPromoQueryKeyAccessPoint[]; +// TODO(https://crbug.com/1205147): Auto close is unused. Remove it. +extern const char kSignInPromoQueryKeyAutoClose[]; +extern const char kSignInPromoQueryKeyForceKeepData[]; +extern const char kSignInPromoQueryKeyReason[]; + +#if !BUILDFLAG(IS_CHROMEOS_ASH) +// These functions are only used to unlock the profile from the desktop user +// manager and the windows credential provider. + +// Returns the sign in promo URL that can be used in a modal dialog with +// the given arguments in the query. +// |access_point| indicates where the sign in is being initiated. +// |reason| indicates the purpose of using this URL. +// |auto_close| whether to close the sign in promo automatically when done. +GURL GetEmbeddedPromoURL(signin_metrics::AccessPoint access_point, + signin_metrics::Reason reason, + bool auto_close); + +// Returns a sign in promo URL specifically for reauthenticating |email| that +// can be used in a modal dialog. +GURL GetEmbeddedReauthURLWithEmail(signin_metrics::AccessPoint access_point, + signin_metrics::Reason reason, + const std::string& email); +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) + +// Returns the URL to be used to signin and turn on Sync when DICE is enabled. +// If email is not empty, then it will pass email as hint to the page so that it +// will be autofilled by Gaia. +// If |continue_url| is empty, this may redirect to myaccount. +GURL GetChromeSyncURLForDice(const std::string& email, + const std::string& continue_url); + +// Returns the URL to be used to add (secondary) account when DICE is enabled. +// If email is not empty, then it will pass email as hint to the page so that it +// will be autofilled by Gaia. +// If |continue_url| is empty, this may redirect to myaccount. +GURL GetAddAccountURLForDice(const std::string& email, + const std::string& continue_url); + +// Gets the partition for the embedded sign in frame/webview. +content::StoragePartition* GetSigninPartition( + content::BrowserContext* browser_context); + +// Gets the access point from the query portion of the sign in promo URL. +signin_metrics::AccessPoint GetAccessPointForEmbeddedPromoURL(const GURL& url); + +// Gets the sign in reason from the query portion of the sign in promo URL. +signin_metrics::Reason GetSigninReasonForEmbeddedPromoURL(const GURL& url); + +// Registers the preferences the Sign In Promo needs. +void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry); + +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_PROMO_H_ diff --git a/chromium/chrome/browser/signin/signin_promo_unittest.cc b/chromium/chrome/browser/signin/signin_promo_unittest.cc new file mode 100644 index 00000000000..c6dc4054af2 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_promo_unittest.cc @@ -0,0 +1,56 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/signin_promo.h" + +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/common/webui_url_constants.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace signin { + +#if !BUILDFLAG(IS_CHROMEOS_ASH) +TEST(SigninPromoTest, TestPromoURL) { + GURL::Replacements replace_query; + replace_query.SetQueryStr("access_point=0&reason=0&auto_close=1"); + EXPECT_EQ( + GURL(chrome::kChromeUIChromeSigninURL).ReplaceComponents(replace_query), + GetEmbeddedPromoURL(signin_metrics::AccessPoint::ACCESS_POINT_START_PAGE, + signin_metrics::Reason::kSigninPrimaryAccount, true)); + replace_query.SetQueryStr("access_point=15&reason=1"); + EXPECT_EQ( + GURL(chrome::kChromeUIChromeSigninURL).ReplaceComponents(replace_query), + GetEmbeddedPromoURL( + signin_metrics::AccessPoint::ACCESS_POINT_SIGNIN_PROMO, + signin_metrics::Reason::kAddSecondaryAccount, false)); +} + +TEST(SigninPromoTest, TestReauthURL) { + GURL::Replacements replace_query; + replace_query.SetQueryStr( + "access_point=0&reason=6&auto_close=1" + "&email=example%40domain.com&validateEmail=1" + "&readOnlyEmail=1"); + EXPECT_EQ( + GURL(chrome::kChromeUIChromeSigninURL).ReplaceComponents(replace_query), + GetEmbeddedReauthURLWithEmail( + signin_metrics::AccessPoint::ACCESS_POINT_START_PAGE, + signin_metrics::Reason::kFetchLstOnly, "example@domain.com")); +} +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) + +TEST(SigninPromoTest, SigninURLForDice) { + EXPECT_EQ( + "https://accounts.google.com/signin/chrome/sync?ssp=1&" + "email_hint=email%40gmail.com&continue=https%3A%2F%2Fcontinue_url%2F", + GetChromeSyncURLForDice("email@gmail.com", "https://continue_url/")); + EXPECT_EQ( + "https://accounts.google.com/AddSession?" + "Email=email%40gmail.com&continue=https%3A%2F%2Fcontinue_url%2F", + GetAddAccountURLForDice("email@gmail.com", "https://continue_url/")); +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/signin_promo_util.cc b/chromium/chrome/browser/signin/signin_promo_util.cc new file mode 100644 index 00000000000..4497bbf6b72 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_promo_util.cc @@ -0,0 +1,49 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/signin_promo_util.h" + +#include "build/chromeos_buildflags.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "components/prefs/pref_service.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/primary_account_mutator.h" +#include "net/base/network_change_notifier.h" + +namespace signin { + +bool ShouldShowPromo(Profile* profile) { +#if BUILDFLAG(IS_CHROMEOS_ASH) + // There's no need to show the sign in promo on cros since cros users are + // already logged in. + return false; +#else + + // Don't bother if we don't have any kind of network connection. + if (net::NetworkChangeNotifier::IsOffline()) + return false; + + // Consider original profile even if an off-the-record profile was + // passed to this method as sign-in state is only defined for the + // primary profile. + Profile* original_profile = profile->GetOriginalProfile(); + + // Don't show for supervised child profiles. + if (original_profile->IsChild()) + return false; + + // Don't show if sign-in is not allowed. + if (!original_profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed)) + return false; + + // Display the signin promo if the user is not signed in. + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(original_profile); + return !identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync); +#endif +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/signin_promo_util.h b/chromium/chrome/browser/signin/signin_promo_util.h new file mode 100644 index 00000000000..36a68d39e00 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_promo_util.h @@ -0,0 +1,18 @@ +// Copyright 2016 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 CHROME_BROWSER_SIGNIN_SIGNIN_PROMO_UTIL_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_PROMO_UTIL_H_ + +class Profile; + +namespace signin { + +// Returns true if the sign in promo should be visible. +// |profile| is the profile of the tab the promo would be shown on. +bool ShouldShowPromo(Profile* profile); + +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_PROMO_UTIL_H_ diff --git a/chromium/chrome/browser/signin/signin_ui_util.cc b/chromium/chrome/browser/signin/signin_ui_util.cc new file mode 100644 index 00000000000..18c541f96a2 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_ui_util.cc @@ -0,0 +1,608 @@ +// Copyright (c) 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 "chrome/browser/signin/signin_ui_util.h" + +#include "base/bind.h" +#include "base/callback.h" +#include "base/callback_helpers.h" +#include "base/feature_list.h" +#include "base/metrics/histogram_functions.h" +#include "base/metrics/histogram_macros.h" +#include "base/metrics/user_metrics.h" +#include "base/notreached.h" +#include "base/strings/strcat.h" +#include "base/strings/string_util.h" +#include "base/strings/sys_string_conversions.h" +#include "base/strings/utf_string_conversions.h" +#include "base/supports_user_data.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/feature_engagement/tracker_factory.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/signin_features.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_navigator.h" +#include "chrome/browser/ui/browser_navigator_params.h" +#include "chrome/browser/ui/chrome_pages.h" +#include "chrome/browser/ui/scoped_tabbed_browser_displayer.h" +#include "chrome/browser/ui/ui_features.h" +#include "chrome/common/pref_names.h" +#include "components/feature_engagement/public/tracker.h" +#include "components/prefs/pref_service.h" +#include "components/signin/public/base/consent_level.h" +#include "components/signin/public/base/signin_metrics.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/account_info.h" +#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_utils.h" +#include "third_party/re2/src/re2/re2.h" +#include "ui/gfx/font_list.h" +#include "ui/gfx/text_elider.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "chrome/browser/ash/profiles/profile_helper.h" +#endif + +#if BUILDFLAG(IS_CHROMEOS_LACROS) +#include "components/account_manager_core/account_manager_facade.h" +#include "components/account_manager_core/chromeos/account_manager_facade_factory.h" +#endif + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h" +#endif + +namespace { + +// Key for storing animated identity per-profile data. +const char kAnimatedIdentityKeyName[] = "animated_identity_user_data"; + +constexpr base::TimeDelta kDelayForCrossWindowAnimationReplay = + base::Seconds(5); + +// UserData attached to the user profile, keeping track of the last time the +// animation was shown to the user. +class AvatarButtonUserData : public base::SupportsUserData::Data { + public: + ~AvatarButtonUserData() override = default; + + // Returns the last time the animated identity was shown. Returns the null + // time if it was never shown. + static base::TimeTicks GetAnimatedIdentityLastShown(Profile* profile) { + DCHECK(profile); + AvatarButtonUserData* data = GetForProfile(profile); + if (!data) + return base::TimeTicks(); + return data->animated_identity_last_shown_; + } + + // Sets the time when the animated identity was shown. + static void SetAnimatedIdentityLastShown(Profile* profile, + base::TimeTicks time) { + DCHECK(!time.is_null()); + GetOrCreateForProfile(profile)->animated_identity_last_shown_ = time; + } + + private: + // Returns nullptr if there is no AvatarButtonUserData attached to the + // profile. + static AvatarButtonUserData* GetForProfile(Profile* profile) { + return static_cast( + profile->GetUserData(kAnimatedIdentityKeyName)); + } + + // Never returns nullptr. + static AvatarButtonUserData* GetOrCreateForProfile(Profile* profile) { + DCHECK(profile); + AvatarButtonUserData* existing_data = GetForProfile(profile); + if (existing_data) + return existing_data; + + auto new_data = std::make_unique(); + auto* new_data_ptr = new_data.get(); + profile->SetUserData(kAnimatedIdentityKeyName, std::move(new_data)); + return new_data_ptr; + } + + base::TimeTicks animated_identity_last_shown_; +}; + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +void CreateDiceTurnSyncOnHelper( + Profile* profile, + Browser* browser, + signin_metrics::AccessPoint signin_access_point, + signin_metrics::PromoAction signin_promo_action, + signin_metrics::Reason signin_reason, + const CoreAccountId& account_id, + DiceTurnSyncOnHelper::SigninAbortedMode signin_aborted_mode) { + // DiceTurnSyncOnHelper is suicidal (it will delete itself once it finishes + // enabling sync). + new DiceTurnSyncOnHelper(profile, browser, signin_access_point, + signin_promo_action, signin_reason, account_id, + signin_aborted_mode); +} +#endif // BUILDFLAG(ENABLE_DICE_SUPPORT) + +std::string GetReauthAccessPointHistogramSuffix( + signin_metrics::ReauthAccessPoint access_point) { + switch (access_point) { + case signin_metrics::ReauthAccessPoint::kUnknown: + NOTREACHED(); + return std::string(); + case signin_metrics::ReauthAccessPoint::kAutofillDropdown: + return "ToFillPassword"; + case signin_metrics::ReauthAccessPoint::kPasswordSaveBubble: + return "ToSaveOrUpdatePassword"; + case signin_metrics::ReauthAccessPoint::kPasswordSettings: + return "ToManageInSettings"; + case signin_metrics::ReauthAccessPoint::kGeneratePasswordDropdown: + case signin_metrics::ReauthAccessPoint::kGeneratePasswordContextMenu: + return "ToGeneratePassword"; + case signin_metrics::ReauthAccessPoint::kPasswordMoveBubble: + return "ToMovePassword"; + case signin_metrics::ReauthAccessPoint::kPasswordSaveLocallyBubble: + return "ToSavePasswordLocallyThenMove"; + } +} + +#if BUILDFLAG(IS_CHROMEOS_LACROS) +account_manager::AccountManagerFacade::AccountAdditionSource +GetAccountReauthSourceFromAccessPoint( + signin_metrics::AccessPoint access_point) { + switch (access_point) { + case signin_metrics::AccessPoint::ACCESS_POINT_AVATAR_BUBBLE_SIGN_IN: + return account_manager::AccountManagerFacade::AccountAdditionSource:: + kAvatarBubbleReauthAccountButton; + default: + NOTREACHED() << "Reauth is requested from an unknown access point " + << static_cast(access_point); + return account_manager::AccountManagerFacade::AccountAdditionSource:: + kMaxValue; + } +} +#endif + +} // namespace + +namespace signin_ui_util { + +std::u16string GetAuthenticatedUsername(Profile* profile) { + DCHECK(profile); + std::string user_display_name; + auto* identity_manager = IdentityManagerFactory::GetForProfile(profile); + if (identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) { + user_display_name = + identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSync) + .email; +#if BUILDFLAG(IS_CHROMEOS_ASH) + // See https://crbug.com/994798 for details. + user_manager::User* user = + chromeos::ProfileHelper::Get()->GetUserByProfile(profile); + // |user| may be null in tests. + if (user) + user_display_name = user->GetDisplayEmail(); +#endif // BUILDFLAG(IS_CHROMEOS_ASH) + } + + return base::UTF8ToUTF16(user_display_name); +} + +void InitializePrefsForProfile(Profile* profile) { + if (profile->IsNewProfile()) { + // Suppresses the upgrade tutorial for a new profile. + profile->GetPrefs()->SetInteger(prefs::kProfileAvatarTutorialShown, + kUpgradeWelcomeTutorialShowMax + 1); + } +} + +void ShowSigninErrorLearnMorePage(Profile* profile) { + static const char kSigninErrorLearnMoreUrl[] = + "https://support.google.com/chrome/answer/1181420?"; + NavigateParams params(profile, GURL(kSigninErrorLearnMoreUrl), + ui::PAGE_TRANSITION_LINK); + params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB; + Navigate(¶ms); +} + +void ShowReauthForPrimaryAccountWithAuthError( + Browser* browser, + signin_metrics::AccessPoint access_point) { +#if BUILDFLAG(IS_CHROMEOS_ASH) + // On ChromeOS, sync errors are fixed by re-signing into the OS. + NOTREACHED(); +#elif BUILDFLAG(IS_CHROMEOS_LACROS) + internal::ShowReauthForPrimaryAccountWithAuthErrorLacros( + browser, access_point, + ::GetAccountManagerFacade(browser->profile()->GetPath().value())); +#else + browser->signin_view_controller()->ShowSignin( + profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH, access_point); +#endif +} + +void ShowExtensionSigninPrompt(Profile* profile, + bool enable_sync, + const std::string& email_hint) { +#if BUILDFLAG(IS_CHROMEOS_ASH) + NOTREACHED(); +#else + internal::ShowExtensionSigninPrompt( + profile, +#if BUILDFLAG(IS_CHROMEOS_LACROS) + ::GetAccountManagerFacade(profile->GetPath().value()), +#endif // BUILDFLAG(IS_CHROMEOS_LACROS) + enable_sync, email_hint); +#endif // BUILDFLAG(IS_CHROMEOS_ASH) +} + +namespace internal { +#if BUILDFLAG(IS_CHROMEOS_LACROS) +void ShowReauthForPrimaryAccountWithAuthErrorLacros( + Browser* browser, + signin_metrics::AccessPoint access_point, + account_manager::AccountManagerFacade* account_manager_facade) { + Profile* profile = browser->profile(); + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile); + CoreAccountInfo primary_account_info = + identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin); + DCHECK(!primary_account_info.IsEmpty()); + DCHECK(identity_manager->HasAccountWithRefreshTokenInPersistentErrorState( + primary_account_info.account_id)); + account_manager_facade->ShowReauthAccountDialog( + GetAccountReauthSourceFromAccessPoint(access_point), + primary_account_info.email); +} +#endif // BUILDFLAG(IS_CHROMEOS_LACROS) + +#if !BUILDFLAG(IS_CHROMEOS_ASH) +void ShowExtensionSigninPrompt( + Profile* profile, +#if BUILDFLAG(IS_CHROMEOS_LACROS) + account_manager::AccountManagerFacade* account_manager_facade, +#endif + bool enable_sync, + const std::string& email_hint) { + // There is no sign-in flow for guest or system profile. + if (profile->IsGuestSession() || profile->IsSystemProfile()) + return; + // Locked profile should be unlocked with UserManager only. + ProfileAttributesEntry* entry = + g_browser_process->profile_manager() + ->GetProfileAttributesStorage() + .GetProfileAttributesWithPath(profile->GetPath()); + if (entry && entry->IsSigninRequired()) { + return; + } + + // This may be called in incognito. Redirect to the original profile. + profile = profile->GetOriginalProfile(); + +#if BUILDFLAG(IS_CHROMEOS_LACROS) + // There is no sign-in without Mirror. + if (!AccountConsistencyModeManager::IsMirrorEnabledForProfile(profile)) + return; + + if (email_hint.empty()) { + // Add a new account. + // TODO(https://crbug.com/1260291): add support for signed out profiles. + NOTREACHED() << "Lacros doesn't support signed-out profiles yet."; + return; + } + + // Re-authenticate an existing account. + account_manager_facade->ShowReauthAccountDialog( + account_manager::AccountManagerFacade::AccountAdditionSource:: + kChromeExtensionReauth, + email_hint); +#elif BUILDFLAG(ENABLE_DICE_SUPPORT) + chrome::ScopedTabbedBrowserDisplayer displayer(profile); + Browser* browser = displayer.browser(); + + // Cannot sign in if browser cannot be displayed. + if (!browser) + return; + + if (enable_sync) { + // Set a primary account. + browser->signin_view_controller()->ShowDiceEnableSyncTab( + signin_metrics::AccessPoint::ACCESS_POINT_EXTENSIONS, + signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO, email_hint); + } else { + // Add an account to the web without setting a primary account. + browser->signin_view_controller()->ShowDiceAddAccountTab( + signin_metrics::AccessPoint::ACCESS_POINT_EXTENSIONS, email_hint); + } +#endif // BUILDFLAG(IS_CHROMEOS_LACROS) +} +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) + +} // namespace internal + +void EnableSyncFromSingleAccountPromo( + Browser* browser, + const AccountInfo& account, + signin_metrics::AccessPoint access_point) { + EnableSyncFromMultiAccountPromo(browser, account, access_point, + /*is_default_promo_account=*/true); +} + +void EnableSyncFromMultiAccountPromo(Browser* browser, + const AccountInfo& account, + signin_metrics::AccessPoint access_point, + bool is_default_promo_account) { +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + internal::EnableSyncFromPromo(browser, account, access_point, + is_default_promo_account, + base::BindOnce(&CreateDiceTurnSyncOnHelper)); +#else + NOTREACHED(); +#endif +} + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +namespace internal { +void EnableSyncFromPromo( + Browser* browser, + const AccountInfo& account, + signin_metrics::AccessPoint access_point, + bool is_default_promo_account, + base::OnceCallback< + void(Profile* profile, + Browser* browser, + signin_metrics::AccessPoint signin_access_point, + signin_metrics::PromoAction signin_promo_action, + signin_metrics::Reason signin_reason, + const CoreAccountId& account_id, + DiceTurnSyncOnHelper::SigninAbortedMode signin_aborted_mode)> + create_dice_turn_sync_on_helper_callback) { + DCHECK(browser); + DCHECK_NE(signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN, access_point); + Profile* profile = browser->profile(); + DCHECK(!profile->IsOffTheRecord()); + + if (IdentityManagerFactory::GetForProfile(profile)->HasPrimaryAccount( + signin::ConsentLevel::kSync)) { + DVLOG(1) << "There is already a primary account."; + return; + } + + if (account.IsEmpty()) { + chrome::ShowBrowserSignin(browser, access_point, + signin::ConsentLevel::kSync); + return; + } + + DCHECK(!account.account_id.empty()); + DCHECK(!account.email.empty()); + DCHECK(AccountConsistencyModeManager::IsDiceEnabledForProfile(profile)); + + signin_metrics::PromoAction promo_action = + is_default_promo_account + ? signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT + : signin_metrics::PromoAction::PROMO_ACTION_NOT_DEFAULT; + + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile); + bool needs_reauth_before_enable_sync = + !identity_manager->HasAccountWithRefreshToken(account.account_id) || + identity_manager->HasAccountWithRefreshTokenInPersistentErrorState( + account.account_id); + if (needs_reauth_before_enable_sync) { + browser->signin_view_controller()->ShowDiceEnableSyncTab( + access_point, promo_action, account.email); + return; + } + + signin_metrics::LogSigninAccessPointStarted(access_point, promo_action); + signin_metrics::RecordSigninUserActionForAccessPoint(access_point, + promo_action); + std::move(create_dice_turn_sync_on_helper_callback) + .Run(profile, browser, access_point, promo_action, + signin_metrics::Reason::kSigninPrimaryAccount, account.account_id, + DiceTurnSyncOnHelper::SigninAbortedMode::KEEP_ACCOUNT); +} +} // namespace internal + +std::vector GetAccountsForDicePromos(Profile* profile) { + // Fetch account ids for accounts that have a token. + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile); + std::vector accounts_with_tokens = + identity_manager->GetExtendedAccountInfoForAccountsWithRefreshToken(); + + // Compute the default account. + CoreAccountId default_account_id = + identity_manager->GetPrimaryAccountId(signin::ConsentLevel::kSignin); + + // Fetch account information for each id and make sure that the first account + // in the list matches the unconsented primary account (if available). + std::vector accounts; + for (auto& account_info : accounts_with_tokens) { + DCHECK(!account_info.IsEmpty()); + if (!signin::IsUsernameAllowedByPatternFromPrefs( + g_browser_process->local_state(), account_info.email)) { + continue; + } + if (account_info.account_id == default_account_id) + accounts.insert(accounts.begin(), std::move(account_info)); + else + accounts.push_back(std::move(account_info)); + } + return accounts; +} + +AccountInfo GetSingleAccountForDicePromos(Profile* profile) { + std::vector accounts = GetAccountsForDicePromos(profile); + if (!accounts.empty()) + return accounts[0]; + return AccountInfo(); +} + +#endif // BUILDFLAG(ENABLE_DICE_SUPPORT) + +std::u16string GetShortProfileIdentityToDisplay( + const ProfileAttributesEntry& profile_attributes_entry, + Profile* profile) { + DCHECK(profile); + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile); + CoreAccountInfo core_info = + identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin); + // If there's no unconsented primary account, simply return the name of the + // profile according to profile attributes. + if (core_info.IsEmpty()) + return profile_attributes_entry.GetName(); + + AccountInfo extended_info = + identity_manager->FindExtendedAccountInfoByAccountId( + core_info.account_id); + // If there's no given name available, return the user email. + if (extended_info.given_name.empty()) + return base::UTF8ToUTF16(core_info.email); + + return base::UTF8ToUTF16(extended_info.given_name); +} + +std::string GetAllowedDomain(std::string signin_pattern) { + std::vector splitted_signin_pattern = base::SplitString( + signin_pattern, "@", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL); + + // There are more than one '@'s in the pattern. + if (splitted_signin_pattern.size() != 2) + return std::string(); + + std::string domain = splitted_signin_pattern[1]; + + // Trims tailing '$' if existed. + if (!domain.empty() && domain.back() == '$') + domain.pop_back(); + + // Trims tailing '\E' if existed. + if (domain.size() > 1 && + base::EndsWith(domain, "\\E", base::CompareCase::SENSITIVE)) + domain.erase(domain.size() - 2); + + // Check if there is any special character in the domain. Note that + // jsmith@[192.168.2.1] is not supported. + if (!re2::RE2::FullMatch(domain, "[a-zA-Z0-9\\-.]+")) + return std::string(); + + return domain; +} + +bool ShouldShowAnimatedIdentityOnOpeningWindow( + const ProfileAttributesStorage& profile_attributes_storage, + Profile* profile) { + DCHECK(profile); + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile); + DCHECK(identity_manager->AreRefreshTokensLoaded()); + + base::TimeTicks animation_last_shown = + AvatarButtonUserData::GetAnimatedIdentityLastShown(profile); + // When a new window is created, only show the animation if it was never shown + // for this profile, or if it was shown in another window in the last few + // seconds (because the user may have missed it). + if (!animation_last_shown.is_null() && + base::TimeTicks::Now() - animation_last_shown > + kDelayForCrossWindowAnimationReplay) { + return false; + } + + // Show the user identity for users with multiple profiles. + if (profile_attributes_storage.GetNumberOfProfiles() > 1) { + return true; + } + + // Show the user identity for users with multiple signed-in accounts. + return identity_manager->GetAccountsWithRefreshTokens().size() > 1; +} + +void RecordAnimatedIdentityTriggered(Profile* profile) { + AvatarButtonUserData::SetAnimatedIdentityLastShown(profile, + base::TimeTicks::Now()); +} + +void RecordAvatarIconHighlighted(Profile* profile) { + base::RecordAction(base::UserMetricsAction("AvatarToolbarButtonHighlighted")); +} + +void RecordProfileMenuViewShown(Profile* profile) { + base::RecordAction(base::UserMetricsAction("ProfileMenu_Opened")); + if (profile->IsRegularProfile()) { + base::RecordAction(base::UserMetricsAction("ProfileMenu_Opened_Regular")); + // Record usage for profile switch promo. + feature_engagement::TrackerFactory::GetForBrowserContext(profile) + ->NotifyEvent("profile_menu_shown"); + } else if (profile->IsGuestSession()) { + base::RecordAction(base::UserMetricsAction("ProfileMenu_Opened_Guest")); + } else if (profile->IsIncognitoProfile()) { + base::RecordAction(base::UserMetricsAction("ProfileMenu_Opened_Incognito")); + } + + base::TimeTicks last_shown = + AvatarButtonUserData::GetAnimatedIdentityLastShown(profile); + if (!last_shown.is_null()) { + base::UmaHistogramLongTimes("Profile.Menu.OpenedAfterAvatarAnimation", + base::TimeTicks::Now() - last_shown); + } +} + +void RecordProfileMenuClick(Profile* profile) { + base::RecordAction( + base::UserMetricsAction("ProfileMenu_ActionableItemClicked")); + if (profile->IsRegularProfile()) { + base::RecordAction( + base::UserMetricsAction("ProfileMenu_ActionableItemClicked_Regular")); + } else if (profile->IsGuestSession()) { + base::RecordAction( + base::UserMetricsAction("ProfileMenu_ActionableItemClicked_Guest")); + } else if (profile->IsIncognitoProfile()) { + base::RecordAction( + base::UserMetricsAction("ProfileMenu_ActionableItemClicked_Incognito")); + } +} + +void RecordTransactionalReauthResult( + signin_metrics::ReauthAccessPoint access_point, + signin::ReauthResult result) { + const char kHistogramName[] = "Signin.TransactionalReauthResult"; + base::UmaHistogramEnumeration(kHistogramName, result); + + std::string access_point_suffix = + GetReauthAccessPointHistogramSuffix(access_point); + if (!access_point_suffix.empty()) { + std::string suffixed_histogram_name = + base::StrCat({kHistogramName, ".", access_point_suffix}); + base::UmaHistogramEnumeration(suffixed_histogram_name, result); + } +} + +void RecordTransactionalReauthUserAction( + signin_metrics::ReauthAccessPoint access_point, + SigninReauthViewController::UserAction user_action) { + const char kHistogramName[] = "Signin.TransactionalReauthUserAction"; + base::UmaHistogramEnumeration(kHistogramName, user_action); + + std::string access_point_suffix = + GetReauthAccessPointHistogramSuffix(access_point); + if (!access_point_suffix.empty()) { + std::string suffixed_histogram_name = + base::StrCat({kHistogramName, ".", access_point_suffix}); + base::UmaHistogramEnumeration(suffixed_histogram_name, user_action); + } +} + +} // namespace signin_ui_util diff --git a/chromium/chrome/browser/signin/signin_ui_util.h b/chromium/chrome/browser/signin/signin_ui_util.h new file mode 100644 index 00000000000..075952e959b --- /dev/null +++ b/chromium/chrome/browser/signin/signin_ui_util.h @@ -0,0 +1,188 @@ +// Copyright (c) 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 CHROME_BROWSER_SIGNIN_SIGNIN_UI_UTIL_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_UI_UTIL_H_ + +#include +#include + +#include "base/callback_forward.h" +#include "build/buildflag.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/signin/reauth_result.h" +#include "chrome/browser/ui/signin_reauth_view_controller.h" +#include "components/signin/public/base/signin_buildflags.h" +#include "components/signin/public/base/signin_metrics.h" + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h" +#endif + +struct AccountInfo; +class Browser; +class Profile; +class ProfileAttributesEntry; +class ProfileAttributesStorage; + +#if BUILDFLAG(IS_CHROMEOS_LACROS) +namespace account_manager { +class AccountManagerFacade; +} +#endif + +// Utility functions to gather status information from the various signed in +// services and construct messages suitable for showing in UI. +namespace signin_ui_util { + +// The maximum number of times to show the welcome tutorial for an upgrade user. +const int kUpgradeWelcomeTutorialShowMax = 1; + +// Returns the username of the primary account or an empty string if there is +// no primary account or the account has not consented to browser sync. +std::u16string GetAuthenticatedUsername(Profile* profile); + +// Initializes signin-related preferences. +void InitializePrefsForProfile(Profile* profile); + +// Shows a learn more page for signin errors. +void ShowSigninErrorLearnMorePage(Profile* profile); + +// Shows a reauth page/dialog to reauthanticate a primary account in error +// state. +void ShowReauthForPrimaryAccountWithAuthError( + Browser* browser, + signin_metrics::AccessPoint access_point); + +// Delegates to an existing sign-in tab if one exists. If not, a new sign-in tab +// is created. +void ShowExtensionSigninPrompt(Profile* profile, + bool enable_sync, + const std::string& email_hint); + +namespace internal { +#if BUILDFLAG(IS_CHROMEOS_LACROS) +// Same as `ShowReauthForPrimaryAccountWithAuthError` but with a getter function +// for AccountManagerFacade so that it can be unit tested. +void ShowReauthForPrimaryAccountWithAuthErrorLacros( + Browser* browser, + signin_metrics::AccessPoint access_point, + account_manager::AccountManagerFacade* account_manager_facade); +#endif // BUILDFLAG(IS_CHROMEOS_LACROS) + +#if !BUILDFLAG(IS_CHROMEOS_ASH) +void ShowExtensionSigninPrompt( + Profile* profile, +#if BUILDFLAG(IS_CHROMEOS_LACROS) + account_manager::AccountManagerFacade* account_manager_facade, +#endif + bool enable_sync, + const std::string& email_hint); +#endif +} // namespace internal + +// This function is used to enable sync for a given account: +// * This function does nothing if the user is already signed in to Chrome. +// * If |account| is empty, then it presents the Chrome sign-in page. +// * If token service has an invalid refreh token for account |account|, +// then it presents the Chrome sign-in page with |account.emil| prefilled. +// * If token service has a valid refresh token for |account|, then it +// enables sync for |account|. +void EnableSyncFromSingleAccountPromo(Browser* browser, + const AccountInfo& account, + signin_metrics::AccessPoint access_point); + +// This function is used to enable sync for a given account. It has the same +// behavior as |EnableSyncFromSingleAccountPromo()| except that it also logs +// some additional information if the action is started from a promo that +// supports selecting the account that may be used for sync. +// +// |is_default_promo_account| is true if |account| corresponds to the default +// account in the promo. It is ignored if |account| is empty. +void EnableSyncFromMultiAccountPromo(Browser* browser, + const AccountInfo& account, + signin_metrics::AccessPoint access_point, + bool is_default_promo_account); + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +// Returns the list of all accounts that have a token. The unconsented primary +// account will be the first account in the list. +std::vector GetAccountsForDicePromos(Profile* profile); + +// Returns single account to use in Dice promos. +AccountInfo GetSingleAccountForDicePromos(Profile* profile); + +#endif + +// Returns the short user identity to display for |profile|. It is based on the +// current unconsented primary account (if exists). +// TODO(crbug.com/1012179): Move this logic into ProfileAttributesEntry once +// AvatarToolbarButton becomes an observer of ProfileAttributesStorage and thus +// ProfileAttributesEntry is up-to-date when AvatarToolbarButton needs it. +std::u16string GetShortProfileIdentityToDisplay( + const ProfileAttributesEntry& profile_attributes_entry, + Profile* profile); + +// Returns the domain of the policy value of RestrictSigninToPattern. Returns +// an empty string if the policy is not set or can not be parsed. The parser +// only supports the policy value that matches [^@]+@[a-zA-Z0-9\-.]+(\\E)?\$?$. +// Also, the parser does not validate the policy value. +std::string GetAllowedDomain(std::string signin_pattern); + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +namespace internal { +// Same as |EnableSyncFromPromo| but with a callback that creates a +// DiceTurnSyncOnHelper so that it can be unit tested. +void EnableSyncFromPromo( + Browser* browser, + const AccountInfo& account, + signin_metrics::AccessPoint access_point, + bool is_default_promo_account, + base::OnceCallback< + void(Profile* profile, + Browser* browser, + signin_metrics::AccessPoint signin_access_point, + signin_metrics::PromoAction signin_promo_action, + signin_metrics::Reason signin_reason, + const CoreAccountId& account_id, + DiceTurnSyncOnHelper::SigninAbortedMode signin_aborted_mode)> + create_dice_turn_sync_on_helper_callback); +} // namespace internal +#endif + +// Returns whether Chrome should show the identity of the user (using a brief +// animation) on opening a new window. IdentityManager's refresh tokens must be +// loaded when this function gets called. +bool ShouldShowAnimatedIdentityOnOpeningWindow( + const ProfileAttributesStorage& profile_attributes_storage, + Profile* profile); + +// Records that the animated identity was shown for the given profile. This is +// used for metrics and to decide whether/when the animation can be shown again. +void RecordAnimatedIdentityTriggered(Profile* profile); + +// Records that the avatar icon was highlighted for the given profile. This is +// used for metrics. +void RecordAvatarIconHighlighted(Profile* profile); + +// Called when the ProfileMenuView is opened. Used for metrics. +void RecordProfileMenuViewShown(Profile* profile); + +// Called when a button/link in the profile menu was clicked. +void RecordProfileMenuClick(Profile* profile); + +// Records the result of a re-auth challenge to finish a transaction (like +// unlocking the account store for passwords). +void RecordTransactionalReauthResult( + signin_metrics::ReauthAccessPoint access_point, + signin::ReauthResult result); + +// Records user action performed in a transactional reauth dialog/tab. +void RecordTransactionalReauthUserAction( + signin_metrics::ReauthAccessPoint access_point, + SigninReauthViewController::UserAction user_action); + +} // namespace signin_ui_util + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_UI_UTIL_H_ diff --git a/chromium/chrome/browser/signin/signin_ui_util_browsertest.cc b/chromium/chrome/browser/signin/signin_ui_util_browsertest.cc new file mode 100644 index 00000000000..299e45539d6 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_ui_util_browsertest.cc @@ -0,0 +1,85 @@ +// Copyright 2021 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 CHROME_BROWSER_SIGNIN_SIGNIN_UI_UTIL_BROWSERTEST_CC_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_UI_UTIL_BROWSERTEST_CC_ + +#include "chrome/browser/signin/signin_ui_util.h" + +#include "base/callback_helpers.h" +#include "base/test/bind.h" +#include "build/buildflag.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/ui_test_utils.h" +#include "components/signin/public/base/signin_buildflags.h" +#include "content/public/test/browser_test.h" + +#if !BUILDFLAG(ENABLE_DICE_SUPPORT) +#error This file only contains DICE browser tests for now. +#endif + +namespace signin_ui_util { + +class DiceSigninUiUtilBrowserTest : public InProcessBrowserTest { + public: + DiceSigninUiUtilBrowserTest() = default; + ~DiceSigninUiUtilBrowserTest() override = default; + + Profile* CreateProfile() { + Profile* new_profile = nullptr; + base::RunLoop run_loop; + ProfileManager::CreateMultiProfileAsync( + u"test_profile", /*icon_index=*/0, /*is_hidden=*/false, + base::BindLambdaForTesting( + [&new_profile, &run_loop](Profile* profile, + Profile::CreateStatus status) { + ASSERT_NE(status, Profile::CREATE_STATUS_LOCAL_FAIL); + if (status == Profile::CREATE_STATUS_INITIALIZED) { + new_profile = profile; + run_loop.Quit(); + } + })); + run_loop.Run(); + return new_profile; + } + + private: +}; + +// Tests that `ShowExtensionSigninPrompt()` doesn't crash when it cannot create +// a new browser. Regression test for https://crbug.com/1273370. +IN_PROC_BROWSER_TEST_F(DiceSigninUiUtilBrowserTest, + ShowExtensionSigninPrompt_NoBrowser) { + Profile* new_profile = CreateProfile(); + + // New profile should not have any browser windows. + EXPECT_FALSE(chrome::FindBrowserWithProfile(new_profile)); + + ShowExtensionSigninPrompt(new_profile, /*enable_sync=*/true, + /*email_hint=*/std::string()); + // `ShowExtensionSigninPrompt()` creates a new browser. + Browser* browser = chrome::FindBrowserWithProfile(new_profile); + ASSERT_TRUE(browser); + EXPECT_EQ(1, browser->tab_strip_model()->count()); + + // Profile deletion closes the browser. + g_browser_process->profile_manager()->ScheduleProfileForDeletion( + new_profile->GetPath(), base::DoNothing()); + ui_test_utils::WaitForBrowserToClose(browser); + EXPECT_FALSE(chrome::FindBrowserWithProfile(new_profile)); + + // `ShowExtensionSigninPrompt()` does nothing for deleted profile. + ShowExtensionSigninPrompt(new_profile, /*enable_sync=*/true, + /*email_hint=*/std::string()); + EXPECT_FALSE(chrome::FindBrowserWithProfile(new_profile)); +} + +} // namespace signin_ui_util + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_UI_UTIL_BROWSERTEST_CC_ diff --git a/chromium/chrome/browser/signin/signin_ui_util_unittest.cc b/chromium/chrome/browser/signin/signin_ui_util_unittest.cc new file mode 100644 index 00000000000..43be4319fcc --- /dev/null +++ b/chromium/chrome/browser/signin/signin_ui_util_unittest.cc @@ -0,0 +1,736 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/signin_ui_util.h" + +#include "base/bind.h" +#include "base/memory/raw_ptr.h" +#include "base/test/metrics/histogram_tester.h" +#include "base/test/metrics/user_action_tester.h" +#include "base/test/task_environment.h" +#include "build/build_config.h" +#include "build/buildflag.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/profiles/profile_attributes_init_params.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h" +#include "chrome/browser/signin/signin_promo.h" +#include "chrome/browser/signin/signin_util.h" +#include "chrome/browser/ui/ui_features.h" +#include "chrome/test/base/browser_with_test_window_test.h" +#include "chrome/test/base/testing_browser_process.h" +#include "chrome/test/base/testing_profile_manager.h" +#include "components/account_id/account_id.h" +#include "components/google/core/common/google_util.h" +#include "components/signin/public/base/consent_level.h" +#include "components/signin/public/base/signin_buildflags.h" +#include "components/signin/public/identity_manager/account_info.h" +#include "components/signin/public/identity_manager/accounts_mutator.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_test_utils.h" +#include "google_apis/gaia/gaia_urls.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if BUILDFLAG(IS_CHROMEOS_LACROS) +#include "components/account_manager_core/mock_account_manager_facade.h" +#endif + +namespace signin_ui_util { + +namespace { +const char kMainEmail[] = "main_email@example.com"; +const char kMainGaiaID[] = "main_gaia_id"; +const char kSecondaryEmail[] = "secondary_email@example.com"; +const char kSecondaryGaiaID[] = "secondary_gaia_id"; +} // namespace + +class GetAllowedDomainTest : public ::testing::Test {}; + +TEST_F(GetAllowedDomainTest, WithInvalidPattern) { + EXPECT_EQ(std::string(), GetAllowedDomain("email")); + EXPECT_EQ(std::string(), GetAllowedDomain("email@a@b")); + EXPECT_EQ(std::string(), GetAllowedDomain("email@a[b")); + EXPECT_EQ(std::string(), GetAllowedDomain("@$")); + EXPECT_EQ(std::string(), GetAllowedDomain("@\\E$")); + EXPECT_EQ(std::string(), GetAllowedDomain("@\\E$a")); + EXPECT_EQ(std::string(), GetAllowedDomain("email@")); + EXPECT_EQ(std::string(), GetAllowedDomain("@")); + EXPECT_EQ(std::string(), GetAllowedDomain("example@a.com|example@b.com")); + EXPECT_EQ(std::string(), GetAllowedDomain("")); +} + +TEST_F(GetAllowedDomainTest, WithValidPattern) { + EXPECT_EQ("example.com", GetAllowedDomain("email@example.com")); + EXPECT_EQ("example.com", GetAllowedDomain("email@example.com\\E")); + EXPECT_EQ("example.com", GetAllowedDomain("email@example.com$")); + EXPECT_EQ("example.com", GetAllowedDomain("email@example.com\\E$")); + EXPECT_EQ("example.com", GetAllowedDomain("*@example.com\\E$")); + EXPECT_EQ("example.com", GetAllowedDomain(".*@example.com\\E$")); + EXPECT_EQ("example-1.com", GetAllowedDomain("email@example-1.com")); +} + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + +namespace { + +class SigninUiUtilTestBrowserWindow : public TestBrowserWindow { + public: + SigninUiUtilTestBrowserWindow() = default; + + SigninUiUtilTestBrowserWindow(const SigninUiUtilTestBrowserWindow&) = delete; + SigninUiUtilTestBrowserWindow& operator=( + const SigninUiUtilTestBrowserWindow&) = delete; + + ~SigninUiUtilTestBrowserWindow() override = default; + void set_browser(Browser* browser) { browser_ = browser; } + + void ShowAvatarBubbleFromAvatarButton( + AvatarBubbleMode mode, + signin_metrics::AccessPoint access_point, + bool is_source_keyboard) override { + ASSERT_TRUE(browser_); + // Simulate what |BrowserView| does for a regular Chrome sign-in flow. + browser_->signin_view_controller()->ShowSignin( + profiles::BubbleViewMode::BUBBLE_VIEW_MODE_GAIA_SIGNIN, access_point); + } + + private: + raw_ptr browser_ = nullptr; +}; + +} // namespace + +class DiceSigninUiUtilTest : public BrowserWithTestWindowTest { + public: + DiceSigninUiUtilTest() = default; + ~DiceSigninUiUtilTest() override = default; + + struct CreateDiceTurnSyncOnHelperParams { + public: + raw_ptr profile = nullptr; + raw_ptr browser = nullptr; + signin_metrics::AccessPoint signin_access_point = + signin_metrics::AccessPoint::ACCESS_POINT_MAX; + signin_metrics::PromoAction signin_promo_action = + signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO; + signin_metrics::Reason signin_reason = + signin_metrics::Reason::kUnknownReason; + CoreAccountId account_id; + DiceTurnSyncOnHelper::SigninAbortedMode signin_aborted_mode = + DiceTurnSyncOnHelper::SigninAbortedMode::REMOVE_ACCOUNT; + }; + + void CreateDiceTurnSyncOnHelper( + Profile* profile, + Browser* browser, + signin_metrics::AccessPoint signin_access_point, + signin_metrics::PromoAction signin_promo_action, + signin_metrics::Reason signin_reason, + const CoreAccountId& account_id, + DiceTurnSyncOnHelper::SigninAbortedMode signin_aborted_mode) { + create_dice_turn_sync_on_helper_called_ = true; + create_dice_turn_sync_on_helper_params_.profile = profile; + create_dice_turn_sync_on_helper_params_.browser = browser; + create_dice_turn_sync_on_helper_params_.signin_access_point = + signin_access_point; + create_dice_turn_sync_on_helper_params_.signin_promo_action = + signin_promo_action; + create_dice_turn_sync_on_helper_params_.signin_reason = signin_reason; + create_dice_turn_sync_on_helper_params_.account_id = account_id; + create_dice_turn_sync_on_helper_params_.signin_aborted_mode = + signin_aborted_mode; + } + + protected: + // BrowserWithTestWindowTest: + void SetUp() override { + BrowserWithTestWindowTest::SetUp(); + static_cast(browser()->window()) + ->set_browser(browser()); + } + + // BrowserWithTestWindowTest: + TestingProfile::TestingFactories GetTestingFactories() override { + return IdentityTestEnvironmentProfileAdaptor:: + GetIdentityTestEnvironmentFactories(); + } + + // BrowserWithTestWindowTest: + std::unique_ptr CreateBrowserWindow() override { + return std::make_unique(); + } + + // Returns the identity manager. + signin::IdentityManager* GetIdentityManager() { + return IdentityManagerFactory::GetForProfile(profile()); + } + + void EnableSync(const AccountInfo& account_info, + bool is_default_promo_account) { + signin_ui_util::internal::EnableSyncFromPromo( + browser(), account_info, access_point_, is_default_promo_account, + base::BindOnce(&DiceSigninUiUtilTest::CreateDiceTurnSyncOnHelper, + base::Unretained(this))); + } + + void ExpectNoSigninStartedHistograms( + const base::HistogramTester& histogram_tester) { + histogram_tester.ExpectTotalCount("Signin.SigninStartedAccessPoint", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.WithDefault", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NotDefault", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NewAccountExistingAccount", 0); + } + + void ExpectOneSigninStartedHistograms( + const base::HistogramTester& histogram_tester, + signin_metrics::PromoAction expected_promo_action) { + histogram_tester.ExpectUniqueSample("Signin.SigninStartedAccessPoint", + access_point_, 1); + switch (expected_promo_action) { + case signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO: + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NotDefault", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.WithDefault", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NewAccountExistingAccount", 0); + break; + case signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT: + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NotDefault", 0); + histogram_tester.ExpectUniqueSample( + "Signin.SigninStartedAccessPoint.WithDefault", access_point_, 1); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NewAccountExistingAccount", 0); + break; + case signin_metrics::PromoAction::PROMO_ACTION_NOT_DEFAULT: + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.WithDefault", 0); + histogram_tester.ExpectUniqueSample( + "Signin.SigninStartedAccessPoint.NotDefault", access_point_, 1); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NewAccountExistingAccount", 0); + break; + case signin_metrics::PromoAction:: + PROMO_ACTION_NEW_ACCOUNT_NO_EXISTING_ACCOUNT: + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.WithDefault", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NotDefault", 0); + histogram_tester.ExpectUniqueSample( + "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", + access_point_, 1); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NewAccountExistingAccount", 0); + break; + case signin_metrics::PromoAction:: + PROMO_ACTION_NEW_ACCOUNT_EXISTING_ACCOUNT: + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.WithDefault", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NotDefault", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", 0); + histogram_tester.ExpectUniqueSample( + "Signin.SigninStartedAccessPoint.NewAccountExistingAccount", + access_point_, 1); + break; + } + } + + signin_metrics::AccessPoint access_point_ = + signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_BUBBLE; + + bool create_dice_turn_sync_on_helper_called_ = false; + CreateDiceTurnSyncOnHelperParams create_dice_turn_sync_on_helper_params_; +}; + +TEST_F(DiceSigninUiUtilTest, EnableSyncWithExistingAccount) { + CoreAccountId account_id = + GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount( + kMainGaiaID, kMainEmail, "refresh_token", false, + signin_metrics::SourceForRefreshTokenOperation::kUnknown); + + for (bool is_default_promo_account : {true, false}) { + base::HistogramTester histogram_tester; + base::UserActionTester user_action_tester; + + ExpectNoSigninStartedHistograms(histogram_tester); + EXPECT_EQ(0, user_action_tester.GetActionCount( + "Signin_Signin_FromBookmarkBubble")); + + EnableSync( + GetIdentityManager()->FindExtendedAccountInfoByAccountId(account_id), + is_default_promo_account); + signin_metrics::PromoAction expected_promo_action = + is_default_promo_account + ? signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT + : signin_metrics::PromoAction::PROMO_ACTION_NOT_DEFAULT; + ASSERT_TRUE(create_dice_turn_sync_on_helper_called_); + ExpectOneSigninStartedHistograms(histogram_tester, expected_promo_action); + + EXPECT_EQ(1, user_action_tester.GetActionCount( + "Signin_Signin_FromBookmarkBubble")); + if (is_default_promo_account) { + EXPECT_EQ(1, user_action_tester.GetActionCount( + "Signin_SigninWithDefault_FromBookmarkBubble")); + } else { + EXPECT_EQ(1, user_action_tester.GetActionCount( + "Signin_SigninNotDefault_FromBookmarkBubble")); + } + + // Verify that the helper to enable sync is created with the expected + // params. + EXPECT_EQ(profile(), create_dice_turn_sync_on_helper_params_.profile); + EXPECT_EQ(browser(), create_dice_turn_sync_on_helper_params_.browser); + EXPECT_EQ(account_id, create_dice_turn_sync_on_helper_params_.account_id); + EXPECT_EQ(signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_BUBBLE, + create_dice_turn_sync_on_helper_params_.signin_access_point); + EXPECT_EQ(expected_promo_action, + create_dice_turn_sync_on_helper_params_.signin_promo_action); + EXPECT_EQ(signin_metrics::Reason::kSigninPrimaryAccount, + create_dice_turn_sync_on_helper_params_.signin_reason); + EXPECT_EQ(DiceTurnSyncOnHelper::SigninAbortedMode::KEEP_ACCOUNT, + create_dice_turn_sync_on_helper_params_.signin_aborted_mode); + } +} + +TEST_F(DiceSigninUiUtilTest, EnableSyncWithAccountThatNeedsReauth) { + AddTab(browser(), GURL("http://example.com")); + CoreAccountId account_id = + GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount( + kMainGaiaID, kMainEmail, "refresh_token", false, + signin_metrics::SourceForRefreshTokenOperation::kUnknown); + + // Add an account and then put its refresh token into an error state to + // require a reauth before enabling sync. + signin::UpdatePersistentErrorOfRefreshTokenForAccount( + GetIdentityManager(), account_id, + GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS)); + + for (bool is_default_promo_account : {true, false}) { + base::HistogramTester histogram_tester; + base::UserActionTester user_action_tester; + + ExpectNoSigninStartedHistograms(histogram_tester); + EXPECT_EQ(0, user_action_tester.GetActionCount( + "Signin_Signin_FromBookmarkBubble")); + + EnableSync( + GetIdentityManager()->FindExtendedAccountInfoByAccountId(account_id), + is_default_promo_account); + ASSERT_FALSE(create_dice_turn_sync_on_helper_called_); + + ExpectOneSigninStartedHistograms( + histogram_tester, + is_default_promo_account + ? signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT + : signin_metrics::PromoAction::PROMO_ACTION_NOT_DEFAULT); + EXPECT_EQ(1, user_action_tester.GetActionCount( + "Signin_Signin_FromBookmarkBubble")); + + if (is_default_promo_account) { + EXPECT_EQ(1, user_action_tester.GetActionCount( + "Signin_SigninWithDefault_FromBookmarkBubble")); + } else { + EXPECT_EQ(1, user_action_tester.GetActionCount( + "Signin_SigninNotDefault_FromBookmarkBubble")); + } + + // Verify that the active tab has the correct DICE sign-in URL. + TabStripModel* tab_strip = browser()->tab_strip_model(); + content::WebContents* active_contents = tab_strip->GetActiveWebContents(); + ASSERT_TRUE(active_contents); + EXPECT_EQ(signin::GetChromeSyncURLForDice(kMainEmail, + google_util::kGoogleHomepageURL), + active_contents->GetVisibleURL()); + tab_strip->CloseWebContentsAt( + tab_strip->GetIndexOfWebContents(active_contents), + TabStripModel::CLOSE_USER_GESTURE); + } +} + +TEST_F(DiceSigninUiUtilTest, EnableSyncForNewAccountWithNoTab) { + base::HistogramTester histogram_tester; + base::UserActionTester user_action_tester; + + ExpectNoSigninStartedHistograms(histogram_tester); + EXPECT_EQ( + 0, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble")); + + EnableSync(AccountInfo(), false /* is_default_promo_account (not used)*/); + ASSERT_FALSE(create_dice_turn_sync_on_helper_called_); + + ExpectOneSigninStartedHistograms( + histogram_tester, signin_metrics::PromoAction:: + PROMO_ACTION_NEW_ACCOUNT_NO_EXISTING_ACCOUNT); + EXPECT_EQ( + 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble")); + EXPECT_EQ(1, + user_action_tester.GetActionCount( + "Signin_SigninNewAccountNoExistingAccount_FromBookmarkBubble")); + + // Verify that the active tab has the correct DICE sign-in URL. + content::WebContents* active_contents = + browser()->tab_strip_model()->GetActiveWebContents(); + ASSERT_TRUE(active_contents); + EXPECT_EQ( + signin::GetChromeSyncURLForDice("", google_util::kGoogleHomepageURL), + active_contents->GetVisibleURL()); +} + +TEST_F(DiceSigninUiUtilTest, EnableSyncForNewAccountWithNoTabWithExisting) { + base::HistogramTester histogram_tester; + base::UserActionTester user_action_tester; + + GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount( + kMainGaiaID, kMainEmail, "refresh_token", false, + signin_metrics::SourceForRefreshTokenOperation::kUnknown); + + ExpectNoSigninStartedHistograms(histogram_tester); + EXPECT_EQ( + 0, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble")); + + EnableSync(AccountInfo(), false /* is_default_promo_account (not used)*/); + ASSERT_FALSE(create_dice_turn_sync_on_helper_called_); + + ExpectOneSigninStartedHistograms( + histogram_tester, + signin_metrics::PromoAction::PROMO_ACTION_NEW_ACCOUNT_EXISTING_ACCOUNT); + EXPECT_EQ( + 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble")); + EXPECT_EQ(1, + user_action_tester.GetActionCount( + "Signin_SigninNewAccountExistingAccount_FromBookmarkBubble")); +} + +TEST_F(DiceSigninUiUtilTest, EnableSyncForNewAccountWithOneTab) { + base::HistogramTester histogram_tester; + base::UserActionTester user_action_tester; + AddTab(browser(), GURL("http://foo/1")); + + ExpectNoSigninStartedHistograms(histogram_tester); + EXPECT_EQ( + 0, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble")); + + EnableSync(AccountInfo(), false /* is_default_promo_account (not used)*/); + ASSERT_FALSE(create_dice_turn_sync_on_helper_called_); + + ExpectOneSigninStartedHistograms( + histogram_tester, signin_metrics::PromoAction:: + PROMO_ACTION_NEW_ACCOUNT_NO_EXISTING_ACCOUNT); + EXPECT_EQ( + 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble")); + EXPECT_EQ(1, + user_action_tester.GetActionCount( + "Signin_SigninNewAccountNoExistingAccount_FromBookmarkBubble")); + + // Verify that the active tab has the correct DICE sign-in URL. + content::WebContents* active_contents = + browser()->tab_strip_model()->GetActiveWebContents(); + ASSERT_TRUE(active_contents); + EXPECT_EQ( + signin::GetChromeSyncURLForDice("", google_util::kGoogleHomepageURL), + active_contents->GetVisibleURL()); +} + +TEST_F(DiceSigninUiUtilTest, GetAccountsForDicePromos) { + // Should start off with no accounts. + std::vector accounts = GetAccountsForDicePromos(profile()); + EXPECT_TRUE(accounts.empty()); + + // TODO(tangltom): Flesh out this test. +} + +TEST_F(DiceSigninUiUtilTest, MergeDiceSigninTab) { + base::UserActionTester user_action_tester; + EnableSync(AccountInfo(), false); + EXPECT_EQ( + 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble")); + + // Signin tab is reused. + EnableSync(AccountInfo(), false); + EXPECT_EQ( + 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble")); + + // Give focus to a different tab. + TabStripModel* tab_strip = browser()->tab_strip_model(); + ASSERT_EQ(0, tab_strip->active_index()); + GURL other_url = GURL("http://example.com"); + AddTab(browser(), other_url); + tab_strip->ActivateTabAt(0, {TabStripModel::GestureType::kOther}); + ASSERT_EQ(other_url, tab_strip->GetActiveWebContents()->GetVisibleURL()); + ASSERT_EQ(0, tab_strip->active_index()); + + // Extensions re-use the tab but do not take focus. + access_point_ = signin_metrics::AccessPoint::ACCESS_POINT_EXTENSIONS; + EnableSync(AccountInfo(), false); + EXPECT_EQ( + 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble")); + EXPECT_EQ(0, tab_strip->active_index()); + + // Other access points re-use the tab and take focus. + access_point_ = signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS; + EnableSync(AccountInfo(), false); + EXPECT_EQ( + 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble")); + EXPECT_EQ(1, tab_strip->active_index()); +} + +TEST_F(DiceSigninUiUtilTest, ShowReauthTab) { + AddTab(browser(), GURL("http://example.com")); + AccountInfo account_info = signin::MakePrimaryAccountAvailable( + GetIdentityManager(), "foo@example.com", signin::ConsentLevel::kSync); + + // Add an account and then put its refresh token into an error state to + // require a reauth before enabling sync. + signin::UpdatePersistentErrorOfRefreshTokenForAccount( + GetIdentityManager(), account_info.account_id, + GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS)); + + signin_ui_util::ShowReauthForPrimaryAccountWithAuthError( + browser(), + signin_metrics::AccessPoint::ACCESS_POINT_AVATAR_BUBBLE_SIGN_IN); + + // Verify that the active tab has the correct DICE sign-in URL. + TabStripModel* tab_strip = browser()->tab_strip_model(); + content::WebContents* active_contents = tab_strip->GetActiveWebContents(); + ASSERT_TRUE(active_contents); + EXPECT_EQ(signin::GetChromeSyncURLForDice(account_info.email, + google_util::kGoogleHomepageURL), + active_contents->GetVisibleURL()); +} + +TEST_F(DiceSigninUiUtilTest, + ShouldShowAnimatedIdentityOnOpeningWindow_ReturnsTrueForMultiProfiles) { + const char kSecondProfile[] = "SecondProfile"; + const char16_t kSecondProfile16[] = u"SecondProfile"; + const base::FilePath profile_path = + profile_manager()->profiles_dir().AppendASCII(kSecondProfile); + ProfileAttributesInitParams params; + params.profile_path = profile_path; + params.profile_name = kSecondProfile16; + profile_manager()->profile_attributes_storage()->AddProfile( + std::move(params)); + + EXPECT_TRUE(ShouldShowAnimatedIdentityOnOpeningWindow( + *profile_manager()->profile_attributes_storage(), profile())); +} + +TEST_F(DiceSigninUiUtilTest, + ShouldShowAnimatedIdentityOnOpeningWindow_ReturnsTrueForMultiSignin) { + GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount( + kMainGaiaID, kMainEmail, "refresh_token", false, + signin_metrics::SourceForRefreshTokenOperation::kUnknown); + GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount( + kSecondaryGaiaID, kSecondaryEmail, "refresh_token", false, + signin_metrics::SourceForRefreshTokenOperation::kUnknown); + + EXPECT_TRUE(ShouldShowAnimatedIdentityOnOpeningWindow( + *profile_manager()->profile_attributes_storage(), profile())); + + // The identity can be shown again immediately (which is what happens if there + // is multiple windows at startup). + RecordAnimatedIdentityTriggered(profile()); + EXPECT_TRUE(ShouldShowAnimatedIdentityOnOpeningWindow( + *profile_manager()->profile_attributes_storage(), profile())); +} + +TEST_F( + DiceSigninUiUtilTest, + ShouldShowAnimatedIdentityOnOpeningWindow_ReturnsFalseForSingleProfileSingleSignin) { + GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount( + kMainGaiaID, kMainEmail, "refresh_token", false, + signin_metrics::SourceForRefreshTokenOperation::kUnknown); + + EXPECT_FALSE(ShouldShowAnimatedIdentityOnOpeningWindow( + *profile_manager()->profile_attributes_storage(), profile())); +} + +TEST_F(DiceSigninUiUtilTest, ShowExtensionSigninPrompt) { + Profile* profile = browser()->profile(); + TabStripModel* tab_strip = browser()->tab_strip_model(); + ShowExtensionSigninPrompt(profile, /*enable_sync=*/true, + /*email_hint=*/std::string()); + EXPECT_EQ(1, tab_strip->count()); + // Calling the function again reuses the tab. + ShowExtensionSigninPrompt(profile, /*enable_sync=*/true, + /*email_hint=*/std::string()); + EXPECT_EQ(1, tab_strip->count()); + + content::WebContents* tab = tab_strip->GetWebContentsAt(0); + ASSERT_TRUE(tab); + EXPECT_TRUE(base::StartsWith( + tab->GetVisibleURL().spec(), + GaiaUrls::GetInstance()->signin_chrome_sync_dice().spec(), + base::CompareCase::INSENSITIVE_ASCII)); + + // Changing the parameter opens a new tab. + ShowExtensionSigninPrompt(profile, /*enable_sync=*/false, + /*email_hint=*/std::string()); + EXPECT_EQ(2, tab_strip->count()); + // Calling the function again reuses the tab. + ShowExtensionSigninPrompt(profile, /*enable_sync=*/false, + /*email_hint=*/std::string()); + EXPECT_EQ(2, tab_strip->count()); + tab = tab_strip->GetWebContentsAt(1); + ASSERT_TRUE(tab); + EXPECT_TRUE( + base::StartsWith(tab->GetVisibleURL().spec(), + GaiaUrls::GetInstance()->add_account_url().spec(), + base::CompareCase::INSENSITIVE_ASCII)); +} + +TEST_F(DiceSigninUiUtilTest, ShowExtensionSigninPrompt_AsLockedProfile) { + signin_util::ScopedForceSigninSetterForTesting force_signin_setter(true); + Profile* profile = browser()->profile(); + ProfileAttributesEntry* entry = + g_browser_process->profile_manager() + ->GetProfileAttributesStorage() + .GetProfileAttributesWithPath(profile->GetPath()); + ASSERT_NE(entry, nullptr); + entry->LockForceSigninProfile(true); + TabStripModel* tab_strip = browser()->tab_strip_model(); + ShowExtensionSigninPrompt(profile, /*enable_sync=*/true, + /*email_hint=*/std::string()); + EXPECT_EQ(0, tab_strip->count()); + ShowExtensionSigninPrompt(profile, /*enable_sync=*/false, + /*email_hint=*/std::string()); + EXPECT_EQ(0, tab_strip->count()); +} +#endif // BUILDFLAG(ENABLE_DICE_SUPPORT) + +#if BUILDFLAG(IS_CHROMEOS_LACROS) +class MirrorSigninUiUtilTest : public BrowserWithTestWindowTest { + public: + MirrorSigninUiUtilTest() = default; + ~MirrorSigninUiUtilTest() override = default; + + // BrowserWithTestWindowTest: + TestingProfile::TestingFactories GetTestingFactories() override { + return IdentityTestEnvironmentProfileAdaptor:: + GetIdentityTestEnvironmentFactories(); + } +}; + +TEST_F(MirrorSigninUiUtilTest, ShowReauthDialog) { + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile()); + const std::string kEmail = "foo@example.com"; + AccountInfo account_info = signin::MakePrimaryAccountAvailable( + identity_manager, kEmail, signin::ConsentLevel::kSync); + + // Add an account and then put its refresh token into an error state to + // require a reauth before enabling sync. + signin::UpdatePersistentErrorOfRefreshTokenForAccount( + identity_manager, account_info.account_id, + GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS)); + + account_manager::MockAccountManagerFacade mock_facade; + + EXPECT_CALL(mock_facade, + ShowReauthAccountDialog( + account_manager::AccountManagerFacade::AccountAdditionSource:: + kAvatarBubbleReauthAccountButton, + kEmail)); + internal::ShowReauthForPrimaryAccountWithAuthErrorLacros( + browser(), + signin_metrics::AccessPoint::ACCESS_POINT_AVATAR_BUBBLE_SIGN_IN, + &mock_facade); +} + +TEST_F(MirrorSigninUiUtilTest, ShowExtensionSigninPrompt) { + const std::string kEmail = "foo@example.com"; + TabStripModel* tab_strip = browser()->tab_strip_model(); + account_manager::MockAccountManagerFacade mock_facade; + + EXPECT_CALL( + mock_facade, + ShowReauthAccountDialog(account_manager::AccountManagerFacade:: + AccountAdditionSource::kChromeExtensionReauth, + kEmail)); + internal::ShowExtensionSigninPrompt(browser()->profile(), &mock_facade, + /*enable_sync=*/true, kEmail); + // No tabs should be opened. + EXPECT_EQ(0, tab_strip->count()); +} + +TEST_F(MirrorSigninUiUtilTest, ShowExtensionSigninPrompt_AsLockedProfile) { + signin_util::ScopedForceSigninSetterForTesting force_signin_setter(true); + Profile* profile = browser()->profile(); + ProfileAttributesEntry* entry = + g_browser_process->profile_manager() + ->GetProfileAttributesStorage() + .GetProfileAttributesWithPath(profile->GetPath()); + ASSERT_NE(entry, nullptr); + entry->LockForceSigninProfile(true); + + const std::string kEmail = "foo@example.com"; + TabStripModel* tab_strip = browser()->tab_strip_model(); + account_manager::MockAccountManagerFacade mock_facade; + + EXPECT_CALL(mock_facade, ShowReauthAccountDialog(testing::_, testing::_)) + .Times(0); + internal::ShowExtensionSigninPrompt(browser()->profile(), &mock_facade, + /*enable_sync=*/true, kEmail); + // No dialogs and tabs should be opened. + EXPECT_EQ(0, tab_strip->count()); +} + +#endif // BUILDFLAG(IS_CHROMEOS_LACROS) + +// This test does not use the DiceSigninUiUtilTest test fixture, because it +// needs a mock time environment, and BrowserWithTestWindowTest may be flaky +// when used with mock time (see https://crbug.com/1014790). +TEST(ShouldShowAnimatedIdentityOnOpeningWindow, ReturnsFalseForNewWindow) { + // Setup a testing profile manager with mock time. + content::BrowserTaskEnvironment task_environment( + base::test::TaskEnvironment::TimeSource::MOCK_TIME); + ScopedTestingLocalState local_state(TestingBrowserProcess::GetGlobal()); + TestingProfileManager profile_manager(TestingBrowserProcess::GetGlobal(), + &local_state); + ASSERT_TRUE(profile_manager.SetUp()); + std::string name("testing_profile"); + TestingProfile* profile = profile_manager.CreateTestingProfile( + name, std::unique_ptr(), + base::UTF8ToUTF16(name), 0, std::string(), + IdentityTestEnvironmentProfileAdaptor:: + GetIdentityTestEnvironmentFactories()); + + // Setup accounts. + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile); + identity_manager->GetAccountsMutator()->AddOrUpdateAccount( + kMainGaiaID, kMainEmail, "refresh_token", false, + signin_metrics::SourceForRefreshTokenOperation::kUnknown); + identity_manager->GetAccountsMutator()->AddOrUpdateAccount( + kSecondaryGaiaID, kSecondaryEmail, "refresh_token", false, + signin_metrics::SourceForRefreshTokenOperation::kUnknown); + EXPECT_TRUE(ShouldShowAnimatedIdentityOnOpeningWindow( + *profile_manager.profile_attributes_storage(), profile)); + + // Animation is shown once. + RecordAnimatedIdentityTriggered(profile); + + // Wait a few seconds. + task_environment.FastForwardBy(base::Seconds(6)); + + // Animation is not shown again in a new window. + EXPECT_FALSE(ShouldShowAnimatedIdentityOnOpeningWindow( + *profile_manager.profile_attributes_storage(), profile)); +} + +} // namespace signin_ui_util diff --git a/chromium/chrome/browser/signin/signin_util.cc b/chromium/chrome/browser/signin/signin_util.cc new file mode 100644 index 00000000000..d3ab11f8671 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_util.cc @@ -0,0 +1,382 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/signin_util.h" + +#include + +#include "base/bind.h" +#include "base/feature_list.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/metrics/histogram_functions.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/supports_user_data.h" +#include "base/task/post_task.h" +#include "base/threading/thread_task_runner_handle.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/policy/cloud/user_policy_signin_service_internal.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profiles_state.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/signin_features.h" +#include "chrome/browser/ui/simple_message_box.h" +#include "chrome/browser/ui/startup/startup_types.h" +#include "chrome/browser/ui/webui/profile_helper.h" +#include "chrome/common/pref_names.h" +#include "chrome/common/url_constants.h" +#include "chrome/grit/generated_resources.h" +#include "components/google/core/common/google_util.h" +#include "components/prefs/pref_service.h" +#include "components/signin/public/base/signin_metrics.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_utils.h" +#include "components/signin/public/identity_manager/primary_account_mutator.h" +#include "google_apis/gaia/gaia_auth_util.h" +#include "ui/base/l10n/l10n_util.h" + +#if defined(OS_WIN) || defined(OS_LINUX) || defined(OS_CHROMEOS) || \ + defined(OS_MAC) +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/browser_list.h" +#include "chrome/browser/ui/browser_list_observer.h" +#include "chrome/browser/ui/browser_window.h" +#define CAN_DELETE_PROFILE +#endif + +namespace signin_util { +namespace { + +constexpr char kSignoutSettingKey[] = "signout_setting"; + +#if defined(CAN_DELETE_PROFILE) +// Manager that presents the profile will be deleted dialog on the first active +// browser window. +class DeleteProfileDialogManager : public BrowserListObserver { + public: + class Delegate { + public: + // Called when the profile was marked for deletion. It is safe for the + // delegate to delete |manager| when this is called. + virtual void OnProfileDeleted(DeleteProfileDialogManager* manager) = 0; + }; + + DeleteProfileDialogManager(std::string primary_account_email, + Delegate* delegate) + : primary_account_email_(primary_account_email), delegate_(delegate) {} + + DeleteProfileDialogManager(const DeleteProfileDialogManager&) = delete; + DeleteProfileDialogManager& operator=(const DeleteProfileDialogManager&) = + delete; + + ~DeleteProfileDialogManager() override { BrowserList::RemoveObserver(this); } + + void PresentDialogOnAllBrowserWindows(Profile* profile) { + DCHECK(profile); + DCHECK(profile_path_.empty()); + profile_path_ = profile->GetPath(); + + BrowserList::AddObserver(this); + Browser* active_browser = chrome::FindLastActiveWithProfile(profile); + if (active_browser) + OnBrowserSetLastActive(active_browser); + } + + void OnBrowserSetLastActive(Browser* browser) override { + DCHECK(!profile_path_.empty()); + + if (profile_path_ != browser->profile()->GetPath()) + return; + + active_browser_ = browser; + + // Display the dialog on the next run loop as otherwise the dialog can block + // browser from displaying because the dialog creates a nested run loop. + // + // This happens because the browser window is not fully created yet when + // OnBrowserSetLastActive() is called. To finish the creation, the code + // needs to return from OnBrowserSetLastActive(). + // + // However, if we open a warning dialog from OnBrowserSetLastActive() + // synchronously, it will create a nested run loop that will not return + // from OnBrowserSetLastActive() until the dialog is dismissed. But the user + // cannot dismiss the dialog because the browser is not even shown! + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(&DeleteProfileDialogManager::ShowDeleteProfileDialog, + weak_factory_.GetWeakPtr(), browser)); + } + + // Called immediately after a browser becomes not active. + void OnBrowserNoLongerActive(Browser* browser) override { + if (active_browser_ == browser) + active_browser_ = nullptr; + } + + void OnBrowserRemoved(Browser* browser) override { + if (active_browser_ == browser) + active_browser_ = nullptr; + } + + private: + void ShowDeleteProfileDialog(Browser* browser) { + // Block opening dialog from nested task. + static bool is_dialog_shown = false; + if (is_dialog_shown) + return; + base::AutoReset auto_reset(&is_dialog_shown, true); + + // Check that |browser| is still active. + if (!active_browser_ || active_browser_ != browser) + return; + + // Show the dialog. + DCHECK(browser->window()->GetNativeWindow()); + chrome::MessageBoxResult result = chrome::ShowWarningMessageBox( + browser->window()->GetNativeWindow(), + l10n_util::GetStringUTF16(IDS_PROFILE_WILL_BE_DELETED_DIALOG_TITLE), + l10n_util::GetStringFUTF16( + IDS_PROFILE_WILL_BE_DELETED_DIALOG_DESCRIPTION, + base::ASCIIToUTF16(primary_account_email_), + base::ASCIIToUTF16( + gaia::ExtractDomainName(primary_account_email_)))); + + switch (result) { + case chrome::MessageBoxResult::MESSAGE_BOX_RESULT_NO: { + // If the warning dialog is automatically dismissed or the user closed + // the dialog by clicking on the close "X" button, then re-present the + // dialog (the user should not be able to interact with the browser + // window as the profile must be deleted). + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(&DeleteProfileDialogManager::ShowDeleteProfileDialog, + weak_factory_.GetWeakPtr(), browser)); + break; + } + case chrome::MessageBoxResult::MESSAGE_BOX_RESULT_YES: + webui::DeleteProfileAtPath( + profile_path_, + ProfileMetrics::DELETE_PROFILE_PRIMARY_ACCOUNT_NOT_ALLOWED); + delegate_->OnProfileDeleted(this); + // |this| may be destroyed at this point. Avoid using it. + break; + case chrome::MessageBoxResult::MESSAGE_BOX_RESULT_DEFERRED: + NOTREACHED() << "Message box must not return deferred result when run " + "synchronously"; + break; + } + } + + std::string primary_account_email_; + raw_ptr delegate_; + base::FilePath profile_path_; + raw_ptr active_browser_; + base::WeakPtrFactory weak_factory_{this}; +}; +#endif // defined(CAN_DELETE_PROFILE) + +// Per-profile manager for the signout allowed setting. +#if defined(CAN_DELETE_PROFILE) +class UserSignoutSetting : public base::SupportsUserData::Data, + public DeleteProfileDialogManager::Delegate { +#else +class UserSignoutSetting : public base::SupportsUserData::Data { +#endif // defined(CAN_DELETE_PROFILE) + public: + enum class State { kUndefined, kAllowed, kDisallowed }; + + // Fetch from Profile. Make and store if not already present. + static UserSignoutSetting* GetForProfile(Profile* profile) { + UserSignoutSetting* signout_setting = static_cast( + profile->GetUserData(kSignoutSettingKey)); + + if (!signout_setting) { + profile->SetUserData(kSignoutSettingKey, + std::make_unique()); + signout_setting = static_cast( + profile->GetUserData(kSignoutSettingKey)); + } + + return signout_setting; + } + + State state() const { return state_; } + void set_state(State state) { state_ = state; } + +#if defined(CAN_DELETE_PROFILE) + // Shows the delete profile dialog on the first browser active window. + void ShowDeleteProfileDialog(Profile* profile, const std::string& email) { + if (delete_profile_dialog_manager_) + return; + delete_profile_dialog_manager_ = + std::make_unique(email, this); + delete_profile_dialog_manager_->PresentDialogOnAllBrowserWindows(profile); + } + + void OnProfileDeleted(DeleteProfileDialogManager* dialog_manager) override { + DCHECK_EQ(delete_profile_dialog_manager_.get(), dialog_manager); + delete_profile_dialog_manager_.reset(); + } +#endif + + private: + State state_ = State::kUndefined; + +#if defined(CAN_DELETE_PROFILE) + std::unique_ptr delete_profile_dialog_manager_; +#endif +}; + +enum ForceSigninPolicyCache { + NOT_CACHED = 0, + ENABLE, + DISABLE +} g_is_force_signin_enabled_cache = NOT_CACHED; + +void SetForceSigninPolicy(bool enable) { + g_is_force_signin_enabled_cache = enable ? ENABLE : DISABLE; +} + +} // namespace + +ScopedForceSigninSetterForTesting::ScopedForceSigninSetterForTesting( + bool enable) { + SetForceSigninForTesting(enable); // IN-TEST +} + +ScopedForceSigninSetterForTesting::~ScopedForceSigninSetterForTesting() { + ResetForceSigninForTesting(); // IN-TEST +} + +bool IsForceSigninEnabled() { + if (g_is_force_signin_enabled_cache == NOT_CACHED) { + PrefService* prefs = g_browser_process->local_state(); + if (prefs) + SetForceSigninPolicy(prefs->GetBoolean(prefs::kForceBrowserSignin)); + else + return false; + } + return (g_is_force_signin_enabled_cache == ENABLE); +} + +void SetForceSigninForTesting(bool enable) { + SetForceSigninPolicy(enable); +} + +void ResetForceSigninForTesting() { + g_is_force_signin_enabled_cache = NOT_CACHED; +} + +bool IsUserSignoutAllowedForProfile(Profile* profile) { + return UserSignoutSetting::GetForProfile(profile)->state() == + UserSignoutSetting::State::kAllowed; +} + +void EnsureUserSignoutAllowedIsInitializedForProfile(Profile* profile) { + if (UserSignoutSetting::GetForProfile(profile)->state() == + UserSignoutSetting::State::kUndefined) { + SetUserSignoutAllowedForProfile(profile, true); + } +} + +void SetUserSignoutAllowedForProfile(Profile* profile, bool is_allowed) { + UserSignoutSetting::State new_state = + is_allowed ? UserSignoutSetting::State::kAllowed + : UserSignoutSetting::State::kDisallowed; + UserSignoutSetting::GetForProfile(profile)->set_state(new_state); +} + +void EnsurePrimaryAccountAllowedForProfile(Profile* profile) { +// All primary accounts are allowed on ChromeOS, so this method is a no-op on +// ChromeOS. +#if !BUILDFLAG(IS_CHROMEOS_ASH) + auto* identity_manager = IdentityManagerFactory::GetForProfile(profile); + if (!identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) + return; + + CoreAccountInfo primary_account = + identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSync); + if (profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed) && + signin::IsUsernameAllowedByPatternFromPrefs( + g_browser_process->local_state(), primary_account.email)) { + return; + } + + UserSignoutSetting* signout_setting = + UserSignoutSetting::GetForProfile(profile); + switch (signout_setting->state()) { + case UserSignoutSetting::State::kUndefined: + NOTREACHED(); + break; + case UserSignoutSetting::State::kAllowed: { + // Force clear the primary account if it is no longer allowed and if sign + // out is allowed. + auto* primary_account_mutator = + identity_manager->GetPrimaryAccountMutator(); + primary_account_mutator->ClearPrimaryAccount( + signin_metrics::SIGNIN_NOT_ALLOWED_ON_PROFILE_INIT, + signin_metrics::SignoutDelete::kIgnoreMetric); + break; + } + case UserSignoutSetting::State::kDisallowed: +#if defined(CAN_DELETE_PROFILE) + // Force remove the profile if sign out is not allowed and if the + // primary account is no longer allowed. + // This may be called while the profile is initializing, so it must be + // scheduled for later to allow the profile initialization to complete. + CHECK(profiles::IsMultipleProfilesEnabled()); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(&UserSignoutSetting::ShowDeleteProfileDialog, + base::Unretained(signout_setting), profile, + primary_account.email)); +#else + CHECK(false) << "Deleting profiles is not supported."; +#endif // defined(CAN_DELETE_PROFILE) + break; + } +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) +} + +#if !defined(OS_ANDROID) +bool ProfileSeparationEnforcedByPolicy( + Profile* profile, + const std::string& intercepted_account_level_policy_value) { + if (!base::FeatureList::IsEnabled(kAccountPoliciesLoadedWithoutSync)) + return false; + std::string current_profile_account_restriction = + profile->GetPrefs()->GetString(prefs::kManagedAccountsSigninRestriction); + + bool is_machine_level_policy = profile->GetPrefs()->GetBoolean( + prefs::kManagedAccountsSigninRestrictionScopeMachine); + + // Enforce profile separation for all new signins if any restriction is + // applied at a machine level. + if (is_machine_level_policy) { + return !current_profile_account_restriction.empty() && + current_profile_account_restriction != "none"; + } + + // Enforce profile separation for all new signins if "primary_account_strict" + // is set at the user account level. + return current_profile_account_restriction == "primary_account_strict" || + base::StartsWith(intercepted_account_level_policy_value, + "primary_account"); +} + +void RecordEnterpriseProfileCreationUserChoice(bool enforced_by_policy, + bool created) { + base::UmaHistogramBoolean( + enforced_by_policy + ? "Signin.Enterprise.WorkProfile.ProfileCreatedWithPolicySet" + : "Signin.Enterprise.WorkProfile.ProfileCreatedwithPolicyUnset", + created); +} + +#endif + +} // namespace signin_util diff --git a/chromium/chrome/browser/signin/signin_util.h b/chromium/chrome/browser/signin/signin_util.h new file mode 100644 index 00000000000..10a436d9ea1 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_util.h @@ -0,0 +1,73 @@ +// Copyright 2017 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 CHROME_BROWSER_SIGNIN_SIGNIN_UTIL_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_UTIL_H_ + +#include + +#include "build/build_config.h" + +class Profile; + +namespace signin_util { + +// This class calls ResetForceSigninForTesting when destroyed, so that +// ForcedSigning doesn't leak across tests. +class ScopedForceSigninSetterForTesting { + public: + explicit ScopedForceSigninSetterForTesting(bool enable); + ~ScopedForceSigninSetterForTesting(); +}; + +// Return whether the force sign in policy is enabled or not. +// The state of this policy will not be changed without relaunch Chrome. +bool IsForceSigninEnabled(); + +// Enable or disable force sign in for testing. Please use +// ScopedForceSigninSetterForTesting instead, if possible. If not, make sure +// ResetForceSigninForTesting is called before the test finishes. +void SetForceSigninForTesting(bool enable); + +// Reset force sign in to uninitialized state for testing. +void ResetForceSigninForTesting(); + +// Returns true if clearing the primary profile is allowed. +bool IsUserSignoutAllowedForProfile(Profile* profile); + +// Sign-out is allowed by default, but some Chrome profiles (e.g. for cloud- +// managed enterprise accounts) may wish to disallow user-initiated sign-out. +// Note that this exempts sign-outs that are not user-initiated (e.g. sign-out +// triggered when cloud policy no longer allows current email pattern). See +// ChromeSigninClient::PreSignOut(). +void SetUserSignoutAllowedForProfile(Profile* profile, bool is_allowed); + +// Updates the user sign-out state to |true| if is was never initialized. +// This should be called at the end of the flow to initialize a profile to +// ensure that the signout allowed flag is updated. +void EnsureUserSignoutAllowedIsInitializedForProfile(Profile* profile); + +// Ensures that the primary account for |profile| is allowed: +// * If profile does not have any primary account, then this is a no-op. +// * If |IsUserSignoutAllowedForProfile| is allowed and the primary account +// is no longer allowed, then this clears the primary account. +// * If |IsUserSignoutAllowedForProfile| is not allowed and the primary account +// is not longer allowed, then this removes the profile. +void EnsurePrimaryAccountAllowedForProfile(Profile* profile); + +#if !defined(OS_ANDROID) +// Returns true if profile separation is enforced by policy. +bool ProfileSeparationEnforcedByPolicy( + Profile* profile, + const std::string& intercepted_account_level_policy_value); + +// Records a UMA metric if the user accepts or not to create an enterprise +// profile. +void RecordEnterpriseProfileCreationUserChoice(bool enforced_by_policy, + bool created); +#endif + +} // namespace signin_util + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_UTIL_H_ diff --git a/chromium/chrome/browser/signin/signin_util_unittest.cc b/chromium/chrome/browser/signin/signin_util_unittest.cc new file mode 100644 index 00000000000..80862578339 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_util_unittest.cc @@ -0,0 +1,127 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/signin_util.h" + +#include + +#include "base/feature_list.h" +#include "build/buildflag.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/prefs/browser_prefs.h" +#include "chrome/browser/signin/signin_features.h" +#include "chrome/common/pref_names.h" +#include "chrome/test/base/browser_with_test_window_test.h" +#include "chrome/test/base/testing_browser_process.h" +#include "chrome/test/base/testing_profile.h" +#include "components/prefs/pref_service.h" +#include "components/prefs/testing_pref_service.h" + +class SigninUtilTest : public BrowserWithTestWindowTest { + public: + void SetUp() override { + BrowserWithTestWindowTest::SetUp(); + signin_util::ResetForceSigninForTesting(); + } + + void TearDown() override { + signin_util::ResetForceSigninForTesting(); + BrowserWithTestWindowTest::TearDown(); + } +}; + +TEST_F(SigninUtilTest, GetForceSigninPolicy) { + EXPECT_FALSE(signin_util::IsForceSigninEnabled()); + + g_browser_process->local_state()->SetBoolean(prefs::kForceBrowserSignin, + true); + signin_util::ResetForceSigninForTesting(); + EXPECT_TRUE(signin_util::IsForceSigninEnabled()); + g_browser_process->local_state()->SetBoolean(prefs::kForceBrowserSignin, + false); + signin_util::ResetForceSigninForTesting(); + EXPECT_FALSE(signin_util::IsForceSigninEnabled()); +} + +#if !BUILDFLAG(IS_CHROMEOS_LACROS) +class SigninUtilEnterpriseTest : public BrowserWithTestWindowTest { + public: + SigninUtilEnterpriseTest() + : feature_list_(kAccountPoliciesLoadedWithoutSync) {} + + private: + base::test::ScopedFeatureList feature_list_; +}; + +TEST_F(SigninUtilEnterpriseTest, ProfileSeparationEnforcedByPolicy) { + std::unique_ptr profile = TestingProfile::Builder().Build(); + + // No policy set on the active profile. + EXPECT_FALSE(signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), + std::string())); + EXPECT_FALSE( + signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), "none")); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy( + profile.get(), "primary_account")); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy( + profile.get(), "primary_account_strict")); + + // Active profile has "primary_account" as a user level policy. + profile->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account"); + profile->GetPrefs()->SetBoolean( + prefs::kManagedAccountsSigninRestrictionScopeMachine, false); + EXPECT_FALSE(signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), + std::string())); + EXPECT_FALSE( + signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), "none")); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy( + profile.get(), "primary_account")); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy( + profile.get(), "primary_account_strict")); + + // Active profile has "primary_account_strict" as a user level policy. + profile->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account_strict"); + profile->GetPrefs()->SetBoolean( + prefs::kManagedAccountsSigninRestrictionScopeMachine, false); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), + std::string())); + EXPECT_TRUE( + signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), "none")); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy( + profile.get(), "primary_account")); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy( + profile.get(), "primary_account_strict")); + + // Active profile has "primary_account" as a machine level policy. + profile->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account"); + profile->GetPrefs()->SetBoolean( + prefs::kManagedAccountsSigninRestrictionScopeMachine, true); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), + std::string())); + EXPECT_TRUE( + signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), "none")); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy( + profile.get(), "primary_account")); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy( + profile.get(), "primary_account_strict")); + + // Active profile has "primary_account_strict" as a machine level policy. + profile->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account"); + profile->GetPrefs()->SetBoolean( + prefs::kManagedAccountsSigninRestrictionScopeMachine, true); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), + std::string())); + EXPECT_TRUE( + signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), "none")); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy( + profile.get(), "primary_account")); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy( + profile.get(), "primary_account_strict")); +} +#endif diff --git a/chromium/chrome/browser/signin/signin_util_win.cc b/chromium/chrome/browser/signin/signin_util_win.cc new file mode 100644 index 00000000000..0ffc61db1b6 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_util_win.cc @@ -0,0 +1,332 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/signin_util_win.h" + +#include +#include +#include + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/no_destructor.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/win/registry.h" +#include "base/win/win_util.h" +#include "base/win/wincrypt_shim.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/first_run/first_run.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_attributes_entry.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/profiles/profile_window.h" +#include "chrome/browser/signin/about_signin_internals_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/browser_list.h" +#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h" +#include "chrome/browser/ui/webui/signin/signin_ui_error.h" +#include "chrome/browser/ui/webui/signin/signin_utils_desktop.h" +#include "chrome/credential_provider/common/gcp_strings.h" +#include "components/prefs/pref_service.h" +#include "components/signin/core/browser/about_signin_internals.h" +#include "components/signin/public/base/signin_metrics.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/accounts_mutator.h" +#include "components/signin/public/identity_manager/identity_manager.h" + +namespace signin_util { + +namespace { + +std::unique_ptr* +GetDiceTurnSyncOnHelperDelegateForTestingStorage() { + static base::NoDestructor> + delegate; + return delegate.get(); +} + +std::string DecryptRefreshToken(const std::string& cipher_text) { + DATA_BLOB input; + input.pbData = + const_cast(reinterpret_cast(cipher_text.data())); + input.cbData = static_cast(cipher_text.length()); + DATA_BLOB output; + BOOL result = ::CryptUnprotectData(&input, nullptr, nullptr, nullptr, nullptr, + CRYPTPROTECT_UI_FORBIDDEN, &output); + + if (!result) + return std::string(); + + std::string refresh_token(reinterpret_cast(output.pbData), + output.cbData); + ::LocalFree(output.pbData); + return refresh_token; +} + +// Finish the process of import credentials. This is either called directly +// from ImportCredentialsFromProvider() if a browser window for the profile is +// already available or is delayed until a browser can first be opened. +void FinishImportCredentialsFromProvider(const CoreAccountId& account_id, + Browser* browser, + Profile* profile, + Profile::CreateStatus status) { + // DiceTurnSyncOnHelper deletes itself once done. + if (GetDiceTurnSyncOnHelperDelegateForTestingStorage()->get()) { + new DiceTurnSyncOnHelper( + profile, signin_metrics::AccessPoint::ACCESS_POINT_MACHINE_LOGON, + signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT, + signin_metrics::Reason::kSigninPrimaryAccount, account_id, + DiceTurnSyncOnHelper::SigninAbortedMode::KEEP_ACCOUNT, + std::move(*GetDiceTurnSyncOnHelperDelegateForTestingStorage()), + base::DoNothing()); + } else { + if (!browser) + browser = chrome::FindLastActiveWithProfile(profile); + + new DiceTurnSyncOnHelper( + profile, browser, + signin_metrics::AccessPoint::ACCESS_POINT_MACHINE_LOGON, + signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT, + signin_metrics::Reason::kSigninPrimaryAccount, account_id, + DiceTurnSyncOnHelper::SigninAbortedMode::KEEP_ACCOUNT); + } +} + +// Start the process of importing credentials from the credential provider given +// that all the required information is available. The process depends on +// having a browser window for the profile. If a browser window exists the +// profile be signed in and sync will be starting up. If not, the profile will +// be still be signed in but sync will be started once the browser window is +// ready. +void ImportCredentialsFromProvider(Profile* profile, + const std::wstring& gaia_id, + const std::wstring& email, + const std::string& refresh_token, + bool turn_on_sync) { + // For debugging purposes, record that the credentials for this profile + // came from a credential provider. + AboutSigninInternals* signin_internals = + AboutSigninInternalsFactory::GetInstance()->GetForProfile(profile); + signin_internals->OnAuthenticationResultReceived("Credential Provider"); + + CoreAccountId account_id = + IdentityManagerFactory::GetForProfile(profile) + ->GetAccountsMutator() + ->AddOrUpdateAccount(base::WideToUTF8(gaia_id), + base::WideToUTF8(email), refresh_token, + /*is_under_advanced_protection=*/false, + signin_metrics::SourceForRefreshTokenOperation:: + kMachineLogon_CredentialProvider); + + if (turn_on_sync) { + Browser* browser = chrome::FindLastActiveWithProfile(profile); + if (browser) { + FinishImportCredentialsFromProvider(account_id, browser, profile, + Profile::CREATE_STATUS_CREATED); + } else { + // If no active browser exists yet, this profile is in the process of + // being created. Wait for the browser to be created before finishing the + // sign in. This object deletes itself when done. + new profiles::BrowserAddedForProfileObserver( + profile, base::BindRepeating(&FinishImportCredentialsFromProvider, + account_id, nullptr)); + } + } + + // Mark this profile as having been signed in with the credential provider. + profile->GetPrefs()->SetBoolean(prefs::kSignedInWithCredentialProvider, true); +} + +// Extracts the |cred_provider_gaia_id| and |cred_provider_email| for the user +// signed in throuhg credential provider. +void ExtractCredentialProviderUser(std::wstring* cred_provider_gaia_id, + std::wstring* cred_provider_email) { + DCHECK(cred_provider_gaia_id); + DCHECK(cred_provider_email); + + cred_provider_gaia_id->clear(); + cred_provider_email->clear(); + + base::win::RegKey key; + if (key.Open(HKEY_CURRENT_USER, credential_provider::kRegHkcuAccountsPath, + KEY_READ) != ERROR_SUCCESS) { + return; + } + + base::win::RegistryKeyIterator it(key.Handle(), L""); + if (!it.Valid() || it.SubkeyCount() != 1) + return; + + base::win::RegKey key_account(key.Handle(), it.Name(), KEY_QUERY_VALUE); + if (!key_account.Valid()) + return; + + std::wstring email; + if (key_account.ReadValue( + base::UTF8ToWide(credential_provider::kKeyEmail).c_str(), &email) != + ERROR_SUCCESS) { + return; + } + + *cred_provider_gaia_id = it.Name(); + *cred_provider_email = email; +} + +// Attempt to sign in with a credentials from a system installed credential +// provider if available. If |auth_gaia_id| is not empty then the system +// credential must be for the same account. Starts the process to turn on DICE +// only if |turn_on_sync| is true. +bool TrySigninWithCredentialProvider(Profile* profile, + const std::wstring& auth_gaia_id, + bool turn_on_sync) { + base::win::RegKey key; + if (key.Open(HKEY_CURRENT_USER, credential_provider::kRegHkcuAccountsPath, + KEY_READ) != ERROR_SUCCESS) { + return false; + } + + base::win::RegistryKeyIterator it(key.Handle(), L""); + if (!it.Valid() || it.SubkeyCount() == 0) + return false; + + base::win::RegKey key_account(key.Handle(), it.Name(), KEY_READ | KEY_WRITE); + if (!key_account.Valid()) + return false; + + std::wstring gaia_id = it.Name(); + if (!auth_gaia_id.empty() && auth_gaia_id != gaia_id) + return false; + + std::wstring email; + if (key_account.ReadValue( + base::UTF8ToWide(credential_provider::kKeyEmail).c_str(), &email) != + ERROR_SUCCESS) { + return false; + } + + // Read the encrypted refresh token. The data is stored in binary format. + // No matter what happens, delete the registry entry. + + std::string encrypted_refresh_token; + DWORD size = 0; + DWORD type; + if (key_account.ReadValue( + base::UTF8ToWide(credential_provider::kKeyRefreshToken).c_str(), + nullptr, &size, &type) != ERROR_SUCCESS) { + return false; + } + + encrypted_refresh_token.resize(size); + bool reauth_attempted = false; + key_account.ReadValue( + base::UTF8ToWide(credential_provider::kKeyRefreshToken).c_str(), + const_cast(encrypted_refresh_token.c_str()), &size, &type); + if (!gaia_id.empty() && !email.empty() && type == REG_BINARY && + !encrypted_refresh_token.empty()) { + std::string refresh_token = DecryptRefreshToken(encrypted_refresh_token); + if (!refresh_token.empty()) { + reauth_attempted = true; + ImportCredentialsFromProvider(profile, gaia_id, email, refresh_token, + turn_on_sync); + } + } + + key_account.DeleteValue( + base::UTF8ToWide(credential_provider::kKeyRefreshToken).c_str()); + return reauth_attempted; +} + +} // namespace + +void SetDiceTurnSyncOnHelperDelegateForTesting( + std::unique_ptr delegate) { + GetDiceTurnSyncOnHelperDelegateForTestingStorage()->swap(delegate); +} + +// Credential provider needs to stick to profile it previously used to import +// credentials. Thus, if there is another profile that was previously signed in +// with credential provider regardless of whether user signed in or out, +// credential provider shouldn't attempt to import credentials into current +// profile. +bool IsGCPWUsedInOtherProfile(Profile* profile) { + DCHECK(profile); + + ProfileManager* profile_manager = g_browser_process->profile_manager(); + if (profile_manager) { + std::vector entries = + profile_manager->GetProfileAttributesStorage() + .GetAllProfilesAttributes(); + + for (const ProfileAttributesEntry* entry : entries) { + if (entry->GetPath() == profile->GetPath()) + continue; + + if (entry->IsSignedInWithCredentialProvider()) + return true; + } + } + + return false; +} + +void SigninWithCredentialProviderIfPossible(Profile* profile) { + // This flow is used for first time signin through credential provider. Any + // subsequent signin for the credential provider user needs to go through + // reauth flow. + if (profile->GetPrefs()->GetBoolean(prefs::kSignedInWithCredentialProvider)) + return; + + std::wstring cred_provider_gaia_id; + std::wstring cred_provider_email; + + ExtractCredentialProviderUser(&cred_provider_gaia_id, &cred_provider_email); + if (cred_provider_gaia_id.empty() || cred_provider_email.empty()) + return; + + // Chrome doesn't allow signing into current profile if the same user is + // signed in another profile. + if (!CanOfferSignin(profile, base::WideToUTF8(cred_provider_gaia_id), + base::WideToUTF8(cred_provider_email)) + .IsOk() || + IsGCPWUsedInOtherProfile(profile)) { + return; + } + + auto* identity_manager = IdentityManagerFactory::GetForProfile(profile); + std::wstring gaia_id; + if (identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) { + gaia_id = base::UTF8ToWide( + identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSync) + .gaia); + } + + TrySigninWithCredentialProvider(profile, gaia_id, gaia_id.empty()); +} + +bool ReauthWithCredentialProviderIfPossible(Profile* profile) { + // Check to see if auto signin information is available. Only applies if: + // + // - The profile is marked as having been signed in with a system credential. + // - The profile is already signed in. + // - The profile is in an auth error state. + auto* identity_manager = IdentityManagerFactory::GetForProfile(profile); + if (!(profile->GetPrefs()->GetBoolean( + prefs::kSignedInWithCredentialProvider) && + identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync) && + identity_manager->HasAccountWithRefreshTokenInPersistentErrorState( + identity_manager->GetPrimaryAccountId( + signin::ConsentLevel::kSync)))) { + return false; + } + + std::wstring gaia_id = base::UTF8ToWide( + identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSync) + .gaia.c_str()); + return TrySigninWithCredentialProvider(profile, gaia_id, false); +} + +} // namespace signin_util diff --git a/chromium/chrome/browser/signin/signin_util_win.h b/chromium/chrome/browser/signin/signin_util_win.h new file mode 100644 index 00000000000..771085f9bbb --- /dev/null +++ b/chromium/chrome/browser/signin/signin_util_win.h @@ -0,0 +1,31 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_UTIL_WIN_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_UTIL_WIN_H_ + +#include + +#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h" + +class Profile; + +namespace signin_util { + +// Attempt to sign in with a credentials from a system installed credential +// provider if available. +void SigninWithCredentialProviderIfPossible(Profile* profile); + +// Attempt to reauthenticate with a credentials from a system installed +// credential provider if available. If a new authentication token was +// installed returns true. +bool ReauthWithCredentialProviderIfPossible(Profile* profile); + +// Sets the DiceTurnSyncOnHelper delegate for browser tests. +void SetDiceTurnSyncOnHelperDelegateForTesting( + std::unique_ptr delegate); + +} // namespace signin_util + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_UTIL_WIN_H_ diff --git a/chromium/chrome/browser/signin/signin_util_win_browsertest.cc b/chromium/chrome/browser/signin/signin_util_win_browsertest.cc new file mode 100644 index 00000000000..87832ab6e98 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_util_win_browsertest.cc @@ -0,0 +1,698 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include + +#include "base/bind.h" +#include "base/callback.h" +#include "base/command_line.h" +#include "base/run_loop.h" +#include "base/strings/utf_string_conversions.h" +#include "base/test/bind.h" +#include "base/test/scoped_feature_list.h" +#include "base/test/test_reg_util_win.h" +#include "base/win/wincrypt_shim.h" +#include "build/build_config.h" +#include "chrome/browser/first_run/first_run.h" +#include "chrome/browser/profiles/profile_attributes_entry.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/profiles/profile_window.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/signin_util_win.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/ui_features.h" +#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/common/pref_names.h" +#include "chrome/credential_provider/common/gcp_strings.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/testing_browser_process.h" +#include "components/prefs/pref_service.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_test_utils.h" +#include "components/signin/public/identity_manager/primary_account_mutator.h" +#include "content/public/test/browser_test.h" + +class SigninUIError; + +namespace { + +class TestDiceTurnSyncOnHelperDelegate : public DiceTurnSyncOnHelper::Delegate { + ~TestDiceTurnSyncOnHelperDelegate() override {} + + // DiceTurnSyncOnHelper::Delegate: + void ShowLoginError(const SigninUIError& error) override {} + void ShowMergeSyncDataConfirmation( + const std::string& previous_email, + const std::string& new_email, + DiceTurnSyncOnHelper::SigninChoiceCallback callback) override { + std::move(callback).Run(DiceTurnSyncOnHelper::SIGNIN_CHOICE_CONTINUE); + } + void ShowEnterpriseAccountConfirmation( + const AccountInfo& account_info, + DiceTurnSyncOnHelper::SigninChoiceCallback callback) override { + std::move(callback).Run(DiceTurnSyncOnHelper::SIGNIN_CHOICE_CONTINUE); + } + void ShowSyncConfirmation( + base::OnceCallback + callback) override { + std::move(callback).Run(LoginUIService::SYNC_WITH_DEFAULT_SETTINGS); + } + void ShowSyncDisabledConfirmation( + bool is_managed_account, + base::OnceCallback + callback) override {} + void ShowSyncSettings() override {} + void SwitchToProfile(Profile* new_profile) override {} +}; + +struct SigninUtilWinBrowserTestParams { + SigninUtilWinBrowserTestParams(bool is_first_run, + const std::wstring& gaia_id, + const std::wstring& email, + const std::string& refresh_token, + bool expect_is_started) + : is_first_run(is_first_run), + gaia_id(gaia_id), + email(email), + refresh_token(refresh_token), + expect_is_started(expect_is_started) {} + + bool is_first_run = false; + std::wstring gaia_id; + std::wstring email; + std::string refresh_token; + bool expect_is_started = false; +}; + +void AssertSigninStarted(bool expect_is_started, Profile* profile) { + ProfileManager* profile_manager = g_browser_process->profile_manager(); + + ProfileAttributesStorage& storage = + profile_manager->GetProfileAttributesStorage(); + + ProfileAttributesEntry* entry = + storage.GetProfileAttributesWithPath(profile->GetPath()); + + ASSERT_NE(entry, nullptr); + + ASSERT_EQ(expect_is_started, entry->IsSignedInWithCredentialProvider()); +} + +} // namespace + +class BrowserTestHelper { + public: + BrowserTestHelper(const std::wstring& gaia_id, + const std::wstring& email, + const std::string& refresh_token) + : gaia_id_(gaia_id), email_(email), refresh_token_(refresh_token) {} + + protected: + void CreateRegKey(base::win::RegKey* key) { + if (!gaia_id_.empty()) { + EXPECT_EQ( + ERROR_SUCCESS, + key->Create(HKEY_CURRENT_USER, + credential_provider::kRegHkcuAccountsPath, KEY_WRITE)); + EXPECT_EQ(ERROR_SUCCESS, key->CreateKey(gaia_id_.c_str(), KEY_WRITE)); + } + } + + void WriteRefreshToken(base::win::RegKey* key, + const std::string& refresh_token) { + EXPECT_TRUE(key->Valid()); + DATA_BLOB plaintext; + plaintext.pbData = + reinterpret_cast(const_cast(refresh_token.c_str())); + plaintext.cbData = static_cast(refresh_token.length()); + + DATA_BLOB ciphertext; + ASSERT_TRUE(::CryptProtectData(&plaintext, L"Gaia refresh token", nullptr, + nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, + &ciphertext)); + std::string encrypted_data(reinterpret_cast(ciphertext.pbData), + ciphertext.cbData); + EXPECT_EQ( + ERROR_SUCCESS, + key->WriteValue( + base::ASCIIToWide(credential_provider::kKeyRefreshToken).c_str(), + encrypted_data.c_str(), encrypted_data.length(), REG_BINARY)); + LocalFree(ciphertext.pbData); + } + + void ExpectRefreshTokenExists(bool exists) { + base::win::RegKey key; + EXPECT_EQ(ERROR_SUCCESS, + key.Open(HKEY_CURRENT_USER, + credential_provider::kRegHkcuAccountsPath, KEY_READ)); + EXPECT_EQ(ERROR_SUCCESS, key.OpenKey(gaia_id_.c_str(), KEY_READ)); + EXPECT_EQ( + exists, + key.HasValue( + base::ASCIIToWide(credential_provider::kKeyRefreshToken).c_str())); + } + + public: + void SetSigninUtilRegistry() { + base::win::RegKey key; + CreateRegKey(&key); + + if (!email_.empty()) { + EXPECT_TRUE(key.Valid()); + EXPECT_EQ(ERROR_SUCCESS, + key.WriteValue( + base::ASCIIToWide(credential_provider::kKeyEmail).c_str(), + email_.c_str())); + } + + if (!refresh_token_.empty()) + WriteRefreshToken(&key, refresh_token_); + } + + bool IsPreTest() { + std::string test_name = + ::testing::UnitTest::GetInstance()->current_test_info()->name(); + LOG(INFO) << "PRE_ test_name " << test_name; + return test_name.find("PRE_") != std::string::npos; + } + + bool IsPrePreTest() { + std::string test_name = + ::testing::UnitTest::GetInstance()->current_test_info()->name(); + LOG(INFO) << "PRE_PRE_ test_name " << test_name; + return test_name.find("PRE_PRE_") != std::string::npos; + } + + private: + std::wstring gaia_id_; + std::wstring email_; + std::string refresh_token_; +}; + +class SigninUtilWinBrowserTest + : public BrowserTestHelper, + public InProcessBrowserTest, + public testing::WithParamInterface { + public: + SigninUtilWinBrowserTest() + : BrowserTestHelper(GetParam().gaia_id, + GetParam().email, + GetParam().refresh_token) {} + + protected: + void SetUpCommandLine(base::CommandLine* command_line) override { + command_line->AppendSwitch(GetParam().is_first_run + ? switches::kForceFirstRun + : switches::kNoFirstRun); + } + + bool SetUpUserDataDirectory() override { + registry_override_.OverrideRegistry(HKEY_CURRENT_USER); + + signin_util::SetDiceTurnSyncOnHelperDelegateForTesting( + std::unique_ptr( + new TestDiceTurnSyncOnHelperDelegate())); + + SetSigninUtilRegistry(); + + return InProcessBrowserTest::SetUpUserDataDirectory(); + } + + private: + registry_util::RegistryOverrideManager registry_override_; +}; + +IN_PROC_BROWSER_TEST_P(SigninUtilWinBrowserTest, Run) { + ASSERT_EQ(GetParam().is_first_run, first_run::IsChromeFirstRun()); + + ProfileManager* profile_manager = g_browser_process->profile_manager(); + ASSERT_EQ(1u, profile_manager->GetNumberOfProfiles()); + + Profile* profile = profile_manager->GetLastUsedProfile(); + ASSERT_EQ(profile_manager->GetInitialProfileDir(), profile->GetBaseName()); + + Browser* browser = chrome::FindLastActiveWithProfile(profile); + ASSERT_NE(nullptr, browser); + + AssertSigninStarted(GetParam().expect_is_started, profile); + + // If a refresh token was specified and a sign in attempt was expected, make + // sure the refresh token was removed from the registry. + if (!GetParam().refresh_token.empty() && GetParam().expect_is_started) + ExpectRefreshTokenExists(false); +} + +IN_PROC_BROWSER_TEST_P(SigninUtilWinBrowserTest, ReauthNoop) { + ProfileManager* profile_manager = g_browser_process->profile_manager(); + ASSERT_EQ(1u, profile_manager->GetNumberOfProfiles()); + + Profile* profile = profile_manager->GetLastUsedProfile(); + + // Whether the profile was signed in with the credential provider or not, + // reauth should be a noop. + ASSERT_FALSE(signin_util::ReauthWithCredentialProviderIfPossible(profile)); +} + +IN_PROC_BROWSER_TEST_P(SigninUtilWinBrowserTest, NoReauthAfterSignout) { + ProfileManager* profile_manager = g_browser_process->profile_manager(); + ASSERT_EQ(1u, profile_manager->GetNumberOfProfiles()); + + Profile* profile = profile_manager->GetLastUsedProfile(); + + if (GetParam().expect_is_started) { + // Write a new refresh token. + base::win::RegKey key; + CreateRegKey(&key); + WriteRefreshToken(&key, "lst-new"); + ASSERT_FALSE(signin_util::ReauthWithCredentialProviderIfPossible(profile)); + + // Sign user out of browser. + auto* primary_account_mutator = + IdentityManagerFactory::GetForProfile(profile) + ->GetPrimaryAccountMutator(); + primary_account_mutator->RevokeSyncConsent( + signin_metrics::FORCE_SIGNOUT_ALWAYS_ALLOWED_FOR_TEST, + signin_metrics::SignoutDelete::kDeleted); + + // Even with a refresh token available, no reauth happens if the profile + // is signed out. + ASSERT_FALSE(signin_util::ReauthWithCredentialProviderIfPossible(profile)); + } +} + +IN_PROC_BROWSER_TEST_P(SigninUtilWinBrowserTest, FixReauth) { + ProfileManager* profile_manager = g_browser_process->profile_manager(); + + ASSERT_EQ(1u, profile_manager->GetNumberOfProfiles()); + + Profile* profile = profile_manager->GetLastUsedProfile(); + + if (GetParam().expect_is_started) { + // Write a new refresh token. This time reauth should work. + base::win::RegKey key; + CreateRegKey(&key); + WriteRefreshToken(&key, "lst-new"); + ASSERT_FALSE(signin_util::ReauthWithCredentialProviderIfPossible(profile)); + + // Make sure the profile stays signed in, but in an auth error state. + auto* identity_manager = IdentityManagerFactory::GetForProfile(profile); + signin::UpdatePersistentErrorOfRefreshTokenForAccount( + identity_manager, + identity_manager->GetPrimaryAccountId(signin::ConsentLevel::kSync), + GoogleServiceAuthError::FromInvalidGaiaCredentialsReason( + GoogleServiceAuthError::InvalidGaiaCredentialsReason:: + CREDENTIALS_REJECTED_BY_SERVER)); + + // If the profile remains signed in but is in an auth error state, + // reauth should happen. + ASSERT_TRUE(signin_util::ReauthWithCredentialProviderIfPossible(profile)); + } +} + +INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest1, + SigninUtilWinBrowserTest, + testing::Values(SigninUtilWinBrowserTestParams( + /*is_first_run=*/false, + /*gaia_id=*/std::wstring(), + /*email=*/std::wstring(), + /*refresh_token=*/std::string(), + /*expect_is_started=*/false))); + +INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest2, + SigninUtilWinBrowserTest, + testing::Values(SigninUtilWinBrowserTestParams( + /*is_first_run=*/true, + /*gaia_id=*/std::wstring(), + /*email=*/std::wstring(), + /*refresh_token=*/std::string(), + /*expect_is_started=*/false))); + +INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest3, + SigninUtilWinBrowserTest, + testing::Values(SigninUtilWinBrowserTestParams( + /*is_first_run=*/true, + /*gaia_id=*/L"gaia-123456", + /*email=*/std::wstring(), + /*refresh_token=*/std::string(), + /*expect_is_started=*/false))); + +INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest4, + SigninUtilWinBrowserTest, + testing::Values(SigninUtilWinBrowserTestParams( + /*is_first_run=*/true, + /*gaia_id=*/L"gaia-123456", + /*email=*/L"foo@gmail.com", + /*refresh_token=*/std::string(), + /*expect_is_started=*/false))); + +INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest5, + SigninUtilWinBrowserTest, + testing::Values(SigninUtilWinBrowserTestParams( + /*is_first_run=*/true, + /*gaia_id=*/L"gaia-123456", + /*email=*/L"foo@gmail.com", + /*refresh_token=*/"lst-123456", + /*expect_is_started=*/true))); + +INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest6, + SigninUtilWinBrowserTest, + testing::Values(SigninUtilWinBrowserTestParams( + /*is_first_run=*/false, + /*gaia_id=*/L"gaia-123456", + /*email=*/L"foo@gmail.com", + /*refresh_token=*/"lst-123456", + /*expect_is_started=*/true))); + +struct ExistingWinBrowserSigninUtilTestParams : SigninUtilWinBrowserTestParams { + ExistingWinBrowserSigninUtilTestParams( + const std::wstring& gaia_id, + const std::wstring& email, + const std::string& refresh_token, + const std::wstring& existing_email, + bool expect_is_started) + : SigninUtilWinBrowserTestParams(false, + gaia_id, + email, + refresh_token, + expect_is_started), + existing_email(existing_email) {} + + std::wstring existing_email; +}; + +class ExistingWinBrowserSigninUtilTest + : public BrowserTestHelper, + public InProcessBrowserTest, + public testing::WithParamInterface< + ExistingWinBrowserSigninUtilTestParams> { + public: + ExistingWinBrowserSigninUtilTest() + : BrowserTestHelper(GetParam().gaia_id, + GetParam().email, + GetParam().refresh_token) {} + + protected: + bool SetUpUserDataDirectory() override { + registry_override_.OverrideRegistry(HKEY_CURRENT_USER); + + signin_util::SetDiceTurnSyncOnHelperDelegateForTesting( + std::unique_ptr( + new TestDiceTurnSyncOnHelperDelegate())); + if (!IsPreTest()) + SetSigninUtilRegistry(); + + return InProcessBrowserTest::SetUpUserDataDirectory(); + } + + private: + registry_util::RegistryOverrideManager registry_override_; +}; + +IN_PROC_BROWSER_TEST_P(ExistingWinBrowserSigninUtilTest, + PRE_ExistingWinBrowser) { + ProfileManager* profile_manager = g_browser_process->profile_manager(); + Profile* profile = profile_manager->GetLastUsedProfile(); + + ASSERT_EQ(profile_manager->GetInitialProfileDir(), profile->GetBaseName()); + + if (!GetParam().existing_email.empty()) { + auto* identity_manager = IdentityManagerFactory::GetForProfile(profile); + + ASSERT_TRUE(identity_manager); + + signin::MakePrimaryAccountAvailable( + identity_manager, base::WideToUTF8(GetParam().existing_email), + signin::ConsentLevel::kSync); + + ASSERT_TRUE( + identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)); + } +} + +IN_PROC_BROWSER_TEST_P(ExistingWinBrowserSigninUtilTest, ExistingWinBrowser) { + ProfileManager* profile_manager = g_browser_process->profile_manager(); + ASSERT_EQ(1u, profile_manager->GetNumberOfProfiles()); + + Profile* profile = profile_manager->GetLastUsedProfile(); + ASSERT_EQ(profile_manager->GetInitialProfileDir(), profile->GetBaseName()); + + AssertSigninStarted(GetParam().expect_is_started, profile); + + // If a refresh token was specified and a sign in attempt was expected, make + // sure the refresh token was removed from the registry. + if (!GetParam().refresh_token.empty() && GetParam().expect_is_started) + ExpectRefreshTokenExists(false); +} + +INSTANTIATE_TEST_SUITE_P(AllowSubsequentRun, + ExistingWinBrowserSigninUtilTest, + testing::Values(ExistingWinBrowserSigninUtilTestParams( + /*gaia_id=*/L"gaia-123456", + /*email=*/L"foo@gmail.com", + /*refresh_token=*/"lst-123456", + /*existing_email=*/std::wstring(), + /*expect_is_started=*/true))); + +INSTANTIATE_TEST_SUITE_P(OnlyAllowProfileWithNoPrimaryAccount, + ExistingWinBrowserSigninUtilTest, + testing::Values(ExistingWinBrowserSigninUtilTestParams( + /*gaia_id=*/L"gaia_id_for_foo_gmail.com", + /*email=*/L"foo@gmail.com", + /*refresh_token=*/"lst-123456", + /*existing_email=*/L"bar@gmail.com", + /*expect_is_started=*/false))); + +INSTANTIATE_TEST_SUITE_P(AllowProfileWithPrimaryAccount_DifferentUser, + ExistingWinBrowserSigninUtilTest, + testing::Values(ExistingWinBrowserSigninUtilTestParams( + /*gaia_id=*/L"gaia_id_for_foo_gmail.com", + /*email=*/L"foo@gmail.com", + /*refresh_token=*/"lst-123456", + /*existing_email=*/L"bar@gmail.com", + /*expect_is_started=*/false))); + + +INSTANTIATE_TEST_SUITE_P(AllowProfileWithPrimaryAccount_SameUser, + ExistingWinBrowserSigninUtilTest, + testing::Values(ExistingWinBrowserSigninUtilTestParams( + /*gaia_id=*/L"gaia_id_for_foo_gmail.com", + /*email=*/L"foo@gmail.com", + /*refresh_token=*/"lst-123456", + /*existing_email=*/L"foo@gmail.com", + /*expect_is_started=*/true))); + +void UnblockOnProfileInitialized(base::OnceClosure quit_closure, + Profile* profile, + Profile::CreateStatus status) { + // If the status is CREATE_STATUS_CREATED, then the function will be called + // again with CREATE_STATUS_INITIALIZED. + if (status == Profile::CREATE_STATUS_CREATED) + return; + + EXPECT_EQ(Profile::CREATE_STATUS_INITIALIZED, status); + std::move(quit_closure).Run(); +} + +void CreateAndSwitchToProfile(const std::string& basepath) { + ProfileManager* profile_manager = g_browser_process->profile_manager(); + ASSERT_TRUE(profile_manager); + + base::FilePath path = profile_manager->user_data_dir().AppendASCII(basepath); + base::RunLoop run_loop; + profile_manager->CreateProfileAsync( + path, base::BindRepeating(&UnblockOnProfileInitialized, + run_loop.QuitClosure())); + // Run the message loop to allow profile initialization to take place; the + // loop is terminated by UnblockOnProfileInitialized. + run_loop.Run(); + + profiles::SwitchToProfile(path, false, ProfileManager::CreateCallback()); +} + +struct ExistingWinBrowserProfilesSigninUtilTestParams { + ExistingWinBrowserProfilesSigninUtilTestParams( + const std::wstring& email_in_other_profile, + bool cred_provider_used_other_profile, + const std::wstring& current_profile, + const std::wstring& email_in_current_profile, + bool expect_is_started) + : email_in_other_profile(email_in_other_profile), + cred_provider_used_other_profile(cred_provider_used_other_profile), + current_profile(current_profile), + email_in_current_profile(email_in_current_profile), + expect_is_started(expect_is_started) {} + + std::wstring email_in_other_profile; + bool cred_provider_used_other_profile; + std::wstring current_profile; + std::wstring email_in_current_profile; + bool expect_is_started; +}; + +class ExistingWinBrowserProfilesSigninUtilTest + : public BrowserTestHelper, + public InProcessBrowserTest, + public testing::WithParamInterface< + ExistingWinBrowserProfilesSigninUtilTestParams> { + public: + ExistingWinBrowserProfilesSigninUtilTest() + : BrowserTestHelper(L"gaia_id_for_foo_gmail.com", + L"foo@gmail.com", + "lst-123456") {} + + protected: + bool SetUpUserDataDirectory() override { + registry_override_.OverrideRegistry(HKEY_CURRENT_USER); + + signin_util::SetDiceTurnSyncOnHelperDelegateForTesting( + std::unique_ptr( + new TestDiceTurnSyncOnHelperDelegate())); + if (!IsPreTest()) { + SetSigninUtilRegistry(); + } else if (IsPrePreTest() && GetParam().cred_provider_used_other_profile) { + BrowserTestHelper(L"gaia_id_for_bar_gmail.com", L"bar@gmail.com", + "lst-123456") + .SetSigninUtilRegistry(); + } + + return InProcessBrowserTest::SetUpUserDataDirectory(); + } + + private: + base::test::ScopedFeatureList feature_list_; + registry_util::RegistryOverrideManager registry_override_; +}; + +// In PRE_PRE_Run, browser starts for the first time with the initial profile +// dir. If needed by the test, this step can set |email_in_other_profile| as the +// primary account in the profile or it can sign in with credential provider, +// but before this step ends, |current_profile| is created and browser switches +// to that profile just to prepare the browser for the next step. +IN_PROC_BROWSER_TEST_P(ExistingWinBrowserProfilesSigninUtilTest, PRE_PRE_Run) { + g_browser_process->local_state()->SetBoolean( + prefs::kBrowserShowProfilePickerOnStartup, false); + + ProfileManager* profile_manager = g_browser_process->profile_manager(); + + Profile* profile = profile_manager->GetLastUsedProfile(); + + ASSERT_EQ(profile_manager->GetInitialProfileDir(), profile->GetBaseName()); + + auto* identity_manager = IdentityManagerFactory::GetForProfile(profile); + ASSERT_TRUE(identity_manager); + ASSERT_TRUE( + identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync) == + GetParam().cred_provider_used_other_profile); + + if (!GetParam().cred_provider_used_other_profile && + !GetParam().email_in_other_profile.empty()) { + signin::MakePrimaryAccountAvailable( + identity_manager, base::WideToUTF8(GetParam().email_in_other_profile), + signin::ConsentLevel::kSync); + + ASSERT_TRUE( + identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)); + } + + CreateAndSwitchToProfile(base::WideToUTF8(GetParam().current_profile)); +} + +// Browser starts with the |current_profile| profile created in the previous +// step. If needed by the test, this step can set |email_in_current_profile| as +// the primary account in the profile. +IN_PROC_BROWSER_TEST_P(ExistingWinBrowserProfilesSigninUtilTest, PRE_Run) { + ProfileManager* profile_manager = g_browser_process->profile_manager(); + Profile* profile = profile_manager->GetLastUsedProfile(); + + ASSERT_EQ(GetParam().current_profile, profile->GetBaseName().value()); + + auto* identity_manager = IdentityManagerFactory::GetForProfile(profile); + ASSERT_TRUE(identity_manager); + ASSERT_FALSE( + identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)); + + if (!GetParam().email_in_current_profile.empty()) { + signin::MakePrimaryAccountAvailable( + identity_manager, base::WideToUTF8(GetParam().email_in_current_profile), + signin::ConsentLevel::kSync); + + ASSERT_TRUE( + identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)); + } +} + +// Before this step runs, refresh token is written into fake registry. Browser +// starts with the |current_profile| profile. Depending on the test case, +// profile may have a primary account. Similarly the other profile(initial +// profile in this case) may have a primary account as well. +IN_PROC_BROWSER_TEST_P(ExistingWinBrowserProfilesSigninUtilTest, Run) { + ProfileManager* profile_manager = g_browser_process->profile_manager(); + Profile* profile = profile_manager->GetLastUsedProfile(); + + ASSERT_EQ(GetParam().current_profile, profile->GetBaseName().value()); + AssertSigninStarted(GetParam().expect_is_started, profile); +} + +INSTANTIATE_TEST_SUITE_P( + AllowCurrentProfile_NoUserSignedIn, + ExistingWinBrowserProfilesSigninUtilTest, + testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams( + /*email_in_other_profile*/ L"", + /*cred_provider_used_other_profile*/ false, + /*current_profile*/ L"profile1", + /*email_in_current_profile=*/L"", + /*expect_is_started=*/true))); + +INSTANTIATE_TEST_SUITE_P( + AllowCurrentProfile_SameUserSignedIn, + ExistingWinBrowserProfilesSigninUtilTest, + testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams( + /*email_in_other_profile*/ L"", + /*cred_provider_used_other_profile*/ false, + /*current_profile*/ L"profile1", + /*email_in_current_profile=*/L"foo@gmail.com", + /*expect_is_started=*/true))); + +INSTANTIATE_TEST_SUITE_P( + DisallowCurrentProfile_DifferentUserSignedIn, + ExistingWinBrowserProfilesSigninUtilTest, + testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams( + /*email_in_other_profile*/ L"", + /*cred_provider_used_other_profile*/ false, + /*current_profile*/ L"profile1", + /*email_in_current_profile=*/L"bar@gmail.com", + /*expect_is_started=*/false))); + +INSTANTIATE_TEST_SUITE_P( + DisallowCurrentProfile_SameUserSignedInDefaultProfile, + ExistingWinBrowserProfilesSigninUtilTest, + testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams( + /*email_in_other_profile*/ L"foo@gmail.com", + /*cred_provider_used_other_profile*/ false, + /*current_profile*/ L"profile1", + /*email_in_current_profile=*/L"", + /*expect_is_started=*/false))); + +INSTANTIATE_TEST_SUITE_P( + AllowCurrentProfile_DifferentUserSignedInDefaultProfile, + ExistingWinBrowserProfilesSigninUtilTest, + testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams( + /*email_in_other_profile*/ L"bar@gmail.com", + /*cred_provider_used_other_profile*/ false, + /*current_profile*/ L"profile1", + /*email_in_current_profile=*/L"", + /*expect_is_started=*/true))); + +INSTANTIATE_TEST_SUITE_P( + DisallowCurrentProfile_CredProviderUsedDefaultProfile, + ExistingWinBrowserProfilesSigninUtilTest, + testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams( + /*email_in_other_profile*/ L"", + /*cred_provider_used_other_profile*/ true, + /*current_profile*/ L"profile1", + /*email_in_current_profile=*/L"", + /*expect_is_started=*/false))); diff --git a/chromium/chrome/browser/signin/test_signin_client_builder.cc b/chromium/chrome/browser/signin/test_signin_client_builder.cc new file mode 100644 index 00000000000..9795a5e7e85 --- /dev/null +++ b/chromium/chrome/browser/signin/test_signin_client_builder.cc @@ -0,0 +1,18 @@ +// 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 "chrome/browser/signin/test_signin_client_builder.h" + +#include "chrome/browser/profiles/profile.h" +#include "components/signin/public/base/test_signin_client.h" + +namespace signin { + +std::unique_ptr BuildTestSigninClient( + content::BrowserContext* context) { + return std::make_unique( + static_cast(context)->GetPrefs()); +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/test_signin_client_builder.h b/chromium/chrome/browser/signin/test_signin_client_builder.h new file mode 100644 index 00000000000..3863c510150 --- /dev/null +++ b/chromium/chrome/browser/signin/test_signin_client_builder.h @@ -0,0 +1,26 @@ +// 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 CHROME_BROWSER_SIGNIN_TEST_SIGNIN_CLIENT_BUILDER_H_ +#define CHROME_BROWSER_SIGNIN_TEST_SIGNIN_CLIENT_BUILDER_H_ + +#include + +class KeyedService; + +namespace content { +class BrowserContext; +} + +namespace signin { + +// Method to be used by the |ChromeSigninClientFactory| to create a test version +// of the SigninClient +std::unique_ptr BuildTestSigninClient( + content::BrowserContext* context); + +} // namespace signin + + +#endif // CHROME_BROWSER_SIGNIN_TEST_SIGNIN_CLIENT_BUILDER_H_ diff --git a/chromium/chrome/browser/signin/token_revoker_test_utils.cc b/chromium/chrome/browser/signin/token_revoker_test_utils.cc new file mode 100644 index 00000000000..69b26c881b1 --- /dev/null +++ b/chromium/chrome/browser/signin/token_revoker_test_utils.cc @@ -0,0 +1,36 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/signin/token_revoker_test_utils.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/net/system_network_context_manager.h" +#include "content/public/test/test_utils.h" +#include "google_apis/gaia/gaia_auth_fetcher.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" + +namespace token_revoker_test_utils { + +RefreshTokenRevoker::RefreshTokenRevoker() + : gaia_fetcher_(this, + gaia::GaiaSource::kChrome, + g_browser_process->system_network_context_manager() + ->GetSharedURLLoaderFactory()) {} + +RefreshTokenRevoker::~RefreshTokenRevoker() { +} + +void RefreshTokenRevoker::Revoke(const std::string& token) { + DVLOG(1) << "Starting RefreshTokenRevoker for token: " << token; + gaia_fetcher_.StartRevokeOAuth2Token(token); + message_loop_runner_ = new content::MessageLoopRunner; + message_loop_runner_->Run(); +} + +void RefreshTokenRevoker::OnOAuth2RevokeTokenCompleted( + GaiaAuthConsumer::TokenRevocationStatus status) { + DVLOG(1) << "TokenRevoker OnOAuth2RevokeTokenCompleted"; + message_loop_runner_->Quit(); +} + +} // namespace token_revoker_test_utils diff --git a/chromium/chrome/browser/signin/token_revoker_test_utils.h b/chromium/chrome/browser/signin/token_revoker_test_utils.h new file mode 100644 index 00000000000..2c1b08a257a --- /dev/null +++ b/chromium/chrome/browser/signin/token_revoker_test_utils.h @@ -0,0 +1,42 @@ +// Copyright 2016 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 CHROME_BROWSER_SIGNIN_TOKEN_REVOKER_TEST_UTILS_H_ +#define CHROME_BROWSER_SIGNIN_TOKEN_REVOKER_TEST_UTILS_H_ + +#include "base/memory/ref_counted.h" +#include "google_apis/gaia/gaia_auth_fetcher.h" + +namespace content { +class MessageLoopRunner; +} + +namespace token_revoker_test_utils { + +// A helper class that takes care of asynchronously revoking a refresh token. +class RefreshTokenRevoker : public GaiaAuthConsumer { + public: + RefreshTokenRevoker(); + + RefreshTokenRevoker(const RefreshTokenRevoker&) = delete; + RefreshTokenRevoker& operator=(const RefreshTokenRevoker&) = delete; + + ~RefreshTokenRevoker() override; + + // Sends a request to Gaia servers to revoke the refresh token. Blocks until + // it is revoked, i.e. until OnOAuth2RevokeTokenCompleted is fired. + void Revoke(const std::string& token); + + // Called when token is revoked. + void OnOAuth2RevokeTokenCompleted( + GaiaAuthConsumer::TokenRevocationStatus status) override; + + private: + GaiaAuthFetcher gaia_fetcher_; + scoped_refptr message_loop_runner_; +}; + +} // namespace token_revoker_test_utils + +#endif // CHROME_BROWSER_SIGNIN_TOKEN_REVOKER_TEST_UTILS_H_ diff --git a/chromium/components/gcm_driver/DEPS b/chromium/components/gcm_driver/DEPS new file mode 100644 index 00000000000..45c75405fd0 --- /dev/null +++ b/chromium/components/gcm_driver/DEPS @@ -0,0 +1,27 @@ +include_rules = [ + "+components/crx_file/id_util.h", + "+components/os_crypt", + "+components/keyed_service", + "+components/pref_registry", + "+components/prefs", + "+components/signin/public", + "+components/version_info", + "+content/public/browser/browser_context.h", + "+content/public/test", # Only used for tests. + "+crypto", # Only used for tests. + + # Whitelist specific headers from //google_apis/gaia. Contact + # blundell@chromium.org if looking to add more. + "+google_apis/gaia/core_account_id.h", + "+google_apis/gaia/gaia_constants.h", + "+google_apis/gaia/gaia_oauth_client.h", + "+google_apis/gaia/google_service_auth_error.h", + "+google_apis/gcm", + "+mojo/public", + "+net", + "+services/network/public/cpp", + "+services/network/public/mojom", + "+services/network/test", + "+third_party/leveldatabase", # Only used for tests. + "+third_party/re2", # Only used for tests. +] diff --git a/chromium/components/gcm_driver/DIR_METADATA b/chromium/components/gcm_driver/DIR_METADATA new file mode 100644 index 00000000000..fd2070aede7 --- /dev/null +++ b/chromium/components/gcm_driver/DIR_METADATA @@ -0,0 +1,4 @@ +monorail { + component: "Services>CloudMessaging" +} +team_email: "platform-capabilities@chromium.org" diff --git a/chromium/components/gcm_driver/OWNERS b/chromium/components/gcm_driver/OWNERS new file mode 100644 index 00000000000..4dfe4876e05 --- /dev/null +++ b/chromium/components/gcm_driver/OWNERS @@ -0,0 +1,2 @@ +fgorski@chromium.org +peter@chromium.org diff --git a/chromium/components/gcm_driver/account_tracker.cc b/chromium/components/gcm_driver/account_tracker.cc new file mode 100644 index 00000000000..20b821ee9e8 --- /dev/null +++ b/chromium/components/gcm_driver/account_tracker.cc @@ -0,0 +1,147 @@ +// 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 "components/gcm_driver/account_tracker.h" + +#include "base/containers/contains.h" +#include "base/logging.h" +#include "base/trace_event/trace_event.h" +#include "components/signin/public/identity_manager/access_token_info.h" + +namespace gcm { + +AccountTracker::AccountTracker(signin::IdentityManager* identity_manager) + : identity_manager_(identity_manager), shutdown_called_(false) { + identity_manager_->AddObserver(this); +} + +AccountTracker::~AccountTracker() { + DCHECK(shutdown_called_); +} + +void AccountTracker::Shutdown() { + shutdown_called_ = true; + identity_manager_->RemoveObserver(this); +} + +void AccountTracker::AddObserver(Observer* observer) { + observer_list_.AddObserver(observer); +} + +void AccountTracker::RemoveObserver(Observer* observer) { + observer_list_.RemoveObserver(observer); +} + +std::vector AccountTracker::GetAccounts() const { + const CoreAccountId active_account_id = + identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSync); + std::vector accounts; + + for (auto it = accounts_.begin(); it != accounts_.end(); ++it) { + const AccountState& state = it->second; + DCHECK(!state.account.account_id.empty()); + DCHECK(!state.account.gaia.empty()); + DCHECK(!state.account.email.empty()); + bool is_visible = state.is_signed_in; + + if (it->first == active_account_id) { + if (is_visible) + accounts.insert(accounts.begin(), state.account); + else + return std::vector(); + } else if (is_visible) { + accounts.push_back(state.account); + } + } + return accounts; +} + +void AccountTracker::OnRefreshTokenUpdatedForAccount( + const CoreAccountInfo& account_info) { + TRACE_EVENT1("identity", "AccountTracker::OnRefreshTokenUpdatedForAccount", + "account_id", account_info.account_id.ToString()); + + // Ignore refresh tokens if there is no active account ID at all. + if (!identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync)) + return; + + DVLOG(1) << "AVAILABLE " << account_info.account_id; + StartTrackingAccount(account_info); + UpdateSignInState(account_info.account_id, /*is_signed_in=*/true); +} + +void AccountTracker::OnRefreshTokenRemovedForAccount( + const CoreAccountId& account_id) { + TRACE_EVENT1("identity", "AccountTracker::OnRefreshTokenRemovedForAccount", + "account_id", account_id.ToString()); + + DVLOG(1) << "REVOKED " << account_id; + UpdateSignInState(account_id, /*is_signed_in=*/false); +} + +void AccountTracker::OnPrimaryAccountChanged( + const signin::PrimaryAccountChangeEvent& event) { + switch (event.GetEventTypeFor(signin::ConsentLevel::kSync)) { + case signin::PrimaryAccountChangeEvent::Type::kSet: { + TRACE_EVENT0("identity", "AccountTracker::OnPrimaryAccountSet"); + std::vector accounts = + identity_manager_->GetAccountsWithRefreshTokens(); + DVLOG(1) << "LOGIN " << accounts.size() << " accounts available."; + for (const CoreAccountInfo& account_info : accounts) { + StartTrackingAccount(account_info); + UpdateSignInState(account_info.account_id, /*is_signed_in=*/true); + } + break; + } + case signin::PrimaryAccountChangeEvent::Type::kCleared: { + TRACE_EVENT0("identity", "AccountTracker::OnPrimaryAccountCleared"); + DVLOG(1) << "LOGOUT"; + StopTrackingAllAccounts(); + break; + } + case signin::PrimaryAccountChangeEvent::Type::kNone: + break; + } +} + +void AccountTracker::UpdateSignInState(const CoreAccountId& account_id, + bool is_signed_in) { + if (!is_signed_in && !base::Contains(accounts_, account_id)) + return; + + DCHECK(base::Contains(accounts_, account_id)); + AccountState& account = accounts_[account_id]; + if (account.is_signed_in == is_signed_in) + return; + + account.is_signed_in = is_signed_in; + for (auto& observer : observer_list_) + observer.OnAccountSignInChanged(account.account, account.is_signed_in); +} + +void AccountTracker::StartTrackingAccount(const CoreAccountInfo& account) { + if (base::Contains(accounts_, account.account_id)) + return; + + DVLOG(1) << "StartTracking " << account.account_id; + AccountState account_state; + account_state.account = account; + account_state.is_signed_in = false; + accounts_.insert(std::make_pair(account.account_id, account_state)); +} + +void AccountTracker::StopTrackingAccount(const CoreAccountId account_id) { + DVLOG(1) << "StopTracking " << account_id; + if (base::Contains(accounts_, account_id)) { + UpdateSignInState(account_id, /*is_signed_in=*/false); + accounts_.erase(account_id); + } +} + +void AccountTracker::StopTrackingAllAccounts() { + while (!accounts_.empty()) + StopTrackingAccount(accounts_.begin()->first); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/account_tracker.h b/chromium/components/gcm_driver/account_tracker.h new file mode 100644 index 00000000000..61c5def0ddd --- /dev/null +++ b/chromium/components/gcm_driver/account_tracker.h @@ -0,0 +1,78 @@ +// 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 COMPONENTS_GCM_DRIVER_ACCOUNT_TRACKER_H_ +#define COMPONENTS_GCM_DRIVER_ACCOUNT_TRACKER_H_ + +#include +#include +#include + +#include "base/memory/raw_ptr.h" +#include "base/observer_list.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "google_apis/gaia/core_account_id.h" + +namespace gcm { + +// The AccountTracker keeps track of what accounts exist on the +// profile and the state of their credentials. +class AccountTracker : public signin::IdentityManager::Observer { + public: + explicit AccountTracker(signin::IdentityManager* identity_manager); + ~AccountTracker() override; + + class Observer { + public: + virtual void OnAccountSignInChanged(const CoreAccountInfo& account, + 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 account + // have been fetched. The primary account for the profile will be first + // in the vector. Additional accounts will be in order of their gaia account. + std::vector GetAccounts() const; + + private: + struct AccountState { + CoreAccountInfo account; + bool is_signed_in; + }; + + // signin::IdentityManager::Observer implementation. + void OnPrimaryAccountChanged( + const signin::PrimaryAccountChangeEvent& event) override; + void OnRefreshTokenUpdatedForAccount( + const CoreAccountInfo& account_info) override; + void OnRefreshTokenRemovedForAccount( + const CoreAccountId& account_id) override; + + // Add |account_info| to the lists of accounts tracked by this AccountTracker. + void StartTrackingAccount(const CoreAccountInfo& account_info); + + // Stops tracking |account_id|. Notifies all observers if the account was + // previously signed in. + void StopTrackingAccount(const CoreAccountId account_id); + + // Stops tracking all accounts. + void StopTrackingAllAccounts(); + + // Updates the is_signed_in corresponding to the given account. Notifies all + // observers of the signed in state changes. + void UpdateSignInState(const CoreAccountId& account_id, bool is_signed_in); + + raw_ptr identity_manager_; + std::map accounts_; + base::ObserverList::Unchecked observer_list_; + bool shutdown_called_; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_ACCOUNT_TRACKER_H_ diff --git a/chromium/components/gcm_driver/account_tracker_unittest.cc b/chromium/components/gcm_driver/account_tracker_unittest.cc new file mode 100644 index 00000000000..4901d75672b --- /dev/null +++ b/chromium/components/gcm_driver/account_tracker_unittest.cc @@ -0,0 +1,539 @@ +// 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 "components/gcm_driver/account_tracker.h" + +#include +#include +#include + +#include "base/strings/stringprintf.h" +#include "base/test/task_environment.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +const char kPrimaryAccountEmail[] = "primary_account@example.com"; + +enum TrackingEventType { SIGN_IN, SIGN_OUT }; + +class TrackingEvent { + public: + TrackingEvent(TrackingEventType type, + const CoreAccountId& account_id, + const std::string& gaia_id) + : type_(type), account_id_(account_id), gaia_id_(gaia_id) {} + + TrackingEvent(TrackingEventType type, const CoreAccountInfo& account_info) + : type_(type), + account_id_(account_info.account_id), + gaia_id_(account_info.gaia) {} + + bool operator==(const TrackingEvent& event) const { + return type_ == event.type_ && account_id_ == event.account_id_ && + gaia_id_ == event.gaia_id_; + } + + std::string ToString() const { + const char* typestr = "INVALID"; + switch (type_) { + case SIGN_IN: + typestr = " IN"; + break; + case SIGN_OUT: + typestr = "OUT"; + break; + } + return base::StringPrintf("{ type: %s, account_id: %s, gaia: %s }", typestr, + account_id_.ToString().c_str(), gaia_id_.c_str()); + } + + private: + friend bool CompareByUser(TrackingEvent a, TrackingEvent b); + + TrackingEventType type_; + CoreAccountId account_id_; + std::string gaia_id_; +}; + +bool CompareByUser(TrackingEvent a, TrackingEvent b) { + return a.account_id_ < b.account_id_; +} + +std::string Str(const std::vector& events) { + std::string str = "["; + bool needs_comma = false; + for (auto it = events.begin(); it != events.end(); ++it) { + if (needs_comma) + str += ",\n "; + needs_comma = true; + str += it->ToString(); + } + str += "]"; + return str; +} + +} // namespace + +namespace gcm { + +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 + void OnAccountSignInChanged(const CoreAccountInfo& account, + bool is_signed_in) override; + + private: + testing::AssertionResult CheckEvents( + const std::vector& events); + + std::vector events_; +}; + +void AccountTrackerObserver::OnAccountSignInChanged( + const CoreAccountInfo& account, + bool is_signed_in) { + events_.push_back(TrackingEvent(is_signed_in ? SIGN_IN : SIGN_OUT, + account.account_id, account.gaia)); +} + +void AccountTrackerObserver::Clear() { + events_.clear(); +} + +void AccountTrackerObserver::SortEventsByUser() { + std::stable_sort(events_.begin(), events_.end(), CompareByUser); +} + +testing::AssertionResult AccountTrackerObserver::CheckEvents() { + std::vector events; + return CheckEvents(events); +} + +testing::AssertionResult AccountTrackerObserver::CheckEvents( + const TrackingEvent& e1) { + std::vector events; + events.push_back(e1); + return CheckEvents(events); +} + +testing::AssertionResult AccountTrackerObserver::CheckEvents( + const TrackingEvent& e1, + const TrackingEvent& e2) { + std::vector 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 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 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 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 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& 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 AccountTrackerTest : public testing::Test { + public: + AccountTrackerTest() {} + + ~AccountTrackerTest() override {} + + void SetUp() override { + account_tracker_ = + std::make_unique(identity_test_env_.identity_manager()); + account_tracker_->AddObserver(&observer_); + } + + 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. + + // Sets the primary account info. Returns the account ID of the + // newly-set account. + // NOTE: On ChromeOS, the login callback is never fired in production (since + // the underlying GoogleSigninSucceeded callback is never sent). Tests that + // exercise functionality dependent on that callback firing are not relevant + // on ChromeOS and should simply not run on that platform. + CoreAccountInfo SetActiveAccount(const std::string& email) { + return identity_test_env_.SetPrimaryAccount(email, + signin::ConsentLevel::kSync); + } + +// Helpers that go through a logout flow. +// NOTE: On ChromeOS, the logout callback is never fired in production (since +// the underlying GoogleSignedOut callback is never sent). Tests that exercise +// functionality dependent on that callback firing are not relevant on ChromeOS +// and should simply not run on that platform. +#if !BUILDFLAG(IS_CHROMEOS_ASH) + void NotifyLogoutOfAllAccounts() { identity_test_env_.ClearPrimaryAccount(); } +#endif + + CoreAccountInfo AddAccountWithToken(const std::string& email) { + return identity_test_env_.MakeAccountAvailable(email); + } + + void NotifyTokenAvailable(const CoreAccountId& account_id) { + identity_test_env_.SetRefreshTokenForAccount(account_id); + } + + void NotifyTokenRevoked(const CoreAccountId& account_id) { + identity_test_env_.RemoveRefreshTokenForAccount(account_id); + } + + CoreAccountInfo SetupPrimaryLogin() { + // Initial setup for tests that start with a signed in profile. + CoreAccountInfo primary_account = SetActiveAccount(kPrimaryAccountEmail); + NotifyTokenAvailable(primary_account.account_id); + observer()->Clear(); + + return primary_account; + } + + private: + // net:: stuff needs IO message loop. + base::test::SingleThreadTaskEnvironment task_environment_{ + base::test::SingleThreadTaskEnvironment::MainThreadType::IO}; + signin::IdentityTestEnvironment identity_test_env_; + + std::unique_ptr account_tracker_; + AccountTrackerObserver observer_; +}; + +// Primary tests just involve the Active account + +TEST_F(AccountTrackerTest, PrimaryNoEventsBeforeLogin) { + CoreAccountInfo account = AddAccountWithToken("me@dummy.com"); + NotifyTokenRevoked(account.account_id); + +// Logout is not possible on ChromeOS. +#if !BUILDFLAG(IS_CHROMEOS_ASH) + NotifyLogoutOfAllAccounts(); +#endif + + EXPECT_TRUE(observer()->CheckEvents()); +} + +TEST_F(AccountTrackerTest, PrimaryLoginThenTokenAvailable) { + CoreAccountInfo primary_account = SetActiveAccount(kPrimaryAccountEmail); + NotifyTokenAvailable(primary_account.account_id); + EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_IN, primary_account))); +} + +TEST_F(AccountTrackerTest, PrimaryRevoke) { + CoreAccountInfo primary_account = SetActiveAccount(kPrimaryAccountEmail); + NotifyTokenAvailable(primary_account.account_id); + observer()->Clear(); + + NotifyTokenRevoked(primary_account.account_id); + EXPECT_TRUE( + observer()->CheckEvents(TrackingEvent(SIGN_OUT, primary_account))); +} + +TEST_F(AccountTrackerTest, PrimaryRevokeThenTokenAvailable) { + CoreAccountInfo primary_account = SetActiveAccount(kPrimaryAccountEmail); + NotifyTokenAvailable(primary_account.account_id); + NotifyTokenRevoked(primary_account.account_id); + observer()->Clear(); + + NotifyTokenAvailable(primary_account.account_id); + EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_IN, primary_account))); +} + +// These tests exercise true login/logout, which are not possible on ChromeOS. +#if !BUILDFLAG(IS_CHROMEOS_ASH) +TEST_F(AccountTrackerTest, PrimaryTokenAvailableThenLogin) { + AddAccountWithToken(kPrimaryAccountEmail); + EXPECT_TRUE(observer()->CheckEvents()); + + CoreAccountInfo primary_account = SetActiveAccount(kPrimaryAccountEmail); + EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_IN, primary_account))); +} + +TEST_F(AccountTrackerTest, PrimaryTokenAvailableAndRevokedThenLogin) { + CoreAccountInfo primary_account = AddAccountWithToken(kPrimaryAccountEmail); + EXPECT_TRUE(observer()->CheckEvents()); + + NotifyTokenRevoked(primary_account.account_id); + EXPECT_TRUE(observer()->CheckEvents()); + + SetActiveAccount(kPrimaryAccountEmail); + EXPECT_TRUE(observer()->CheckEvents()); +} + +TEST_F(AccountTrackerTest, PrimaryRevokeThenLogin) { + CoreAccountInfo primary_account = SetActiveAccount(kPrimaryAccountEmail); + NotifyTokenAvailable(primary_account.account_id); + NotifyLogoutOfAllAccounts(); + observer()->Clear(); + + SetActiveAccount(kPrimaryAccountEmail); + EXPECT_TRUE(observer()->CheckEvents()); +} + +TEST_F(AccountTrackerTest, PrimaryLogoutThenRevoke) { + CoreAccountInfo primary_account = SetActiveAccount(kPrimaryAccountEmail); + NotifyTokenAvailable(primary_account.account_id); + observer()->Clear(); + + NotifyLogoutOfAllAccounts(); + EXPECT_TRUE( + observer()->CheckEvents(TrackingEvent(SIGN_OUT, primary_account))); + + NotifyTokenRevoked(primary_account.account_id); + EXPECT_TRUE(observer()->CheckEvents()); +} + +#endif + +// Non-primary accounts + +TEST_F(AccountTrackerTest, Available) { + SetupPrimaryLogin(); + + CoreAccountInfo account = AddAccountWithToken("user@example.com"); + EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_IN, account))); +} + +TEST_F(AccountTrackerTest, AvailableRevokeAvailable) { + SetupPrimaryLogin(); + + CoreAccountInfo account = AddAccountWithToken("user@example.com"); + NotifyTokenRevoked(account.account_id); + EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_IN, account), + TrackingEvent(SIGN_OUT, account))); + + NotifyTokenAvailable(account.account_id); + EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_IN, account))); +} + +TEST_F(AccountTrackerTest, AvailableRevokeRevoke) { + SetupPrimaryLogin(); + + CoreAccountInfo account = AddAccountWithToken("user@example.com"); + NotifyTokenRevoked(account.account_id); + EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_IN, account), + TrackingEvent(SIGN_OUT, account))); + + NotifyTokenRevoked(account.account_id); + EXPECT_TRUE(observer()->CheckEvents()); +} + +TEST_F(AccountTrackerTest, AvailableAvailable) { + SetupPrimaryLogin(); + + CoreAccountInfo account = AddAccountWithToken("user@example.com"); + EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_IN, account))); + + NotifyTokenAvailable(account.account_id); + EXPECT_TRUE(observer()->CheckEvents()); +} + +TEST_F(AccountTrackerTest, TwoAccounts) { + SetupPrimaryLogin(); + + CoreAccountInfo alpha_account = AddAccountWithToken("alpha@example.com"); + EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_IN, alpha_account))); + + CoreAccountInfo beta_account = AddAccountWithToken("beta@example.com"); + EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_IN, beta_account))); + + NotifyTokenRevoked(alpha_account.account_id); + EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_OUT, alpha_account))); + + NotifyTokenRevoked(beta_account.account_id); + EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_OUT, beta_account))); +} + +// Primary/non-primary interactions + +TEST_F(AccountTrackerTest, MultiNoEventsBeforeLogin) { + CoreAccountInfo account1 = AddAccountWithToken("user@example.com"); + CoreAccountInfo account2 = AddAccountWithToken("user2@example.com"); + NotifyTokenRevoked(account2.account_id); + NotifyTokenRevoked(account2.account_id); + +// Logout is not possible on ChromeOS. +#if !BUILDFLAG(IS_CHROMEOS_ASH) + NotifyLogoutOfAllAccounts(); +#endif + + EXPECT_TRUE(observer()->CheckEvents()); +} + +TEST_F(AccountTrackerTest, MultiRevokePrimaryDoesNotRemoveAllAccounts) { + CoreAccountInfo primary_account = SetActiveAccount(kPrimaryAccountEmail); + NotifyTokenAvailable(primary_account.account_id); + CoreAccountInfo account = AddAccountWithToken("user@example.com"); + observer()->Clear(); + + NotifyTokenRevoked(primary_account.account_id); + observer()->SortEventsByUser(); + EXPECT_TRUE( + observer()->CheckEvents(TrackingEvent(SIGN_OUT, primary_account))); +} + +TEST_F(AccountTrackerTest, GetAccountsPrimary) { + CoreAccountInfo primary_account = SetupPrimaryLogin(); + + std::vector account = account_tracker()->GetAccounts(); + EXPECT_EQ(1ul, account.size()); + EXPECT_EQ(primary_account.account_id, account[0].account_id); + EXPECT_EQ(primary_account.gaia, account[0].gaia); + EXPECT_EQ(primary_account.email, account[0].email); +} + +TEST_F(AccountTrackerTest, GetAccountsSignedOut) { + std::vector accounts = account_tracker()->GetAccounts(); + EXPECT_EQ(0ul, accounts.size()); +} + +TEST_F(AccountTrackerTest, GetMultipleAccounts) { + CoreAccountInfo primary_account = SetupPrimaryLogin(); + CoreAccountInfo alpha_account = AddAccountWithToken("alpha@example.com"); + CoreAccountInfo beta_account = AddAccountWithToken("beta@example.com"); + + std::vector account = account_tracker()->GetAccounts(); + EXPECT_EQ(3ul, account.size()); + EXPECT_EQ(primary_account.account_id, account[0].account_id); + EXPECT_EQ(primary_account.email, account[0].email); + EXPECT_EQ(primary_account.gaia, account[0].gaia); + + EXPECT_EQ(alpha_account.account_id, account[1].account_id); + EXPECT_EQ(alpha_account.email, account[1].email); + EXPECT_EQ(alpha_account.gaia, account[1].gaia); + + EXPECT_EQ(beta_account.account_id, account[2].account_id); + EXPECT_EQ(beta_account.email, account[2].email); + EXPECT_EQ(beta_account.gaia, account[2].gaia); +} + +TEST_F(AccountTrackerTest, GetAccountsReturnNothingWhenPrimarySignedOut) { + CoreAccountInfo primary_account = SetupPrimaryLogin(); + + CoreAccountInfo zeta_account = AddAccountWithToken("zeta@example.com"); + CoreAccountInfo alpha_account = AddAccountWithToken("alpha@example.com"); + + NotifyTokenRevoked(primary_account.account_id); + + std::vector account = account_tracker()->GetAccounts(); + EXPECT_EQ(0ul, account.size()); +} + +// This test exercises true login/logout, which are not possible on ChromeOS. +#if !BUILDFLAG(IS_CHROMEOS_ASH) +TEST_F(AccountTrackerTest, MultiLogoutRemovesAllAccounts) { + CoreAccountInfo primary_account = SetActiveAccount(kPrimaryAccountEmail); + NotifyTokenAvailable(primary_account.account_id); + CoreAccountInfo account = AddAccountWithToken("user@example.com"); + observer()->Clear(); + + NotifyLogoutOfAllAccounts(); + observer()->SortEventsByUser(); + EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_OUT, primary_account), + TrackingEvent(SIGN_OUT, account))); +} +#endif + +} // namespace gcm diff --git a/chromium/components/gcm_driver/android/OWNERS b/chromium/components/gcm_driver/android/OWNERS new file mode 100644 index 00000000000..69b0ace3379 --- /dev/null +++ b/chromium/components/gcm_driver/android/OWNERS @@ -0,0 +1,2 @@ +johnme@chromium.org +mvanouwerkerk@chromium.org diff --git a/chromium/components/gcm_driver/android/junit/src/org/chromium/components/gcm_driver/GCMMessageTest.java b/chromium/components/gcm_driver/android/junit/src/org/chromium/components/gcm_driver/GCMMessageTest.java new file mode 100644 index 00000000000..f39d2b5dca7 --- /dev/null +++ b/chromium/components/gcm_driver/android/junit/src/org/chromium/components/gcm_driver/GCMMessageTest.java @@ -0,0 +1,267 @@ +// Copyright 2017 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. + +package org.chromium.components.gcm_driver; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import android.os.Bundle; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +import org.chromium.base.test.BaseRobolectricTestRunner; + +/** + * Unit tests for GCMMessage. + */ +@RunWith(BaseRobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class GCMMessageTest { + private void assertMessageEquals(GCMMessage m1, GCMMessage m2) { + assertEquals(m1.getSenderId(), m2.getSenderId()); + assertEquals(m1.getAppId(), m2.getAppId()); + assertEquals(m1.getCollapseKey(), m2.getCollapseKey()); + assertArrayEquals(m1.getDataKeysAndValuesArray(), m2.getDataKeysAndValuesArray()); + } + + /** + * Tests that a message object can be created based on data received from GCM. Note that the raw + * data field is tested separately. + */ + @Test + public void testCreateMessageFromGCM() { + Bundle extras = new Bundle(); + + // Compose a simple message that lacks all optional fields. + extras.putString("subtype", "MyAppId"); + + { + GCMMessage message = new GCMMessage("MySenderId", extras); + + assertEquals("MySenderId", message.getSenderId()); + assertEquals("MyAppId", message.getAppId()); + assertEquals(null, message.getCollapseKey()); + assertArrayEquals(new String[] {}, message.getDataKeysAndValuesArray()); + } + + // Add the optional fields: collapse key, raw binary data, a custom property and an original + // priority. + extras.putString("collapse_key", "MyCollapseKey"); + extras.putByteArray("rawData", new byte[] {0x00, 0x15, 0x30, 0x45}); + extras.putString("property", "value"); + extras.putString("google.original_priority", "normal"); + + { + GCMMessage message = new GCMMessage("MySenderId", extras); + + assertEquals("MySenderId", message.getSenderId()); + assertEquals("MyAppId", message.getAppId()); + assertEquals("MyCollapseKey", message.getCollapseKey()); + assertArrayEquals( + new String[] {"property", "value"}, message.getDataKeysAndValuesArray()); + assertEquals(GCMMessage.Priority.NORMAL, message.getOriginalPriority()); + } + } + + /** + * Tests that the bundle containing extras from GCM will be filtered for values that we either + * pass through by other means, or that we know to be irrelevant to regular GCM messages. + */ + @Test + public void testFiltersExtraBundle() { + Bundle extras = new Bundle(); + + // These should be filtered by full key. + extras.putString("collapse_key", "collapseKey"); + extras.putString("rawData", "rawData"); + extras.putString("from", "from"); + extras.putString("subtype", "subtype"); + + // These should be filtered by prefix matching. + extras.putString("com.google.ipc.invalidation.gcmmplex.foo", "bar"); + extras.putString("com.google.ipc.invalidation.gcmmplex.bar", "baz"); + + // These should be filtered because they're not strings. + extras.putBoolean("myBoolean", true); + extras.putInt("myInteger", 42); + + // These should not be filtered. + extras.putString("collapse_key2", "secondCollapseKey"); + extras.putString("myString", "foobar"); + + GCMMessage message = new GCMMessage("senderId", extras); + + assertArrayEquals(new String[] {"collapse_key2", "secondCollapseKey", "myString", "foobar"}, + message.getDataKeysAndValuesArray()); + } + + /** + * Tests that a GCMMessage object can be serialized to and deserialized from a persistable + * bundle. Note that the raw data field is tested separately. Only run on Android L and beyond + * because it depends on PersistableBundle. + */ + @Test + public void testSerializationToPersistableBundle() { + Bundle extras = new Bundle(); + + // Compose a simple message that lacks all optional fields. + extras.putString("subtype", "MyAppId"); + + { + GCMMessage message = new GCMMessage("MySenderId", extras); + GCMMessage copiedMessage = GCMMessage.createFromBundle(message.toBundle()); + assertMessageEquals(message, copiedMessage); + } + + // Add the optional fields: collapse key, raw binary data and a custom property. + extras.putString("collapse_key", "MyCollapseKey"); + extras.putString("property", "value"); + extras.putString("google.original_priority", "normal"); + + { + GCMMessage message = new GCMMessage("MySenderId", extras); + GCMMessage copiedMessage = GCMMessage.createFromBundle(message.toBundle()); + assertMessageEquals(message, copiedMessage); + } + } + + /** + * Tests that the raw data field can be serialized and deserialized as expected. It should be + * NULL when undefined, an empty byte array when defined but empty, and a regular, filled + * byte array when data has been provided. Only run on Android L and beyond because it depends + * on PersistableBundle. + */ + @Test + public void testRawDataSerializationBehaviour() { + Bundle extras = new Bundle(); + extras.putString("subtype", "MyAppId"); + + // Case 1: No raw data supplied. Should be NULL. + { + GCMMessage message = new GCMMessage("MySenderId", extras); + GCMMessage copiedMessage = GCMMessage.createFromBundle(message.toBundle()); + + assertArrayEquals(null, message.getRawData()); + assertArrayEquals(null, copiedMessage.getRawData()); + } + + extras.putByteArray("rawData", new byte[] {}); + + // Case 2: Empty byte array of raw data supplied. Should be just that. + { + GCMMessage message = new GCMMessage("MySenderId", extras); + GCMMessage copiedMessage = GCMMessage.createFromBundle(message.toBundle()); + + assertArrayEquals(new byte[] {}, message.getRawData()); + assertArrayEquals(new byte[] {}, copiedMessage.getRawData()); + } + + extras.putByteArray("rawData", new byte[] {0x00, 0x15, 0x30, 0x45}); + + // Case 3: Byte array with data supplied. + { + GCMMessage message = new GCMMessage("MySenderId", extras); + GCMMessage copiedMessage = GCMMessage.createFromBundle(message.toBundle()); + + assertArrayEquals(new byte[] {0x00, 0x15, 0x30, 0x45}, message.getRawData()); + assertArrayEquals(new byte[] {0x00, 0x15, 0x30, 0x45}, copiedMessage.getRawData()); + } + } + + /** + * Tests that a GCMMessage object can be serialized to and deserialized from + * a JSONObject. Note that the raw data field is tested separately. + */ + @Test + public void testSerializationToJSON() throws JSONException { + Bundle extras = new Bundle(); + + // Compose a simple message that lacks all optional fields. + extras.putString("subtype", "MyAppId"); + + { + GCMMessage message = new GCMMessage("MySenderId", extras); + JSONObject messageJSON = message.toJSON(); + + // Version must be written to JSON. + assertEquals(messageJSON.get("version"), GCMMessage.VERSION); + GCMMessage copiedMessage = GCMMessage.createFromJSON(messageJSON); + + assertMessageEquals(message, copiedMessage); + } + + // Add the optional fields: collapse key, raw binary data and a custom property. + extras.putString("collapse_key", "MyCollapseKey"); + extras.putString("property", "value"); + extras.putString("google.original_priority", "normal"); + + { + GCMMessage message = new GCMMessage("MySenderId", extras); + GCMMessage copiedMessage = GCMMessage.createFromJSON(message.toJSON()); + + assertMessageEquals(message, copiedMessage); + } + } + + /** + * Tests that the raw data field can be serialized and deserialized as expected from JSONObject. + * It should be NULL when undefined, an empty byte array when defined but empty, and a regular, + * filled byte array when data has been provided. + */ + @Test + public void testRawDataSerializationToJSON() { + Bundle extras = new Bundle(); + extras.putString("subtype", "MyAppId"); + + // Case 1: No raw data supplied. Should be NULL. + { + GCMMessage message = new GCMMessage("MySenderId", extras); + GCMMessage copiedMessage = GCMMessage.createFromJSON(message.toJSON()); + + assertArrayEquals(null, message.getRawData()); + assertArrayEquals(null, copiedMessage.getRawData()); + } + + extras.putByteArray("rawData", new byte[] {}); + + // Case 2: Empty byte array of raw data supplied. Should be just that. + { + GCMMessage message = new GCMMessage("MySenderId", extras); + GCMMessage copiedMessage = GCMMessage.createFromJSON(message.toJSON()); + + assertArrayEquals(new byte[] {}, message.getRawData()); + assertArrayEquals(new byte[] {}, copiedMessage.getRawData()); + } + final byte[] rawData = {0x00, 0x15, 0x30, 0x45}; + extras.putByteArray("rawData", rawData); + + // Case 3: Byte array with data supplied. + { + GCMMessage message = new GCMMessage("MySenderId", extras); + GCMMessage copiedMessage = GCMMessage.createFromJSON(message.toJSON()); + + assertArrayEquals(rawData, message.getRawData()); + assertArrayEquals(rawData, copiedMessage.getRawData()); + } + } + + /** + * Tests that getOriginalPriority returns Priority.NONE if it was not set in the bundle. + */ + @Test + public void testNullOriginalPriority() { + Bundle extras = new Bundle(); + + // Compose a simple message that lacks all optional fields. + extras.putString("subtype", "MyAppId"); + GCMMessage message = new GCMMessage("MySenderId", extras); + + assertEquals(GCMMessage.Priority.NONE, message.getOriginalPriority()); + } +} diff --git a/chromium/components/gcm_driver/android/junit/src/org/chromium/components/gcm_driver/LazySubscriptionsManagerTest.java b/chromium/components/gcm_driver/android/junit/src/org/chromium/components/gcm_driver/LazySubscriptionsManagerTest.java new file mode 100644 index 00000000000..fc77ce0b22f --- /dev/null +++ b/chromium/components/gcm_driver/android/junit/src/org/chromium/components/gcm_driver/LazySubscriptionsManagerTest.java @@ -0,0 +1,267 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.components.gcm_driver; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.content.SharedPreferences; +import android.os.Bundle; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +import org.chromium.base.ContextUtils; +import org.chromium.base.test.BaseRobolectricTestRunner; + +import java.util.Set; + +/** + * Unit tests for LazySubscriptionsManager. + */ +@RunWith(BaseRobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class LazySubscriptionsManagerTest { + /** + * Tests the persistence of the "hasPersistedMessages" flag. + */ + @Test + public void testHasPersistedMessages() { + final String subscriptionId = "subscription_id"; + // By default there is no persisted messages. + assertTrue(LazySubscriptionsManager.getSubscriptionIdsWithPersistedMessages(subscriptionId) + .isEmpty()); + + LazySubscriptionsManager.storeHasPersistedMessagesForSubscription(subscriptionId, true); + assertEquals(1, + LazySubscriptionsManager.getSubscriptionIdsWithPersistedMessages(subscriptionId) + .size()); + + LazySubscriptionsManager.storeHasPersistedMessagesForSubscription(subscriptionId, false); + assertTrue(LazySubscriptionsManager.getSubscriptionIdsWithPersistedMessages(subscriptionId) + .isEmpty()); + } + + /** + * Tests the migration path from one boolean pref to a set subscription ids for persisted + * messages. + */ + @Test + public void testMigrateHasPersistedMessagesPref() { + final String subscriptionId1 = "subscription_id1"; + final String subscriptionId2 = "subscription_id2"; + LazySubscriptionsManager.storeLazinessInformation(subscriptionId1, true); + LazySubscriptionsManager.storeLazinessInformation(subscriptionId2, true); + + SharedPreferences sharedPrefs = ContextUtils.getAppSharedPreferences(); + sharedPrefs.edit() + .putBoolean(LazySubscriptionsManager.LEGACY_HAS_PERSISTED_MESSAGES_KEY, false) + .apply(); + LazySubscriptionsManager.migrateHasPersistedMessagesPref(); + + assertTrue(LazySubscriptionsManager.getSubscriptionIdsWithPersistedMessages(subscriptionId1) + .isEmpty()); + assertTrue(LazySubscriptionsManager.getSubscriptionIdsWithPersistedMessages(subscriptionId2) + .isEmpty()); + + sharedPrefs.edit() + .putBoolean(LazySubscriptionsManager.LEGACY_HAS_PERSISTED_MESSAGES_KEY, true) + .apply(); + LazySubscriptionsManager.migrateHasPersistedMessagesPref(); + + assertEquals(1, + LazySubscriptionsManager.getSubscriptionIdsWithPersistedMessages(subscriptionId1) + .size()); + assertEquals(1, + LazySubscriptionsManager.getSubscriptionIdsWithPersistedMessages(subscriptionId2) + .size()); + } + + /** + * Tests that lazy subscriptions are stored. + */ + @Test + public void testMarkSubscriptionAsLazy() { + final String subscriptionId = "subscription_id"; + LazySubscriptionsManager.storeLazinessInformation(subscriptionId, true); + assertTrue(LazySubscriptionsManager.isSubscriptionLazy(subscriptionId)); + } + + /** + * Tests that unlazy subscriptions are stored. + */ + @Test + public void testMarkSubscriptionAsNotLazy() { + final String subscriptionId = "subscription_id"; + LazySubscriptionsManager.storeLazinessInformation(subscriptionId, false); + assertFalse(LazySubscriptionsManager.isSubscriptionLazy(subscriptionId)); + } + + /** + * Tests subscriptions are not lazy be default. + */ + @Test + public void testDefaultSubscriptionNotLazy() { + final String subscriptionId = "subscription_id"; + assertFalse(LazySubscriptionsManager.isSubscriptionLazy(subscriptionId)); + } + + /** + * Tests that switching from lazy to unlazy should leave no queued messages behind. + */ + @Test + public void testSwitchingFromLazyToUnlazy() { + final String subscriptionId = "subscription_id"; + LazySubscriptionsManager.storeLazinessInformation(subscriptionId, true); + + Bundle extras = new Bundle(); + extras.putString("subtype", "MyAppId"); + extras.putString("collapse_key", "CollapseKey"); + GCMMessage message = new GCMMessage("MySenderId", extras); + + LazySubscriptionsManager.persistMessage(subscriptionId, message); + assertEquals(1, LazySubscriptionsManager.readMessages(subscriptionId).length); + + LazySubscriptionsManager.storeLazinessInformation(subscriptionId, false); + assertEquals(0, LazySubscriptionsManager.readMessages(subscriptionId).length); + } + + /** + * Tests that switching from lazy to unlazy and back to lazy. + */ + @Test + public void testSwitchingFromLazyToUnlazyAndBackToLazy() { + final String subscriptionId = "subscription_id"; + LazySubscriptionsManager.storeLazinessInformation(subscriptionId, true); + assertTrue(LazySubscriptionsManager.isSubscriptionLazy(subscriptionId)); + LazySubscriptionsManager.storeLazinessInformation(subscriptionId, false); + assertFalse(LazySubscriptionsManager.isSubscriptionLazy(subscriptionId)); + LazySubscriptionsManager.storeLazinessInformation(subscriptionId, true); + assertTrue(LazySubscriptionsManager.isSubscriptionLazy(subscriptionId)); + } + + @Test + public void testGetLazySubscriptionIds() { + final String subscriptionId1 = "subscription_id1"; + final String subscriptionId2 = "subscription_id2"; + final String subscriptionId3 = "subscription_id3"; + LazySubscriptionsManager.storeLazinessInformation(subscriptionId1, true); + LazySubscriptionsManager.storeLazinessInformation(subscriptionId2, true); + LazySubscriptionsManager.storeLazinessInformation(subscriptionId3, true); + Set lazySubscriptionIds = LazySubscriptionsManager.getLazySubscriptionIds(); + assertEquals(3, lazySubscriptionIds.size()); + assertTrue(lazySubscriptionIds.contains(subscriptionId1)); + assertTrue(lazySubscriptionIds.contains(subscriptionId2)); + assertTrue(lazySubscriptionIds.contains(subscriptionId3)); + } + + /** + * Tests that GCM messages are persisted and read. + */ + @Test + public void testReadingPersistedMessage() { + final String subscriptionId = "subscriptionId"; + final String anotherSubscriptionId = "AnotherSubscriptionId"; + + Bundle extras = new Bundle(); + extras.putString("subtype", "MyAppId"); + extras.putString("collapse_key", "CollapseKey"); + GCMMessage message = new GCMMessage("MySenderId", extras); + LazySubscriptionsManager.persistMessage(subscriptionId, message); + + GCMMessage messages[] = LazySubscriptionsManager.readMessages(subscriptionId); + assertEquals(1, messages.length); + assertEquals(message.getSenderId(), messages[0].getSenderId()); + + messages = LazySubscriptionsManager.readMessages(anotherSubscriptionId); + assertEquals(0, messages.length); + } + + /** + * Tests that only MESSAGES_QUEUE_SIZE messages are kept. + */ + @Test + public void testPersistingMessageCount() { + // This tests persists MESSAGES_QUEUE_SIZE+extraMessagesCount messages + // and checks if only the most recent |MESSAGES_QUEUE_SIZE| are read. + // |collapse_key| is used to distinguish between messages for + // simplicity. + final String subscriptionId = "subscriptionId"; + final String collapseKeyPrefix = "subscriptionId"; + final int extraMessagesCount = 5; + + // Persist |MESSAGES_QUEUE_SIZE| + |extraMessagesCount| messages. + for (int i = 0; i < LazySubscriptionsManager.MESSAGES_QUEUE_SIZE + extraMessagesCount; + i++) { + Bundle extras = new Bundle(); + extras.putString("subtype", "MyAppId"); + extras.putString("collapse_key", collapseKeyPrefix + i); + GCMMessage message = new GCMMessage("MySenderId", extras); + LazySubscriptionsManager.persistMessage(subscriptionId, message); + } + // Check that only the most recent |MESSAGES_QUEUE_SIZE| are persisted. + GCMMessage messages[] = LazySubscriptionsManager.readMessages(subscriptionId); + assertEquals(LazySubscriptionsManager.MESSAGES_QUEUE_SIZE, messages.length); + for (int i = 0; i < LazySubscriptionsManager.MESSAGES_QUEUE_SIZE; i++) { + assertEquals( + collapseKeyPrefix + (i + extraMessagesCount), messages[i].getCollapseKey()); + } + } + + /** + * Tests that messages with the same collapse key override each other. + */ + @Test + public void testCollapseKeyCollision() { + final String subscriptionId = "subscriptionId"; + final String collapseKey = "collapseKey"; + final byte[] rawData1 = {0x00, 0x15, 0x30, 0x01}; + final byte[] rawData2 = {0x00, 0x15, 0x30, 0x02}; + + Bundle extras = new Bundle(); + extras.putString("subtype", "MyAppId"); + extras.putString("collapse_key", collapseKey); + extras.putByteArray("rawData", rawData1); + + // Persist a message and make sure it's persisted. + GCMMessage message1 = new GCMMessage("MySenderId", extras); + LazySubscriptionsManager.persistMessage(subscriptionId, message1); + + GCMMessage messages[] = LazySubscriptionsManager.readMessages(subscriptionId); + assertEquals(1, messages.length); + assertArrayEquals(rawData1, messages[0].getRawData()); + + // Persist another message with the same collapse key and another raw data. + extras.putByteArray("rawData", rawData2); + GCMMessage message2 = new GCMMessage("MySenderId", extras); + LazySubscriptionsManager.persistMessage(subscriptionId, message2); + + messages = LazySubscriptionsManager.readMessages(subscriptionId); + assertEquals(1, messages.length); + assertArrayEquals(rawData2, messages[0].getRawData()); + } + + /** + * Tests that messages with the same collapse key override each other. + */ + @Test + public void testDeletePersistedMessages() { + final String subscriptionId = "subscriptionId"; + + Bundle extras = new Bundle(); + extras.putString("subtype", "MyAppId"); + extras.putString("collapse_key", "collapseKey"); + extras.putByteArray("rawData", new byte[] {}); + GCMMessage message = new GCMMessage("MySenderId", extras); + LazySubscriptionsManager.persistMessage(subscriptionId, message); + + assertEquals(1, LazySubscriptionsManager.readMessages(subscriptionId).length); + LazySubscriptionsManager.deletePersistedMessagesForSubscriptionId(subscriptionId); + assertEquals(0, LazySubscriptionsManager.readMessages(subscriptionId).length); + } +} diff --git a/chromium/components/gcm_driver/common/gcm_driver_export.h b/chromium/components/gcm_driver/common/gcm_driver_export.h new file mode 100644 index 00000000000..5809ea34435 --- /dev/null +++ b/chromium/components/gcm_driver/common/gcm_driver_export.h @@ -0,0 +1,29 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_GCM_DRIVER_COMMON_GCM_DRIVER_EXPORT_H_ +#define COMPONENTS_GCM_DRIVER_COMMON_GCM_DRIVER_EXPORT_H_ + +#if defined(COMPONENT_BUILD) +#if defined(WIN32) + +#if defined(GCM_DRIVER_IMPLEMENTATION) +#define GCM_DRIVER_EXPORT __declspec(dllexport) +#else +#define GCM_DRIVER_EXPORT __declspec(dllimport) +#endif // defined(GCM_DRIVER_IMPLEMENTATION) + +#else // defined(WIN32) +#if defined(GCM_DRIVER_IMPLEMENTATION) +#define GCM_DRIVER_EXPORT __attribute__((visibility("default"))) +#else +#define GCM_DRIVER_EXPORT +#endif +#endif + +#else // defined(COMPONENT_BUILD) +#define GCM_DRIVER_EXPORT +#endif + +#endif // COMPONENTS_GCM_DRIVER_COMMON_GCM_DRIVER_EXPORT_H_ diff --git a/chromium/components/gcm_driver/common/gcm_message.cc b/chromium/components/gcm_driver/common/gcm_message.cc new file mode 100644 index 00000000000..7937bd8867c --- /dev/null +++ b/chromium/components/gcm_driver/common/gcm_message.cc @@ -0,0 +1,29 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/common/gcm_message.h" + +namespace gcm { + +// static +const int OutgoingMessage::kMaximumTTL = 24 * 60 * 60; // 1 day. + +OutgoingMessage::OutgoingMessage() = default; + +OutgoingMessage::OutgoingMessage(const OutgoingMessage& other) = default; + +OutgoingMessage::~OutgoingMessage() = default; + +IncomingMessage::IncomingMessage() = default; + +IncomingMessage::IncomingMessage(const IncomingMessage& other) = default; +IncomingMessage::IncomingMessage(IncomingMessage&& other) = default; + +IncomingMessage& IncomingMessage::operator=(const IncomingMessage& other) = + default; +IncomingMessage& IncomingMessage::operator=(IncomingMessage&& other) = default; + +IncomingMessage::~IncomingMessage() = default; + +} // namespace gcm diff --git a/chromium/components/gcm_driver/common/gcm_message.h b/chromium/components/gcm_driver/common/gcm_message.h new file mode 100644 index 00000000000..0226934616b --- /dev/null +++ b/chromium/components/gcm_driver/common/gcm_message.h @@ -0,0 +1,56 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_GCM_DRIVER_COMMON_GCM_MESSAGE_H_ +#define COMPONENTS_GCM_DRIVER_COMMON_GCM_MESSAGE_H_ + +#include +#include + +#include "components/gcm_driver/common/gcm_driver_export.h" + +namespace gcm { + +// Message data consisting of key-value pairs. +using MessageData = std::map; + +// Message to be delivered to the other party. +struct GCM_DRIVER_EXPORT OutgoingMessage { + OutgoingMessage(); + OutgoingMessage(const OutgoingMessage& other); + ~OutgoingMessage(); + + // Message ID. + std::string id; + // In seconds. + int time_to_live = kMaximumTTL; + MessageData data; + + static const int kMaximumTTL; +}; + +// Message being received from the other party. +struct GCM_DRIVER_EXPORT IncomingMessage { + IncomingMessage(); + IncomingMessage(const IncomingMessage& other); + IncomingMessage(IncomingMessage&& other); + ~IncomingMessage(); + + IncomingMessage& operator=(const IncomingMessage& other); + IncomingMessage& operator=(IncomingMessage&& other); + + MessageData data; + std::string collapse_key; + std::string sender_id; + std::string message_id; + std::string raw_data; + + // Whether the contents of the message have been decrypted, and are + // available in |raw_data|. + bool decrypted = false; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_COMMON_GCM_MESSAGE_H_ diff --git a/chromium/components/gcm_driver/crypto/DEPS b/chromium/components/gcm_driver/crypto/DEPS new file mode 100644 index 00000000000..4060f526a56 --- /dev/null +++ b/chromium/components/gcm_driver/crypto/DEPS @@ -0,0 +1,7 @@ +include_rules = [ + "+components/gcm_driver", + "+components/leveldb_proto", + "+crypto", + "+net/http", + "+third_party/boringssl/src/include", +] diff --git a/chromium/components/gcm_driver/crypto/encryption_header_parsers.cc b/chromium/components/gcm_driver/crypto/encryption_header_parsers.cc new file mode 100644 index 00000000000..b70dc5276c6 --- /dev/null +++ b/chromium/components/gcm_driver/crypto/encryption_header_parsers.cc @@ -0,0 +1,156 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/crypto/encryption_header_parsers.h" + +#include "base/base64url.h" +#include "base/strings/string_number_conversions.h" + +#include "base/strings/string_util.h" + + +namespace gcm { + +namespace { + +// The default record size in bytes, as defined in section two of +// https://tools.ietf.org/html/draft-thomson-http-encryption. +const uint64_t kDefaultRecordSizeBytes = 4096; + +// Decodes the string in |value| using base64url and writes the decoded value to +// |*salt|. Returns whether the string is not empty and could be decoded. +bool ValueToDecodedString(base::StringPiece value, std::string* salt) { + if (value.empty()) + return false; + + return base::Base64UrlDecode( + value, base::Base64UrlDecodePolicy::IGNORE_PADDING, salt); +} + +// Parses the record size in |value| and writes the value to |*rs|. The value +// must be a positive decimal integer greater than one that does not start +// with a plus. Returns whether the record size was valid. +bool RecordSizeToInt(base::StringPiece value, uint64_t* rs) { + if (value.empty()) + return false; + + // Reject a leading plus, as the fact that the value must be positive is + // dictated by the specification. + if (value[0] == '+') + return false; + + uint64_t candidate_rs; + if (!base::StringToUint64(value, &candidate_rs)) + return false; + + // The record size MUST be greater than one byte. + if (candidate_rs <= 1) + return false; + + *rs = candidate_rs; + return true; +} + +} // namespace + +EncryptionHeaderIterator::EncryptionHeaderIterator( + std::string::const_iterator header_begin, + std::string::const_iterator header_end) + : iterator_(header_begin, header_end, ','), + rs_(kDefaultRecordSizeBytes) {} + +EncryptionHeaderIterator::~EncryptionHeaderIterator() {} + +bool EncryptionHeaderIterator::GetNext() { + keyid_.clear(); + salt_.clear(); + rs_ = kDefaultRecordSizeBytes; + + if (!iterator_.GetNext()) + return false; + + net::HttpUtil::NameValuePairsIterator name_value_pairs( + iterator_.value_begin(), iterator_.value_end(), ';', + net::HttpUtil::NameValuePairsIterator::Values::REQUIRED, + net::HttpUtil::NameValuePairsIterator::Quotes::NOT_STRICT); + + bool found_keyid = false; + bool found_salt = false; + bool found_rs = false; + + while (name_value_pairs.GetNext()) { + const base::StringPiece name = name_value_pairs.name_piece(); + const base::StringPiece value = name_value_pairs.value_piece(); + + if (base::LowerCaseEqualsASCII(name, "keyid")) { + if (found_keyid) + return false; + keyid_.assign(value.data(), value.size()); + found_keyid = true; + } else if (base::LowerCaseEqualsASCII(name, "salt")) { + if (found_salt || !ValueToDecodedString(value, &salt_)) + return false; + found_salt = true; + } else if (base::LowerCaseEqualsASCII(name, "rs")) { + if (found_rs || !RecordSizeToInt(value, &rs_)) + return false; + found_rs = true; + } else { + // Silently ignore unknown directives for forward compatibility. + } + } + + return name_value_pairs.valid(); +} + +CryptoKeyHeaderIterator::CryptoKeyHeaderIterator( + std::string::const_iterator header_begin, + std::string::const_iterator header_end) + : iterator_(header_begin, header_end, ',') {} + +CryptoKeyHeaderIterator::~CryptoKeyHeaderIterator() {} + +bool CryptoKeyHeaderIterator::GetNext() { + keyid_.clear(); + aesgcm128_.clear(); + dh_.clear(); + + if (!iterator_.GetNext()) + return false; + + net::HttpUtil::NameValuePairsIterator name_value_pairs( + iterator_.value_begin(), iterator_.value_end(), ';', + net::HttpUtil::NameValuePairsIterator::Values::REQUIRED, + net::HttpUtil::NameValuePairsIterator::Quotes::NOT_STRICT); + + bool found_keyid = false; + bool found_aesgcm128 = false; + bool found_dh = false; + + while (name_value_pairs.GetNext()) { + const base::StringPiece name = name_value_pairs.name_piece(); + const base::StringPiece value = name_value_pairs.value_piece(); + + if (base::LowerCaseEqualsASCII(name, "keyid")) { + if (found_keyid) + return false; + keyid_.assign(value.data(), value.size()); + found_keyid = true; + } else if (base::LowerCaseEqualsASCII(name, "aesgcm128")) { + if (found_aesgcm128 || !ValueToDecodedString(value, &aesgcm128_)) + return false; + found_aesgcm128 = true; + } else if (base::LowerCaseEqualsASCII(name, "dh")) { + if (found_dh || !ValueToDecodedString(value, &dh_)) + return false; + found_dh = true; + } else { + // Silently ignore unknown directives for forward compatibility. + } + } + + return name_value_pairs.valid(); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/crypto/encryption_header_parsers.h b/chromium/components/gcm_driver/crypto/encryption_header_parsers.h new file mode 100644 index 00000000000..dc2961a85ff --- /dev/null +++ b/chromium/components/gcm_driver/crypto/encryption_header_parsers.h @@ -0,0 +1,92 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_GCM_DRIVER_CRYPTO_ENCRYPTION_HEADER_PARSERS_H_ +#define COMPONENTS_GCM_DRIVER_CRYPTO_ENCRYPTION_HEADER_PARSERS_H_ + +#include +#include + +#include "base/strings/string_piece.h" +#include "net/http/http_util.h" + +namespace gcm { + +// Iterates over a header that follows the syntax of the Encryption HTTP header +// per the Encrypted Content-Encoding for HTTP draft. This header follows the +// #list syntax from the extended ABNF syntax defined in section 1.2 of RFC7230. +// +// https://tools.ietf.org/html/draft-thomson-http-encryption#section-3 +// https://tools.ietf.org/html/rfc7230#section-1.2 +class EncryptionHeaderIterator { + public: + EncryptionHeaderIterator(std::string::const_iterator header_begin, + std::string::const_iterator header_end); + ~EncryptionHeaderIterator(); + + // Advances the iterator to the next header value, if any. Returns true if + // there is a next value. Use the keyid(), salt() and rs() methods to access + // the key-value pairs included in the header value. + bool GetNext(); + + const std::string& keyid() const { + return keyid_; + } + + const std::string& salt() const { + return salt_; + } + + uint64_t rs() const { + return rs_; + } + + private: + net::HttpUtil::ValuesIterator iterator_; + + std::string keyid_; + std::string salt_; + uint64_t rs_; +}; + +// Iterates over a header that follows the syntax of the Crypto-Key HTTP header +// per the Encrypted Content-Encoding for HTTP draft. This header follows the +// #list syntax from the extended ABNF syntax defined in section 1.2 of RFC7230. +// +// https://tools.ietf.org/html/draft-thomson-http-encryption#section-4 +// https://tools.ietf.org/html/rfc7230#section-1.2 +class CryptoKeyHeaderIterator { + public: + CryptoKeyHeaderIterator(std::string::const_iterator header_begin, + std::string::const_iterator header_end); + ~CryptoKeyHeaderIterator(); + + // Advances the iterator to the next header value, if any. Returns true if + // there is a next value. Use the keyid(), aesgcm128() and dh() methods to + // access the key-value pairs included in the header value. + bool GetNext(); + + const std::string& keyid() const { + return keyid_; + } + + const std::string& aesgcm128() const { + return aesgcm128_; + } + + const std::string& dh() const { + return dh_; + } + + private: + net::HttpUtil::ValuesIterator iterator_; + + std::string keyid_; + std::string aesgcm128_; + std::string dh_; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_CRYPTO_ENCRYPTION_HEADER_PARSERS_H_ diff --git a/chromium/components/gcm_driver/crypto/encryption_header_parsers_unittest.cc b/chromium/components/gcm_driver/crypto/encryption_header_parsers_unittest.cc new file mode 100644 index 00000000000..3fd7026d6a0 --- /dev/null +++ b/chromium/components/gcm_driver/crypto/encryption_header_parsers_unittest.cc @@ -0,0 +1,374 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/crypto/encryption_header_parsers.h" + +#include +#include + + +#include "base/cxx17_backports.h" +#include "base/strings/string_number_conversions.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { + +namespace { + +const uint64_t kDefaultRecordSize = 4096; + +TEST(EncryptionHeaderParsersTest, ParseValidEncryptionHeaders) { + struct { + const char* const header; + const char* const parsed_keyid; + const char* const parsed_salt; + uint64_t parsed_rs; + } expected_results[] = { + { "keyid=foo;salt=c2l4dGVlbmNvb2xieXRlcw;rs=1024", + "foo", "sixteencoolbytes", 1024 }, + { "keyid=foo; salt=c2l4dGVlbmNvb2xieXRlcw; rs=1024", + "foo", "sixteencoolbytes", 1024 }, + { "KEYID=foo;SALT=c2l4dGVlbmNvb2xieXRlcw;RS=1024", + "foo", "sixteencoolbytes", 1024 }, + { " keyid = foo ; salt = c2l4dGVlbmNvb2xieXRlcw ; rs = 1024 ", + "foo", "sixteencoolbytes", 1024 }, + { "keyid=foo", "foo", "", kDefaultRecordSize }, + { "keyid=foo;", "foo", "", kDefaultRecordSize }, + { "keyid=\"foo\"", "foo", "", kDefaultRecordSize }, + { "salt=c2l4dGVlbmNvb2xieXRlcw", + "", "sixteencoolbytes", kDefaultRecordSize }, + { "rs=2048", "", "", 2048 }, + { "keyid=foo;someothervalue=1;rs=42", "foo", "", 42 }, + }; + + for (size_t i = 0; i < base::size(expected_results); i++) { + SCOPED_TRACE(i); + + std::string header(expected_results[i].header); + + EncryptionHeaderIterator iterator(header.begin(), header.end()); + ASSERT_TRUE(iterator.GetNext()); + + EXPECT_EQ(expected_results[i].parsed_keyid, iterator.keyid()); + EXPECT_EQ(expected_results[i].parsed_salt, iterator.salt()); + EXPECT_EQ(expected_results[i].parsed_rs, iterator.rs()); + + EXPECT_FALSE(iterator.GetNext()); + } +} + +TEST(EncryptionHeaderParsersTest, ParseValidMultiValueEncryptionHeaders) { + const size_t kNumberOfValues = 2u; + + struct { + const char* const header; + struct { + const char* const keyid; + const char* const salt; + uint64_t rs; + } parsed_values[kNumberOfValues]; + } expected_results[] = { + { "keyid=foo;salt=c2l4dGVlbmNvb2xieXRlcw;rs=1024,keyid=foo;salt=c2l4dGVlbm" + "Nvb2xieXRlcw;rs=1024", + { { "foo", "sixteencoolbytes", 1024 }, + { "foo", "sixteencoolbytes", 1024 } } }, + { "keyid=foo,salt=c2l4dGVlbmNvb2xieXRlcw;rs=1024", + { { "foo", "", kDefaultRecordSize }, + { "", "sixteencoolbytes", 1024 } } }, + { "keyid=foo,keyid=bar;salt=c2l4dGVlbmNvb2xieXRlcw;rs=1024", + { { "foo", "", kDefaultRecordSize }, + { "bar", "sixteencoolbytes", 1024 } } }, + { "keyid=\"foo,keyid=bar\",salt=c2l4dGVlbmNvb2xieXRlcw", + { { "foo,keyid=bar", "", kDefaultRecordSize }, + { "", "sixteencoolbytes", kDefaultRecordSize } } }, + }; + + for (size_t i = 0; i < base::size(expected_results); i++) { + SCOPED_TRACE(i); + + std::string header(expected_results[i].header); + + EncryptionHeaderIterator iterator(header.begin(), header.end()); + for (size_t j = 0; j < kNumberOfValues; ++j) { + ASSERT_TRUE(iterator.GetNext()); + + EXPECT_EQ(expected_results[i].parsed_values[j].keyid, iterator.keyid()); + EXPECT_EQ(expected_results[i].parsed_values[j].salt, iterator.salt()); + EXPECT_EQ(expected_results[i].parsed_values[j].rs, iterator.rs()); + } + + EXPECT_FALSE(iterator.GetNext()); + } +} + +TEST(EncryptionHeaderParsersTest, ParseInvalidEncryptionHeaders) { + const char* const expected_failures[] = { + // Values in the name-value pairs are not optional. + "keyid", + "keyid=", + "keyid=foo;keyid", + "salt", + "salt=", + "rs", + "rs=", + + // Supplying the same name multiple times in the same value is invalid. + "keyid=foo;keyid=bar", + "keyid=foo;bar=baz;keyid=qux", + + // The salt must be a URL-safe base64 decodable string. + "salt=YmV/2ZXJ-sMDA", + "salt=dHdlbHZlY29vbGJ5dGVz=====", + "salt=c2l4dGVlbmNvb2xieXRlcw;salt=123$xyz", + "salt=123$xyz", + + // The record size must be a positive decimal integer greater than one that + // does not start with a plus. + "rs=0", + "rs=0x13", + "rs=1", + "rs=-1", + "rs=+5", + "rs=99999999999999999999999999999999", + "rs=foobar", + }; + + const char* const expected_failures_second_iter[] = { + // Valid first field, missing value in the second field. + "keyid=foo,novaluekey", + + // Valid first field, undecodable salt in the second field. + "salt=c2l4dGVlbmNvb2xieXRlcw,salt=123$xyz", + + // Valid first field, invalid record size in the second field. + "rs=2,rs=0", + }; + + for (size_t i = 0; i < base::size(expected_failures); i++) { + SCOPED_TRACE(i); + + std::string header(expected_failures[i]); + + EncryptionHeaderIterator iterator(header.begin(), header.end()); + EXPECT_FALSE(iterator.GetNext()); + } + + for (size_t i = 0; i < base::size(expected_failures_second_iter); i++) { + SCOPED_TRACE(i); + + std::string header(expected_failures_second_iter[i]); + + EncryptionHeaderIterator iterator(header.begin(), header.end()); + EXPECT_TRUE(iterator.GetNext()); + EXPECT_FALSE(iterator.GetNext()); + } +} + +TEST(EncryptionHeaderParsersTest, ParseValidCryptoKeyHeaders) { + struct { + const char* const header; + const char* const parsed_keyid; + const char* const parsed_aesgcm128; + const char* const parsed_dh; + } expected_results[] = { + { "keyid=foo;aesgcm128=c2l4dGVlbmNvb2xieXRlcw;dh=dHdlbHZlY29vbGJ5dGVz", + "foo", "sixteencoolbytes", "twelvecoolbytes" }, + { "keyid=foo; aesgcm128=c2l4dGVlbmNvb2xieXRlcw; dh=dHdlbHZlY29vbGJ5dGVz", + "foo", "sixteencoolbytes", "twelvecoolbytes" }, + { "keyid = foo ; aesgcm128 = c2l4dGVlbmNvb2xieXRlcw ; dh = dHdlbHZlY29vbGJ5" + "dGVz ", + "foo", "sixteencoolbytes", "twelvecoolbytes" }, + { "KEYID=foo;AESGCM128=c2l4dGVlbmNvb2xieXRlcw;DH=dHdlbHZlY29vbGJ5dGVz", + "foo", "sixteencoolbytes", "twelvecoolbytes" }, + { "keyid=foo", "foo", "", "" }, + { "aesgcm128=c2l4dGVlbmNvb2xieXRlcw", "", "sixteencoolbytes", "" }, + { "aesgcm128=\"c2l4dGVlbmNvb2xieXRlcw\"", "", "sixteencoolbytes", "" }, + { "dh=dHdlbHZlY29vbGJ5dGVz", "", "", "twelvecoolbytes" }, + { "keyid=foo;someothervalue=bar;aesgcm128=dHdlbHZlY29vbGJ5dGVz", + "foo", "twelvecoolbytes", "" }, + }; + + for (size_t i = 0; i < base::size(expected_results); i++) { + SCOPED_TRACE(i); + + std::string header(expected_results[i].header); + + CryptoKeyHeaderIterator iterator(header.begin(), header.end()); + ASSERT_TRUE(iterator.GetNext()); + + EXPECT_EQ(expected_results[i].parsed_keyid, iterator.keyid()); + EXPECT_EQ(expected_results[i].parsed_aesgcm128, iterator.aesgcm128()); + EXPECT_EQ(expected_results[i].parsed_dh, iterator.dh()); + + EXPECT_FALSE(iterator.GetNext()); + } +} + +TEST(EncryptionHeaderParsersTest, ParseValidMultiValueCryptoKeyHeaders) { + const size_t kNumberOfValues = 2u; + + struct { + const char* const header; + struct { + const char* const keyid; + const char* const aesgcm128; + const char* const dh; + } parsed_values[kNumberOfValues]; + } expected_results[] = { + { "keyid=foo;aesgcm128=c2l4dGVlbmNvb2xieXRlcw;dh=dHdlbHZlY29vbGJ5dGVz," + "keyid=bar;aesgcm128=dHdlbHZlY29vbGJ5dGVz;dh=c2l4dGVlbmNvb2xieXRlcw", + { { "foo", "sixteencoolbytes", "twelvecoolbytes" }, + { "bar", "twelvecoolbytes", "sixteencoolbytes" } } }, + { "keyid=foo,aesgcm128=c2l4dGVlbmNvb2xieXRlcw", + { { "foo", "", "" }, + { "", "sixteencoolbytes", "" } } }, + { "keyid=foo,keyid=bar;dh=dHdlbHZlY29vbGJ5dGVz", + { { "foo", "", "" }, + { "bar", "", "twelvecoolbytes" } } }, + { "keyid=\"foo,keyid=bar\",aesgcm128=c2l4dGVlbmNvb2xieXRlcw", + { { "foo,keyid=bar", "", "" }, + { "", "sixteencoolbytes", "" } } }, + }; + + for (size_t i = 0; i < base::size(expected_results); i++) { + SCOPED_TRACE(i); + + std::string header(expected_results[i].header); + + CryptoKeyHeaderIterator iterator(header.begin(), header.end()); + for (size_t j = 0; j < kNumberOfValues; ++j) { + ASSERT_TRUE(iterator.GetNext()); + + EXPECT_EQ(expected_results[i].parsed_values[j].keyid, iterator.keyid()); + EXPECT_EQ(expected_results[i].parsed_values[j].aesgcm128, + iterator.aesgcm128()); + EXPECT_EQ(expected_results[i].parsed_values[j].dh, iterator.dh()); + } + + EXPECT_FALSE(iterator.GetNext()); + } +} + +TEST(EncryptionHeaderParsersTest, DISABLED_ParseInvalidCryptoKeyHeaders) { + const char* const expected_failures[] = { + // Values in the name-value pairs are not optional. + "keyid", + "keyid=", + "keyid=foo;keyid", + "aesgcm128", + "aesgcm128=", + "dh", + "dh=", + + // Supplying the same name multiple times in the same value is invalid. + "keyid=foo;keyid=bar", + "keyid=foo;bar=baz;keyid=qux", + + // The "aesgcm128" parameter must be a URL-safe base64 decodable string. + "aesgcm128=123$xyz", + "aesgcm128=foobar;aesgcm128=123$xyz", + + // The "dh" parameter must be a URL-safe base64 decodable string. + "dh=YmV/2ZXJ-sMDA", + "dh=dHdlbHZlY29vbGJ5dGVz=====", + "dh=123$xyz", + }; + + const char* const expected_failures_second_iter[] = { + // Valid first field, missing value in the second field. + "keyid=foo,novaluekey", + + // Valid first field, undecodable aesgcm128 value in the second field. + "dh=dHdlbHZlY29vbGJ5dGVz,aesgcm128=123$xyz", + }; + + for (size_t i = 0; i < base::size(expected_failures); i++) { + SCOPED_TRACE(i); + + std::string header(expected_failures[i]); + + CryptoKeyHeaderIterator iterator(header.begin(), header.end()); + EXPECT_FALSE(iterator.GetNext()); + } + + for (size_t i = 0; i < base::size(expected_failures_second_iter); i++) { + SCOPED_TRACE(i); + + std::string header(expected_failures_second_iter[i]); + + CryptoKeyHeaderIterator iterator(header.begin(), header.end()); + EXPECT_TRUE(iterator.GetNext()); + EXPECT_FALSE(iterator.GetNext()); + } +} + +TEST(EncryptionHeaderParsersTest, SixValueHeader) { + const std::string header("keyid=0,keyid=1,keyid=2,keyid=3,keyid=4,keyid=5"); + + EncryptionHeaderIterator encryption_iterator(header.begin(), header.end()); + CryptoKeyHeaderIterator crypto_key_iterator(header.begin(), header.end()); + + for (size_t i = 0; i < 6; ++i) { + SCOPED_TRACE(i); + + ASSERT_TRUE(encryption_iterator.GetNext()); + ASSERT_TRUE(crypto_key_iterator.GetNext()); + } + + EXPECT_FALSE(encryption_iterator.GetNext()); + EXPECT_FALSE(crypto_key_iterator.GetNext()); +} + +TEST(EncryptionHeaderParsersTest, InvalidHeadersResetOutput) { + // Valid first field, invalid record size parameter in the second field. + const std::string encryption_header( + "keyid=foo;salt=c2l4dGVlbmNvb2xieXRlcw;rs=1024,rs=foobar"); + + // Valid first field, undecodable aesgcm128 parameter in the second field. + const std::string crypto_key_header( + "keyid=foo;aesgcm128=c2l4dGVlbmNvb2xieXRlcw;dh=dHdlbHZlY29vbGJ5dGVz," + "aesgcm128=$$$"); + + EncryptionHeaderIterator encryption_iterator( + encryption_header.begin(), encryption_header.end()); + + ASSERT_EQ(0u, encryption_iterator.keyid().size()); + ASSERT_EQ(0u, encryption_iterator.salt().size()); + ASSERT_EQ(4096u, encryption_iterator.rs()); + + ASSERT_TRUE(encryption_iterator.GetNext()); + + EXPECT_EQ("foo", encryption_iterator.keyid()); + EXPECT_EQ("sixteencoolbytes", encryption_iterator.salt()); + EXPECT_EQ(1024u, encryption_iterator.rs()); + + ASSERT_FALSE(encryption_iterator.GetNext()); + + EXPECT_EQ(0u, encryption_iterator.keyid().size()); + EXPECT_EQ(0u, encryption_iterator.salt().size()); + EXPECT_EQ(4096u, encryption_iterator.rs()); + + CryptoKeyHeaderIterator crypto_key_iterator( + crypto_key_header.begin(), crypto_key_header.end()); + + ASSERT_EQ(0u, crypto_key_iterator.keyid().size()); + ASSERT_EQ(0u, crypto_key_iterator.aesgcm128().size()); + ASSERT_EQ(0u, crypto_key_iterator.dh().size()); + + ASSERT_TRUE(crypto_key_iterator.GetNext()); + + EXPECT_EQ("foo", crypto_key_iterator.keyid()); + EXPECT_EQ("sixteencoolbytes", crypto_key_iterator.aesgcm128()); + EXPECT_EQ("twelvecoolbytes", crypto_key_iterator.dh()); + + ASSERT_FALSE(crypto_key_iterator.GetNext()); + + EXPECT_EQ(0u, crypto_key_iterator.keyid().size()); + EXPECT_EQ(0u, crypto_key_iterator.aesgcm128().size()); + EXPECT_EQ(0u, crypto_key_iterator.dh().size()); +} + +} // namespace + +} // namespace gcm diff --git a/chromium/components/gcm_driver/crypto/gcm_crypto_test_helpers.cc b/chromium/components/gcm_driver/crypto/gcm_crypto_test_helpers.cc new file mode 100644 index 00000000000..00c0d16cb61 --- /dev/null +++ b/chromium/components/gcm_driver/crypto/gcm_crypto_test_helpers.cc @@ -0,0 +1,84 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/crypto/gcm_crypto_test_helpers.h" + +#include + +#include +#include + +#include "base/base64url.h" +#include "base/strings/string_util.h" +#include "components/gcm_driver/common/gcm_message.h" +#include "components/gcm_driver/crypto/gcm_message_cryptographer.h" +#include "components/gcm_driver/crypto/p256_key_util.h" +#include "crypto/ec_private_key.h" +#include "crypto/random.h" + +namespace gcm { + +bool CreateEncryptedPayloadForTesting(const base::StringPiece& payload, + const base::StringPiece& peer_public_key, + const base::StringPiece& auth_secret, + IncomingMessage* message) { + DCHECK(message); + + // Create an ephemeral key for the sender. + std::unique_ptr key = crypto::ECPrivateKey::Create(); + if (!key) + return false; + + std::string shared_secret; + // Calculate the shared secret between the sender and its peer. + if (!ComputeSharedP256Secret(*key, peer_public_key, &shared_secret)) { + return false; + } + + std::string salt; + + // Generate a cryptographically secure random salt for the message. + const size_t salt_size = GCMMessageCryptographer::kSaltSize; + crypto::RandBytes(base::WriteInto(&salt, salt_size + 1), salt_size); + + GCMMessageCryptographer cryptographer( + GCMMessageCryptographer::Version::DRAFT_03); + + size_t record_size; + std::string ciphertext; + + std::string public_key; + if (!GetRawPublicKey(*key, &public_key)) + return false; + if (!cryptographer.Encrypt(peer_public_key, public_key, shared_secret, + auth_secret, salt, payload, &record_size, + &ciphertext)) { + return false; + } + + std::string encoded_salt, encoded_public_key; + + // Create base64url encoded representations of the salt and local public key. + base::Base64UrlEncode(salt, base::Base64UrlEncodePolicy::OMIT_PADDING, + &encoded_salt); + base::Base64UrlEncode(public_key, base::Base64UrlEncodePolicy::OMIT_PADDING, + &encoded_public_key); + + // Write the Encryption header value to |*message|. + std::stringstream encryption_header; + encryption_header << "salt=" << encoded_salt << ";rs=" << record_size; + + message->data["encryption"] = encryption_header.str(); + + // Write the Crypto-Key value to |*message|. + std::stringstream crypto_key_header; + crypto_key_header << "dh=" << encoded_public_key; + + message->data["crypto-key"] = crypto_key_header.str(); + + message->raw_data.swap(ciphertext); + return true; +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/crypto/gcm_crypto_test_helpers.h b/chromium/components/gcm_driver/crypto/gcm_crypto_test_helpers.h new file mode 100644 index 00000000000..8579ccf1f90 --- /dev/null +++ b/chromium/components/gcm_driver/crypto/gcm_crypto_test_helpers.h @@ -0,0 +1,25 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_GCM_DRIVER_CRYPTO_GCM_CRYPTO_TEST_HELPERS_H_ +#define COMPONENTS_GCM_DRIVER_CRYPTO_GCM_CRYPTO_TEST_HELPERS_H_ + +#include "base/strings/string_piece.h" + +namespace gcm { + +struct IncomingMessage; + +// Creates an encrypted representation of |payload| using the |peer_public_key| +// (as an octet string in uncompressed form per SEC1 2.3.3) and the +// |auth_secret|. Returns whether the payload could be created and has been +// written to the |*message|. +bool CreateEncryptedPayloadForTesting(const base::StringPiece& payload, + const base::StringPiece& peer_public_key, + const base::StringPiece& auth_secret, + IncomingMessage* message); + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_CRYPTO_GCM_CRYPTO_TEST_HELPERS_H_ diff --git a/chromium/components/gcm_driver/crypto/gcm_decryption_result.cc b/chromium/components/gcm_driver/crypto/gcm_decryption_result.cc new file mode 100644 index 00000000000..d5cba73bc37 --- /dev/null +++ b/chromium/components/gcm_driver/crypto/gcm_decryption_result.cc @@ -0,0 +1,50 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/crypto/gcm_decryption_result.h" + +#include "base/notreached.h" + +namespace gcm { + +std::string ToGCMDecryptionResultDetailsString(GCMDecryptionResult result) { + switch (result) { + case GCMDecryptionResult::UNENCRYPTED: + return "Message was not encrypted"; + case GCMDecryptionResult::DECRYPTED_DRAFT_03: + return "Message decrypted (draft 03)"; + case GCMDecryptionResult::DECRYPTED_DRAFT_08: + return "Message decrypted (draft 08)"; + case GCMDecryptionResult::INVALID_ENCRYPTION_HEADER: + return "Invalid format for the Encryption header"; + case GCMDecryptionResult::INVALID_CRYPTO_KEY_HEADER: + return "Invalid format for the Crypto-Key header"; + case GCMDecryptionResult::NO_KEYS: + return "There are no associated keys with the subscription"; + case GCMDecryptionResult::INVALID_SHARED_SECRET: + return "The shared secret cannot be derived from the keying material"; + case GCMDecryptionResult::INVALID_PAYLOAD: + return "AES-GCM decryption failed"; + case GCMDecryptionResult::INVALID_BINARY_HEADER_PAYLOAD_LENGTH: + return "The message payload is smaller than the smallest valid message " + "(104 bytes)"; + case GCMDecryptionResult::INVALID_BINARY_HEADER_RECORD_SIZE: + return "The record size indicated in the binary message header is " + "smaller than the smallest valid record size (18 bytes)"; + case GCMDecryptionResult::INVALID_BINARY_HEADER_PUBLIC_KEY_LENGTH: + return "The public key included in the binary message header must be a " + "valid P-256 ECDH uncompressed point that is 65 bytes in length."; + case GCMDecryptionResult::INVALID_BINARY_HEADER_PUBLIC_KEY_FORMAT: + return "The public key included in the binary message header must be a " + "valid P-256 ECDH uncompressed poin that starts with an 0x04 " + "byte."; + case GCMDecryptionResult::ENUM_SIZE: + break; // deliberate fall-through + } + + NOTREACHED(); + return "(invalid result)"; +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/crypto/gcm_decryption_result.h b/chromium/components/gcm_driver/crypto/gcm_decryption_result.h new file mode 100644 index 00000000000..5f073aa9312 --- /dev/null +++ b/chromium/components/gcm_driver/crypto/gcm_decryption_result.h @@ -0,0 +1,71 @@ +// Copyright 2017 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 COMPONENTS_GCM_DRIVER_CRYPTO_GCM_DECRYPTION_RESULT_H_ +#define COMPONENTS_GCM_DRIVER_CRYPTO_GCM_DECRYPTION_RESULT_H_ + +#include + +namespace gcm { + +// Result of decrypting an incoming message. The values of these reasons must +// not be changed as they are being recorded using UMA. When adding a value, +// please update GCMDecryptionResult in //tools/metrics/histograms/enums.xml. +enum class GCMDecryptionResult { + // The message had not been encrypted by the sender. + UNENCRYPTED = 0, + + // The message had been encrypted by the sender, and could successfully be + // decrypted for the registration it has been received for. The encryption + // scheme used for the message was draft-ietf-webpush-encryption-03. + DECRYPTED_DRAFT_03 = 1, + + // The contents of the Encryption HTTP header could not be parsed. + INVALID_ENCRYPTION_HEADER = 2, + + // The contents of the Crypto-Key HTTP header could not be parsed. + INVALID_CRYPTO_KEY_HEADER = 3, + + // No public/private key-pair was associated with the app_id. + NO_KEYS = 4, + + // The shared secret cannot be derived from the keying material. + INVALID_SHARED_SECRET = 5, + + // The payload could not be decrypted as AES-128-GCM. + INVALID_PAYLOAD = 6, + + // Removed in favour of the more detailed reasons below (values 9-13). + // INVALID_BINARY_HEADER = 7, + + // The message had been encrypted by the sender, and could successfully be + // decrypted for the registration it has been received for. The encryption + // scheme used for the message was draft-ietf-webpush-encryption-08. + DECRYPTED_DRAFT_08 = 8, + + // The payload's length is smaller than the smallest valid message. + INVALID_BINARY_HEADER_PAYLOAD_LENGTH = 9, + + // The payload's record size is smaller than the smallest valid record + 1. + INVALID_BINARY_HEADER_RECORD_SIZE = 10, + + // The public key included in the payload does not have the length + // corresponding to an uncompressed P-256 ECDH key (65 bytes). + INVALID_BINARY_HEADER_PUBLIC_KEY_LENGTH = 11, + + // The public key included in the message does not adhere to the format of + // an uncompressed P-256 ECDH key. (I.e. it must start with 0x04.) + INVALID_BINARY_HEADER_PUBLIC_KEY_FORMAT = 12, + + // Should be one more than the otherwise highest value in this enumeration. + ENUM_SIZE = INVALID_BINARY_HEADER_PUBLIC_KEY_FORMAT + 1 +}; + +// Converts the GCMDecryptionResult value to a string that can be used to +// explain the issue on chrome://gcm-internals/. +std::string ToGCMDecryptionResultDetailsString(GCMDecryptionResult result); + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_CRYPTO_GCM_DECRYPTION_RESULT_H_ diff --git a/chromium/components/gcm_driver/crypto/gcm_encryption_provider.cc b/chromium/components/gcm_driver/crypto/gcm_encryption_provider.cc new file mode 100644 index 00000000000..d7da79727db --- /dev/null +++ b/chromium/components/gcm_driver/crypto/gcm_encryption_provider.cc @@ -0,0 +1,412 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/crypto/gcm_encryption_provider.h" + +#include + +#include "base/base64.h" +#include "base/big_endian.h" +#include "base/bind.h" +#include "base/logging.h" +#include "base/strings/strcat.h" +#include "components/gcm_driver/common/gcm_message.h" +#include "components/gcm_driver/crypto/encryption_header_parsers.h" +#include "components/gcm_driver/crypto/gcm_decryption_result.h" +#include "components/gcm_driver/crypto/gcm_encryption_result.h" +#include "components/gcm_driver/crypto/gcm_key_store.h" +#include "components/gcm_driver/crypto/gcm_message_cryptographer.h" +#include "components/gcm_driver/crypto/message_payload_parser.h" +#include "components/gcm_driver/crypto/p256_key_util.h" +#include "components/gcm_driver/crypto/proto/gcm_encryption_data.pb.h" +#include "crypto/ec_private_key.h" +#include "crypto/random.h" + +namespace gcm { + +namespace { + +const char kEncryptionProperty[] = "encryption"; +const char kCryptoKeyProperty[] = "crypto-key"; +const char kInternalRawData[] = "_googRawData"; + +// Directory in the GCM Store in which the encryption database will be stored. +const base::FilePath::CharType kEncryptionDirectoryName[] = + FILE_PATH_LITERAL("Encryption"); + +IncomingMessage CreateMessageWithId(const std::string& message_id) { + IncomingMessage message; + message.message_id = message_id; + return message; +} + +} // namespace + +GCMEncryptionProvider::GCMEncryptionProvider() {} + +GCMEncryptionProvider::~GCMEncryptionProvider() = default; + +// static +const char GCMEncryptionProvider::kContentEncodingProperty[] = + "content-encoding"; + +// static +const char GCMEncryptionProvider::kContentCodingAes128Gcm[] = "aes128gcm"; + +void GCMEncryptionProvider::Init( + const base::FilePath& store_path, + const scoped_refptr& blocking_task_runner) { + DCHECK(!key_store_); + + base::FilePath encryption_store_path = store_path; + + // |store_path| can be empty in tests, which means that the database should + // be created in memory rather than on-disk. + if (!store_path.empty()) + encryption_store_path = store_path.Append(kEncryptionDirectoryName); + + key_store_ = std::make_unique(encryption_store_path, + blocking_task_runner); +} + +void GCMEncryptionProvider::GetEncryptionInfo( + const std::string& app_id, + const std::string& authorized_entity, + EncryptionInfoCallback callback) { + DCHECK(key_store_); + key_store_->GetKeys( + app_id, authorized_entity, + false /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMEncryptionProvider::DidGetEncryptionInfo, + weak_ptr_factory_.GetWeakPtr(), app_id, authorized_entity, + std::move(callback))); +} + +void GCMEncryptionProvider::DidGetEncryptionInfo( + const std::string& app_id, + const std::string& authorized_entity, + EncryptionInfoCallback callback, + std::unique_ptr key, + const std::string& auth_secret) { + if (!key) { + key_store_->CreateKeys( + app_id, authorized_entity, + base::BindOnce(&GCMEncryptionProvider::DidCreateEncryptionInfo, + weak_ptr_factory_.GetWeakPtr(), std::move(callback))); + return; + } + + std::string public_key; + const bool success = GetRawPublicKey(*key, &public_key); + DCHECK(success); + std::move(callback).Run(public_key, auth_secret); +} + +void GCMEncryptionProvider::RemoveEncryptionInfo( + const std::string& app_id, + const std::string& authorized_entity, + base::OnceClosure callback) { + DCHECK(key_store_); + key_store_->RemoveKeys(app_id, authorized_entity, std::move(callback)); +} + +bool GCMEncryptionProvider::IsEncryptedMessage( + const IncomingMessage& message) const { + // Messages that explicitly specify their content coding to be "aes128gcm" + // indicate that they use draft-ietf-webpush-encryption-08. + auto content_encoding_iter = message.data.find(kContentEncodingProperty); + if (content_encoding_iter != message.data.end() && + content_encoding_iter->second == kContentCodingAes128Gcm) { + return true; + } + + // The Web Push protocol requires the encryption and crypto-key properties to + // be set, and the raw_data field to be populated with the payload. + if (message.data.find(kEncryptionProperty) == message.data.end() || + message.data.find(kCryptoKeyProperty) == message.data.end()) + return false; + + return message.raw_data.size() > 0; +} + +void GCMEncryptionProvider::DecryptMessage(const std::string& app_id, + const IncomingMessage& message, + DecryptMessageCallback callback) { + DCHECK(key_store_); + if (!IsEncryptedMessage(message)) { + std::move(callback).Run(GCMDecryptionResult::UNENCRYPTED, message); + return; + } + + std::string salt, public_key, ciphertext; + GCMMessageCryptographer::Version version; + uint32_t record_size; + + auto content_encoding_iter = message.data.find(kContentEncodingProperty); + if (content_encoding_iter != message.data.end() && + content_encoding_iter->second == kContentCodingAes128Gcm) { + // The message follows encryption per draft-ietf-webpush-encryption-08. Use + // the binary header of the message to derive the values. + + auto parser = std::make_unique(message.raw_data); + if (!parser->IsValid()) { + // Attempt to parse base64 encoded internal raw data. + auto raw_data_iter = message.data.find(kInternalRawData); + std::string raw_data; + if (raw_data_iter == message.data.end() || + !base::Base64Decode(raw_data_iter->second, &raw_data) || + !(parser = std::make_unique(raw_data)) + ->IsValid()) { + DLOG(ERROR) << "Unable to parse the message's binary header"; + std::move(callback).Run(parser->GetFailureReason(), + CreateMessageWithId(message.message_id)); + return; + } + } + + salt = parser->salt(); + public_key = parser->public_key(); + record_size = parser->record_size(); + ciphertext = parser->ciphertext(); + version = GCMMessageCryptographer::Version::DRAFT_08; + } else { + // The message follows encryption per draft-ietf-webpush-encryption-03. Use + // the Encryption and Crypto-Key header values to derive the values. + + const auto& encryption_header = message.data.find(kEncryptionProperty); + DCHECK(encryption_header != message.data.end()); + + const auto& crypto_key_header = message.data.find(kCryptoKeyProperty); + DCHECK(crypto_key_header != message.data.end()); + + EncryptionHeaderIterator encryption_header_iterator( + encryption_header->second.begin(), encryption_header->second.end()); + if (!encryption_header_iterator.GetNext()) { + DLOG(ERROR) << "Unable to parse the value of the Encryption header"; + std::move(callback).Run(GCMDecryptionResult::INVALID_ENCRYPTION_HEADER, + CreateMessageWithId(message.message_id)); + return; + } + + if (encryption_header_iterator.salt().size() != + GCMMessageCryptographer::kSaltSize) { + DLOG(ERROR) << "Invalid values supplied in the Encryption header"; + std::move(callback).Run(GCMDecryptionResult::INVALID_ENCRYPTION_HEADER, + CreateMessageWithId(message.message_id)); + return; + } + + salt = encryption_header_iterator.salt(); + record_size = encryption_header_iterator.rs(); + + CryptoKeyHeaderIterator crypto_key_header_iterator( + crypto_key_header->second.begin(), crypto_key_header->second.end()); + if (!crypto_key_header_iterator.GetNext()) { + DLOG(ERROR) << "Unable to parse the value of the Crypto-Key header"; + std::move(callback).Run(GCMDecryptionResult::INVALID_CRYPTO_KEY_HEADER, + CreateMessageWithId(message.message_id)); + return; + } + + // Ignore values that don't include the "dh" property. When using VAPID, it + // is valid for the application server to supply multiple values. + while (crypto_key_header_iterator.dh().empty() && + crypto_key_header_iterator.GetNext()) { + } + + bool valid_crypto_key_header = false; + + if (!crypto_key_header_iterator.dh().empty()) { + public_key = crypto_key_header_iterator.dh(); + valid_crypto_key_header = true; + + // Guard against the "dh" property being included more than once. + while (crypto_key_header_iterator.GetNext()) { + if (crypto_key_header_iterator.dh().empty()) + continue; + + valid_crypto_key_header = false; + break; + } + } + + if (!valid_crypto_key_header) { + DLOG(ERROR) << "Invalid values supplied in the Crypto-Key header"; + std::move(callback).Run(GCMDecryptionResult::INVALID_CRYPTO_KEY_HEADER, + CreateMessageWithId(message.message_id)); + return; + } + + ciphertext = message.raw_data; + version = GCMMessageCryptographer::Version::DRAFT_03; + } + + // Use |fallback_to_empty_authorized_entity|, since this message might have + // been sent to either an InstanceID token or a non-InstanceID registration. + key_store_->GetKeys( + app_id, message.sender_id /* authorized_entity */, + true /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMEncryptionProvider::DecryptMessageWithKey, + weak_ptr_factory_.GetWeakPtr(), message.message_id, + message.collapse_key, message.sender_id, std::move(salt), + std::move(public_key), record_size, std::move(ciphertext), + version, std::move(callback))); +} // namespace gcm + +void GCMEncryptionProvider::EncryptMessage(const std::string& app_id, + const std::string& authorized_entity, + const std::string& p256dh, + const std::string& auth_secret, + const std::string& message, + EncryptMessageCallback callback) { + DCHECK(key_store_); + key_store_->GetKeys( + app_id, authorized_entity, + false /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMEncryptionProvider::EncryptMessageWithKey, + weak_ptr_factory_.GetWeakPtr(), app_id, authorized_entity, + p256dh, auth_secret, message, std::move(callback))); +} + +void GCMEncryptionProvider::DidCreateEncryptionInfo( + EncryptionInfoCallback callback, + std::unique_ptr key, + const std::string& auth_secret) { + if (!key) { + std::move(callback).Run(std::string() /* p256dh */, + std::string() /* auth_secret */); + return; + } + + std::string public_key; + const bool success = GetRawPublicKey(*key, &public_key); + DCHECK(success); + std::move(callback).Run(public_key, auth_secret); +} + +void GCMEncryptionProvider::DecryptMessageWithKey( + const std::string& message_id, + const std::string& collapse_key, + const std::string& sender_id, + const std::string& salt, + const std::string& public_key, + uint32_t record_size, + const std::string& ciphertext, + GCMMessageCryptographer::Version version, + DecryptMessageCallback callback, + std::unique_ptr key, + const std::string& auth_secret) { + if (!key) { + DLOG(ERROR) << "Unable to retrieve the keys for the incoming message."; + std::move(callback).Run(GCMDecryptionResult::NO_KEYS, + CreateMessageWithId(message_id)); + return; + } + + std::string shared_secret; + if (!ComputeSharedP256Secret(*key, public_key, &shared_secret)) { + DLOG(ERROR) << "Unable to calculate the shared secret."; + std::move(callback).Run(GCMDecryptionResult::INVALID_SHARED_SECRET, + CreateMessageWithId(message_id)); + return; + } + + std::string plaintext; + + GCMMessageCryptographer cryptographer(version); + + std::string exported_public_key; + const bool success = GetRawPublicKey(*key, &exported_public_key); + DCHECK(success); + if (!cryptographer.Decrypt(exported_public_key, public_key, shared_secret, + auth_secret, salt, ciphertext, record_size, + &plaintext)) { + DLOG(ERROR) << "Unable to decrypt the incoming data."; + std::move(callback).Run(GCMDecryptionResult::INVALID_PAYLOAD, + CreateMessageWithId(message_id)); + return; + } + + IncomingMessage decrypted_message; + decrypted_message.message_id = message_id; + decrypted_message.collapse_key = collapse_key; + decrypted_message.sender_id = sender_id; + decrypted_message.raw_data.swap(plaintext); + decrypted_message.decrypted = true; + + // There must be no data associated with the decrypted message at this point, + // to make sure that we don't end up in an infinite decryption loop. + DCHECK_EQ(0u, decrypted_message.data.size()); + + std::move(callback).Run(version == GCMMessageCryptographer::Version::DRAFT_03 + ? GCMDecryptionResult::DECRYPTED_DRAFT_03 + : GCMDecryptionResult::DECRYPTED_DRAFT_08, + std::move(decrypted_message)); +} + +void GCMEncryptionProvider::EncryptMessageWithKey( + const std::string& app_id, + const std::string& authorized_entity, + const std::string& p256dh, + const std::string& auth_secret, + const std::string& message, + EncryptMessageCallback callback, + std::unique_ptr key, + const std::string& sender_auth_secret) { + if (!key) { + DLOG(ERROR) << "Unable to retrieve the keys for the outgoing message."; + std::move(callback).Run(GCMEncryptionResult::NO_KEYS, std::string()); + return; + } + + // Creates a cryptographically secure salt of |salt_size| octets in size, + // and calculate the shared secret for the message. + std::string salt; + crypto::RandBytes(base::WriteInto(&salt, 16 + 1), 16); + + std::string shared_secret; + if (!ComputeSharedP256Secret(*key, p256dh, &shared_secret)) { + DLOG(ERROR) << "Unable to calculate the shared secret."; + std::move(callback).Run(GCMEncryptionResult::INVALID_SHARED_SECRET, + std::string()); + return; + } + + size_t record_size; + std::string ciphertext; + + GCMMessageCryptographer cryptographer( + GCMMessageCryptographer::Version::DRAFT_08); + + std::string sender_public_key; + bool success = GetRawPublicKey(*key, &sender_public_key); + DCHECK(success); + if (!cryptographer.Encrypt(p256dh, sender_public_key, shared_secret, + auth_secret, salt, message, &record_size, + &ciphertext)) { + DLOG(ERROR) << "Unable to encrypt the incoming data."; + std::move(callback).Run(GCMEncryptionResult::ENCRYPTION_FAILED, + std::string()); + return; + } + + // Construct encryption header. + uint32_t rs = record_size; + char rs_buf[sizeof(rs)]; + base::WriteBigEndian(rs_buf, rs); + std::string rs_str(std::begin(rs_buf), std::end(rs_buf)); + + uint8_t key_length = sender_public_key.size(); + char key_length_buf[sizeof(key_length)]; + base::WriteBigEndian(key_length_buf, key_length); + std::string key_length_str(std::begin(key_length_buf), + std::end(key_length_buf)); + + std::string payload = base::StrCat( + {salt, rs_str, key_length_str, sender_public_key, ciphertext}); + std::move(callback).Run(GCMEncryptionResult::ENCRYPTED_DRAFT_08, + std::move(payload)); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/crypto/gcm_encryption_provider.h b/chromium/components/gcm_driver/crypto/gcm_encryption_provider.h new file mode 100644 index 00000000000..bdb6cd3d15e --- /dev/null +++ b/chromium/components/gcm_driver/crypto/gcm_encryption_provider.h @@ -0,0 +1,157 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_GCM_DRIVER_CRYPTO_GCM_ENCRYPTION_PROVIDER_H_ +#define COMPONENTS_GCM_DRIVER_CRYPTO_GCM_ENCRYPTION_PROVIDER_H_ + +#include + +#include +#include + +#include "base/callback_forward.h" +#include "base/gtest_prod_util.h" +#include "base/memory/weak_ptr.h" +#include "components/gcm_driver/crypto/gcm_message_cryptographer.h" + +namespace base { +class FilePath; +class SequencedTaskRunner; +} // namespace base + +namespace crypto { +class ECPrivateKey; +} // namespace crypto + +namespace gcm { + +enum class GCMDecryptionResult; +enum class GCMEncryptionResult; +class GCMKeyStore; +struct IncomingMessage; + +// Provider that enables the GCM Driver to deal with encryption key management +// and decryption of incoming messages. +class GCMEncryptionProvider { + public: + // Callback to be invoked when the public key and auth secret are available. + using EncryptionInfoCallback = + base::OnceCallback; + + // Callback to be invoked when a message may have been decrypted, as indicated + // by the |result|. The |message| contains the dispatchable message in success + // cases, or will be initialized to an empty, default state for failure. + using DecryptMessageCallback = + base::OnceCallback; + + // Callback to be invoked when a message may have been encrypted, as indicated + // by the |result|. The |message| contains the dispatchable message in success + // cases, or will be initialized to an empty, default state for failure. + using EncryptMessageCallback = + base::OnceCallback; + + static const char kContentEncodingProperty[]; + + // Content coding name defined by ietf-httpbis-encryption-encoding. + static const char kContentCodingAes128Gcm[]; + + GCMEncryptionProvider(); + + GCMEncryptionProvider(const GCMEncryptionProvider&) = delete; + GCMEncryptionProvider& operator=(const GCMEncryptionProvider&) = delete; + + ~GCMEncryptionProvider(); + + // Initializes the encryption provider with the |store_path| and the + // |blocking_task_runner|. Done separately from the constructor in order to + // avoid needing a blocking task runner for anything using GCMDriver. + void Init( + const base::FilePath& store_path, + const scoped_refptr& blocking_task_runner); + + // Retrieves the public key and authentication secret associated with the + // |app_id| + |authorized_entity| pair. Will create this info if necessary. + // |authorized_entity| should be the InstanceID token's authorized entity, or + // "" for non-InstanceID GCM registrations. + void GetEncryptionInfo(const std::string& app_id, + const std::string& authorized_entity, + EncryptionInfoCallback callback); + + // Removes all encryption information associated with the |app_id| + + // |authorized_entity| pair, then invokes |callback|. |authorized_entity| + // should be the InstanceID token's authorized entity, or "*" to remove for + // all InstanceID tokens, or "" for non-InstanceID GCM registrations. + void RemoveEncryptionInfo(const std::string& app_id, + const std::string& authorized_entity, + base::OnceClosure callback); + + // Determines whether |message| contains encrypted content. + bool IsEncryptedMessage(const IncomingMessage& message) const; + + // Attempts to decrypt the |message|. If the |message| is not encrypted, the + // |callback| will be invoked immediately. Otherwise |callback| will be called + // asynchronously when |message| has been decrypted. A dispatchable message + // will be used in case of success, an empty message in case of failure. + void DecryptMessage(const std::string& app_id, + const IncomingMessage& message, + DecryptMessageCallback callback); + + // Attempts to encrypt the |message| using draft-ietf-webpush-encryption-08 + // scheme. |callback| will be called asynchronously when |message| has been + // encrypted. A dispatchable message will be used in case of success, an empty + // message in case of failure. + void EncryptMessage(const std::string& app_id, + const std::string& authorized_entity, + const std::string& p256dh, + const std::string& auth_secret, + const std::string& message, + EncryptMessageCallback callback); + + private: + friend class GCMEncryptionProviderTest; + FRIEND_TEST_ALL_PREFIXES(GCMEncryptionProviderTest, + EncryptionRoundTripGCMRegistration); + FRIEND_TEST_ALL_PREFIXES(GCMEncryptionProviderTest, + EncryptionRoundTripInstanceIDToken); + + void DidGetEncryptionInfo(const std::string& app_id, + const std::string& authorized_entity, + EncryptionInfoCallback callback, + std::unique_ptr key, + const std::string& auth_secret); + + void DidCreateEncryptionInfo(EncryptionInfoCallback callback, + std::unique_ptr key, + const std::string& auth_secret); + + void DecryptMessageWithKey(const std::string& message_id, + const std::string& collapse_key, + const std::string& sender_id, + const std::string& salt, + const std::string& public_key, + uint32_t record_size, + const std::string& ciphertext, + GCMMessageCryptographer::Version version, + DecryptMessageCallback callback, + std::unique_ptr key, + const std::string& auth_secret); + + void EncryptMessageWithKey(const std::string& app_id, + const std::string& authorized_entity, + const std::string& p256dh, + const std::string& auth_secret, + const std::string& message, + EncryptMessageCallback callback, + std::unique_ptr key, + const std::string& sender_auth_secret); + + std::unique_ptr key_store_; + + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_CRYPTO_GCM_ENCRYPTION_PROVIDER_H_ diff --git a/chromium/components/gcm_driver/crypto/gcm_encryption_provider_unittest.cc b/chromium/components/gcm_driver/crypto/gcm_encryption_provider_unittest.cc new file mode 100644 index 00000000000..b4fe8b2f6dd --- /dev/null +++ b/chromium/components/gcm_driver/crypto/gcm_encryption_provider_unittest.cc @@ -0,0 +1,672 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/crypto/gcm_encryption_provider.h" + +#include + +#include +#include +#include + +#include "base/base64.h" +#include "base/base64url.h" +#include "base/big_endian.h" +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/files/scoped_temp_dir.h" +#include "base/run_loop.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_piece.h" +#include "base/strings/string_util.h" +#include "base/test/metrics/histogram_tester.h" +#include "base/test/task_environment.h" +#include "base/threading/thread_task_runner_handle.h" +#include "components/gcm_driver/common/gcm_message.h" +#include "components/gcm_driver/crypto/gcm_decryption_result.h" +#include "components/gcm_driver/crypto/gcm_encryption_result.h" +#include "components/gcm_driver/crypto/gcm_key_store.h" +#include "components/gcm_driver/crypto/gcm_message_cryptographer.h" +#include "components/gcm_driver/crypto/p256_key_util.h" +#include "crypto/random.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { +namespace { + +const char kExampleAppId[] = "my-app-id"; +const char kExampleAuthorizedEntity[] = "my-sender-id"; +const char kExampleMessage[] = "Hello, world, this is the GCM Driver!"; + +const char kValidEncryptionHeader[] = + "keyid=foo;salt=MTIzNDU2Nzg5MDEyMzQ1Ng;rs=1024"; +const char kInvalidEncryptionHeader[] = "keyid"; + +const char kValidCryptoKeyHeader[] = + "keyid=foo;dh=BL_UGhfudEkXMUd4U4-D4nP5KHxKjQHsW6j88ybbehXM7fqi1OMFefDUEi0eJ" + "vsKfyVBWYkQjH-lSPJKxjAyslg"; +const char kValidThreeValueCryptoKeyHeader[] = + "keyid=foo,keyid=bar,keyid=baz;dh=BL_UGhfudEkXMUd4U4-D4nP5KHxKjQHsW6j88ybbe" + "hXM7fqi1OMFefDUEi0eJvsKfyVBWYkQjH-lSPJKxjAyslg"; + +const char kInvalidCryptoKeyHeader[] = "keyid"; +const char kInvalidThreeValueCryptoKeyHeader[] = + "keyid=foo,dh=BL_UGhfudEkXMUd4U4-D4nP5KHxKjQHsW6j88ybbehXM7fqi1OMFefDUEi0eJ" + "vsKfyVBWYkQjH-lSPJKxjAyslg,keyid=baz,dh=BL_UGhfudEkXMUd4U4-D4nP5KHxKjQHsW6" + "j88ybbehXM7fqi1OMFefDUEi0eJvsKfyVBWYkQjH-lSPJKxjAyslg"; + +} // namespace + +using ECPrivateKeyUniquePtr = std::unique_ptr; + +class GCMEncryptionProviderTest : public ::testing::Test { + public: + void SetUp() override { + ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir()); + + encryption_provider_ = std::make_unique(); + encryption_provider_->Init(scoped_temp_dir_.GetPath(), + base::ThreadTaskRunnerHandle::Get()); + } + + void TearDown() override { + encryption_provider_.reset(); + + // |encryption_provider_| owns a ProtoDatabase whose destructor deletes + // the underlying LevelDB database on the task runner. + base::RunLoop().RunUntilIdle(); + } + + // To be used as a callback for GCMEncryptionProvider::GetEncryptionInfo(). + void DidGetEncryptionInfo(std::string* p256dh_out, + std::string* auth_secret_out, + std::string p256dh, + std::string auth_secret) { + *p256dh_out = std::move(p256dh); + *auth_secret_out = std::move(auth_secret); + } + + // To be used as a callback for GCMKeyStore::{GetKeys,CreateKeys}. + void HandleKeysCallback(ECPrivateKeyUniquePtr* key_out, + std::string* auth_secret_out, + ECPrivateKeyUniquePtr key, + const std::string& auth_secret) { + *key_out = std::move(key); + *auth_secret_out = auth_secret; + } + + protected: + // Decrypts the |message| and then synchronously waits until either the + // success or failure callbacks has been invoked. + void Decrypt(const IncomingMessage& message) { + encryption_provider_->DecryptMessage( + kExampleAppId, message, + base::BindOnce(&GCMEncryptionProviderTest::DidDecryptMessage, + base::Unretained(this))); + + // The encryption keys will be read asynchronously. + base::RunLoop().RunUntilIdle(); + } + + // Encrypts the |message| and then synchronously waits until either the + // success or failure callbacks has been invoked. + void Encrypt(const std::string& authorized_entity, + const std::string& p256dh, + const std::string& auth_secret, + const std::string& message) { + encryption_provider_->EncryptMessage( + kExampleAppId, authorized_entity, p256dh, auth_secret, message, + base::BindOnce(&GCMEncryptionProviderTest::DidEncryptMessage, + base::Unretained(this))); + + // The encryption keys will be read asynchronously. + base::RunLoop().RunUntilIdle(); + } + + // Checks that the underlying key store has a key for the |kExampleAppId| + + // authorized entity key if and only if |should_have_key| is true. Must wrap + // with ASSERT/EXPECT_NO_FATAL_FAILURE. + void CheckHasKey(const std::string& authorized_entity, bool should_have_key) { + ECPrivateKeyUniquePtr key; + std::string auth_secret; + encryption_provider()->key_store_->GetKeys( + kExampleAppId, authorized_entity, + false /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMEncryptionProviderTest::HandleKeysCallback, + base::Unretained(this), &key, &auth_secret)); + + base::RunLoop().RunUntilIdle(); + + if (should_have_key) { + ASSERT_TRUE(key); + std::string private_key, public_key; + ASSERT_TRUE(GetRawPrivateKey(*key, &private_key)); + ASSERT_TRUE(GetRawPublicKey(*key, &public_key)); + ASSERT_GT(public_key.size(), 0u); + ASSERT_GT(private_key.size(), 0u); + ASSERT_GT(auth_secret.size(), 0u); + } else { + ASSERT_FALSE(key); + ASSERT_EQ(0u, auth_secret.size()); + } + } + + // Returns the result of the previous decryption operation. + GCMDecryptionResult decryption_result() { return decryption_result_; } + + // Returns the result of the previous encryption operation. + GCMEncryptionResult encryption_result() { return encryption_result_; } + + // Returns the message resulting from the previous decryption operation. + const IncomingMessage& decrypted_message() { return decrypted_message_; } + + // Returns the message resulting from the previous encryption operation. + const std::string& encrypted_message() { return encrypted_message_; } + + GCMEncryptionProvider* encryption_provider() { + return encryption_provider_.get(); + } + + // Performs a full round-trip test of the encryption feature. Must wrap this + // in ASSERT_NO_FATAL_FAILURE. + void TestEncryptionRoundTrip(const std::string& app_id, + const std::string& authorized_entity, + GCMMessageCryptographer::Version version, + bool use_internal_raw_data_for_draft08 = false); + + // Performs a test encryption feature without creating proper keys. Must wrap + // this in ASSERT_NO_FATAL_FAILURE. + void TestEncryptionNoKeys(const std::string& app_id, + const std::string& authorized_entity); + + private: + void DidDecryptMessage(GCMDecryptionResult result, IncomingMessage message) { + decryption_result_ = result; + decrypted_message_ = std::move(message); + } + + void DidEncryptMessage(GCMEncryptionResult result, std::string message) { + encryption_result_ = result; + encrypted_message_ = std::move(message); + } + + base::test::SingleThreadTaskEnvironment task_environment_; + base::ScopedTempDir scoped_temp_dir_; + base::HistogramTester histogram_tester_; + + std::unique_ptr encryption_provider_; + + GCMDecryptionResult decryption_result_ = GCMDecryptionResult::UNENCRYPTED; + GCMEncryptionResult encryption_result_ = + GCMEncryptionResult::ENCRYPTION_FAILED; + + IncomingMessage decrypted_message_; + std::string encrypted_message_; +}; + +TEST_F(GCMEncryptionProviderTest, IsEncryptedMessage) { + // Both the Encryption and Encryption-Key headers must be present, and the raw + // data must be non-empty for a message to be considered encrypted. + + IncomingMessage empty_message; + EXPECT_FALSE(encryption_provider()->IsEncryptedMessage(empty_message)); + + IncomingMessage single_header_message; + single_header_message.data["encryption"] = ""; + EXPECT_FALSE( + encryption_provider()->IsEncryptedMessage(single_header_message)); + + IncomingMessage double_header_message; + double_header_message.data["encryption"] = ""; + double_header_message.data["crypto-key"] = ""; + EXPECT_FALSE( + encryption_provider()->IsEncryptedMessage(double_header_message)); + + IncomingMessage double_header_with_data_message; + double_header_with_data_message.data["encryption"] = ""; + double_header_with_data_message.data["crypto-key"] = ""; + double_header_with_data_message.raw_data = "foo"; + EXPECT_TRUE(encryption_provider()->IsEncryptedMessage( + double_header_with_data_message)); + + IncomingMessage draft08_message; + draft08_message.data["content-encoding"] = "aes128gcm"; + draft08_message.raw_data = "foo"; + EXPECT_TRUE(encryption_provider()->IsEncryptedMessage(draft08_message)); +} + +TEST_F(GCMEncryptionProviderTest, VerifiesEncryptionHeaderParsing) { + // The Encryption header must be parsable and contain valid values. + // Note that this is more extensively tested in EncryptionHeaderParsersTest. + + IncomingMessage invalid_message; + invalid_message.data["encryption"] = kInvalidEncryptionHeader; + invalid_message.data["crypto-key"] = kValidCryptoKeyHeader; + invalid_message.raw_data = "foo"; + + ASSERT_NO_FATAL_FAILURE(Decrypt(invalid_message)); + EXPECT_EQ(GCMDecryptionResult::INVALID_ENCRYPTION_HEADER, + decryption_result()); + + IncomingMessage valid_message; + valid_message.data["encryption"] = kValidEncryptionHeader; + valid_message.data["crypto-key"] = kInvalidCryptoKeyHeader; + valid_message.raw_data = "foo"; + + ASSERT_NO_FATAL_FAILURE(Decrypt(valid_message)); + EXPECT_NE(GCMDecryptionResult::INVALID_ENCRYPTION_HEADER, + decryption_result()); +} + +TEST_F(GCMEncryptionProviderTest, VerifiesCryptoKeyHeaderParsing) { + // The Crypto-Key header must be parsable and contain valid values. + // Note that this is more extensively tested in EncryptionHeaderParsersTest. + + IncomingMessage invalid_message; + invalid_message.data["encryption"] = kValidEncryptionHeader; + invalid_message.data["crypto-key"] = kInvalidCryptoKeyHeader; + invalid_message.raw_data = "foo"; + + ASSERT_NO_FATAL_FAILURE(Decrypt(invalid_message)); + EXPECT_EQ(GCMDecryptionResult::INVALID_CRYPTO_KEY_HEADER, + decryption_result()); + + IncomingMessage valid_message; + valid_message.data["encryption"] = kValidEncryptionHeader; + valid_message.data["crypto-key"] = kValidCryptoKeyHeader; + valid_message.raw_data = "foo"; + + ASSERT_NO_FATAL_FAILURE(Decrypt(valid_message)); + EXPECT_NE(GCMDecryptionResult::INVALID_CRYPTO_KEY_HEADER, + decryption_result()); +} + +TEST_F(GCMEncryptionProviderTest, VerifiesCryptoKeyHeaderParsingThirdValue) { + // The Crypto-Key header must be parsable and contain valid values, in which + // values will be ignored unless they contain a "dh" property. + + IncomingMessage valid_message; + valid_message.data["encryption"] = kValidEncryptionHeader; + valid_message.data["crypto-key"] = kValidThreeValueCryptoKeyHeader; + valid_message.raw_data = "foo"; + + ASSERT_NO_FATAL_FAILURE(Decrypt(valid_message)); + EXPECT_NE(GCMDecryptionResult::INVALID_CRYPTO_KEY_HEADER, + decryption_result()); +} + +TEST_F(GCMEncryptionProviderTest, VerifiesCryptoKeyHeaderSingleDhEntry) { + // The Crypto-Key header must include at most one value that contains the + // "dh" property. Having more than once occurrence is forbidden. + + IncomingMessage valid_message; + valid_message.data["encryption"] = kValidEncryptionHeader; + valid_message.data["crypto-key"] = kInvalidThreeValueCryptoKeyHeader; + valid_message.raw_data = "foo"; + + ASSERT_NO_FATAL_FAILURE(Decrypt(valid_message)); + EXPECT_EQ(GCMDecryptionResult::INVALID_CRYPTO_KEY_HEADER, + decryption_result()); +} + +TEST_F(GCMEncryptionProviderTest, VerifiesExistingKeys) { + // When both headers are valid, the encryption keys still must be known to + // the GCM key store before the message can be decrypted. + + IncomingMessage message; + message.data["encryption"] = kValidEncryptionHeader; + message.data["crypto-key"] = kValidCryptoKeyHeader; + message.raw_data = "foo"; + + ASSERT_NO_FATAL_FAILURE(Decrypt(message)); + EXPECT_EQ(GCMDecryptionResult::NO_KEYS, decryption_result()); + + std::string public_key, auth_secret; + encryption_provider()->GetEncryptionInfo( + kExampleAppId, "" /* empty authorized entity for non-InstanceID */, + base::BindOnce(&GCMEncryptionProviderTest::DidGetEncryptionInfo, + base::Unretained(this), &public_key, &auth_secret)); + + // Getting (or creating) the public key will be done asynchronously. + base::RunLoop().RunUntilIdle(); + + ASSERT_GT(public_key.size(), 0u); + ASSERT_GT(auth_secret.size(), 0u); + + ASSERT_NO_FATAL_FAILURE(Decrypt(message)); + EXPECT_NE(GCMDecryptionResult::NO_KEYS, decryption_result()); +} + +TEST_F(GCMEncryptionProviderTest, VerifiesKeyRemovalGCMRegistration) { + // Removing encryption info for an InstanceID token shouldn't affect a + // non-InstanceID GCM registration. + + // Non-InstanceID callers pass an empty string for authorized_entity. + std::string authorized_entity_gcm; + std::string authorized_entity_1 = kExampleAuthorizedEntity + std::string("1"); + std::string authorized_entity_2 = kExampleAuthorizedEntity + std::string("2"); + + // Should create encryption info. + std::string public_key, auth_secret; + encryption_provider()->GetEncryptionInfo( + kExampleAppId, authorized_entity_gcm, + base::BindOnce(&GCMEncryptionProviderTest::DidGetEncryptionInfo, + base::Unretained(this), &public_key, &auth_secret)); + + base::RunLoop().RunUntilIdle(); + + // Should get encryption info created above. + std::string read_public_key, read_auth_secret; + encryption_provider()->GetEncryptionInfo( + kExampleAppId, authorized_entity_gcm, + base::BindOnce(&GCMEncryptionProviderTest::DidGetEncryptionInfo, + base::Unretained(this), &read_public_key, + &read_auth_secret)); + + base::RunLoop().RunUntilIdle(); + + EXPECT_GT(public_key.size(), 0u); + EXPECT_GT(auth_secret.size(), 0u); + EXPECT_EQ(public_key, read_public_key); + EXPECT_EQ(auth_secret, read_auth_secret); + + ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_gcm, true)); + + encryption_provider()->RemoveEncryptionInfo( + kExampleAppId, authorized_entity_1, base::DoNothing()); + + base::RunLoop().RunUntilIdle(); + + ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_gcm, true)); + + encryption_provider()->RemoveEncryptionInfo(kExampleAppId, "*", + base::DoNothing()); + + base::RunLoop().RunUntilIdle(); + + ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_gcm, true)); + + encryption_provider()->RemoveEncryptionInfo( + kExampleAppId, authorized_entity_gcm, base::DoNothing()); + + base::RunLoop().RunUntilIdle(); + + ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_gcm, false)); +} + +TEST_F(GCMEncryptionProviderTest, VerifiesKeyRemovalInstanceIDToken) { + // Removing encryption info for a non-InstanceID GCM registration shouldn't + // affect an InstanceID token. + + // Non-InstanceID callers pass an empty string for authorized_entity. + std::string authorized_entity_gcm; + std::string authorized_entity_1 = kExampleAuthorizedEntity + std::string("1"); + std::string authorized_entity_2 = kExampleAuthorizedEntity + std::string("2"); + + std::string public_key_1, auth_secret_1; + encryption_provider()->GetEncryptionInfo( + kExampleAppId, authorized_entity_1, + base::BindOnce(&GCMEncryptionProviderTest::DidGetEncryptionInfo, + base::Unretained(this), &public_key_1, &auth_secret_1)); + + base::RunLoop().RunUntilIdle(); + + EXPECT_GT(public_key_1.size(), 0u); + EXPECT_GT(auth_secret_1.size(), 0u); + + ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_1, true)); + ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_2, false)); + + std::string public_key_2, auth_secret_2; + encryption_provider()->GetEncryptionInfo( + kExampleAppId, authorized_entity_2, + base::BindOnce(&GCMEncryptionProviderTest::DidGetEncryptionInfo, + base::Unretained(this), &public_key_2, &auth_secret_2)); + + base::RunLoop().RunUntilIdle(); + + EXPECT_GT(public_key_2.size(), 0u); + EXPECT_GT(auth_secret_2.size(), 0u); + EXPECT_NE(public_key_1, public_key_2); + EXPECT_NE(auth_secret_1, auth_secret_2); + + ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_1, true)); + ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_2, true)); + + std::string read_public_key_1, read_auth_secret_1; + encryption_provider()->GetEncryptionInfo( + kExampleAppId, authorized_entity_1, + base::BindOnce(&GCMEncryptionProviderTest::DidGetEncryptionInfo, + base::Unretained(this), &read_public_key_1, + &read_auth_secret_1)); + + base::RunLoop().RunUntilIdle(); + + // Should have returned existing info for authorized_entity_1. + EXPECT_EQ(public_key_1, read_public_key_1); + EXPECT_EQ(auth_secret_1, read_auth_secret_1); + + encryption_provider()->RemoveEncryptionInfo( + kExampleAppId, authorized_entity_gcm, base::DoNothing()); + + base::RunLoop().RunUntilIdle(); + + ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_1, true)); + ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_2, true)); + + encryption_provider()->RemoveEncryptionInfo( + kExampleAppId, authorized_entity_1, base::DoNothing()); + + base::RunLoop().RunUntilIdle(); + + ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_1, false)); + ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_2, true)); + + std::string public_key_1_refreshed, auth_secret_1_refreshed; + encryption_provider()->GetEncryptionInfo( + kExampleAppId, authorized_entity_1, + base::BindOnce(&GCMEncryptionProviderTest::DidGetEncryptionInfo, + base::Unretained(this), &public_key_1_refreshed, + &auth_secret_1_refreshed)); + + base::RunLoop().RunUntilIdle(); + + // Since the info was removed, GetEncryptionInfo should have created new info. + EXPECT_GT(public_key_1_refreshed.size(), 0u); + EXPECT_GT(auth_secret_1_refreshed.size(), 0u); + EXPECT_NE(public_key_1, public_key_1_refreshed); + EXPECT_NE(auth_secret_1, auth_secret_1_refreshed); + EXPECT_NE(public_key_2, public_key_1_refreshed); + EXPECT_NE(auth_secret_2, auth_secret_1_refreshed); + + ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_1, true)); + ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_2, true)); + + encryption_provider()->RemoveEncryptionInfo(kExampleAppId, "*", + base::DoNothing()); + + base::RunLoop().RunUntilIdle(); + + ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_1, false)); + ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_2, false)); +} + +void GCMEncryptionProviderTest::TestEncryptionRoundTrip( + const std::string& app_id, + const std::string& authorized_entity, + GCMMessageCryptographer::Version version, + bool use_internal_raw_data_for_draft08) { + // Performs a full round-trip of the encryption feature, including getting a + // public/private key-key and performing the cryptographic operations. This + // is more of an integration test than a unit test. + + ECPrivateKeyUniquePtr key, server_key; + std::string auth_secret, server_authentication; + + // Retrieve the public/private key-key immediately from the key store, given + // that the GCMEncryptionProvider will only share the public key with users. + // Also create a second key, which will act as the server's keys. + encryption_provider()->key_store_->CreateKeys( + app_id, authorized_entity, + base::BindOnce(&GCMEncryptionProviderTest::HandleKeysCallback, + base::Unretained(this), &key, &auth_secret)); + + encryption_provider()->key_store_->CreateKeys( + "server-" + app_id, authorized_entity, + base::BindOnce(&GCMEncryptionProviderTest::HandleKeysCallback, + base::Unretained(this), &server_key, + &server_authentication)); + + // Creating the public keys will be done asynchronously. + base::RunLoop().RunUntilIdle(); + + std::string public_key, server_public_key; + ASSERT_TRUE(GetRawPublicKey(*key, &public_key)); + ASSERT_TRUE(GetRawPublicKey(*server_key, &server_public_key)); + ASSERT_GT(public_key.size(), 0u); + ASSERT_GT(server_public_key.size(), 0u); + + std::string private_key, server_private_key; + ASSERT_TRUE(GetRawPublicKey(*key, &private_key)); + ASSERT_TRUE(GetRawPublicKey(*server_key, &server_private_key)); + ASSERT_GT(private_key.size(), 0u); + ASSERT_GT(server_private_key.size(), 0u); + + IncomingMessage message; + message.sender_id = authorized_entity; + + switch (version) { + case GCMMessageCryptographer::Version::DRAFT_03: { + std::string salt; + + // Creates a cryptographically secure salt of |salt_size| octets in size, + // and calculate the shared secret for the message. + crypto::RandBytes(base::WriteInto(&salt, 16 + 1), 16); + + std::string shared_secret; + ASSERT_TRUE( + ComputeSharedP256Secret(*key, server_public_key, &shared_secret)); + + size_t record_size; + + // Encrypts the |kExampleMessage| using the generated shared key and the + // random |salt|, storing the result in |record_size| and the message. + GCMMessageCryptographer cryptographer(version); + + std::string ciphertext; + ASSERT_TRUE(cryptographer.Encrypt( + public_key, server_public_key, shared_secret, auth_secret, salt, + kExampleMessage, &record_size, &ciphertext)); + + std::string encoded_salt, encoded_key; + + // Compile the incoming GCM message, including the required headers. + base::Base64UrlEncode(salt, base::Base64UrlEncodePolicy::INCLUDE_PADDING, + &encoded_salt); + base::Base64UrlEncode(server_public_key, + base::Base64UrlEncodePolicy::INCLUDE_PADDING, + &encoded_key); + + std::stringstream encryption_header; + encryption_header << "rs=" << base::NumberToString(record_size) << ";"; + encryption_header << "salt=" << encoded_salt; + + message.data["encryption"] = encryption_header.str(); + message.data["crypto-key"] = "dh=" + encoded_key; + message.raw_data.swap(ciphertext); + break; + } + case GCMMessageCryptographer::Version::DRAFT_08: { + ASSERT_NO_FATAL_FAILURE( + Encrypt(authorized_entity, public_key, auth_secret, kExampleMessage)); + ASSERT_EQ(GCMEncryptionResult::ENCRYPTED_DRAFT_08, encryption_result()); + + message.data["content-encoding"] = "aes128gcm"; + if (use_internal_raw_data_for_draft08) { + std::string raw_data_base64; + base::Base64Encode(encrypted_message(), &raw_data_base64); + message.data["_googRawData"] = raw_data_base64; + } else { + message.raw_data = encrypted_message(); + } + break; + } + } + + ASSERT_TRUE(encryption_provider()->IsEncryptedMessage(message)); + + // Decrypt the message, and expect everything to go wonderfully well. + ASSERT_NO_FATAL_FAILURE(Decrypt(message)); + ASSERT_EQ(version == GCMMessageCryptographer::Version::DRAFT_03 + ? GCMDecryptionResult::DECRYPTED_DRAFT_03 + : GCMDecryptionResult::DECRYPTED_DRAFT_08, + decryption_result()); + + EXPECT_TRUE(decrypted_message().decrypted); + EXPECT_EQ(kExampleMessage, decrypted_message().raw_data); +} + +void GCMEncryptionProviderTest::TestEncryptionNoKeys( + const std::string& app_id, + const std::string& authorized_entity) { + // Only create proper keys for receipeint without creating keys for sender. + ECPrivateKeyUniquePtr key; + std::string auth_secret; + encryption_provider()->key_store_->CreateKeys( + "receiver" + app_id, authorized_entity, + base::BindOnce(&GCMEncryptionProviderTest::HandleKeysCallback, + base::Unretained(this), &key, &auth_secret)); + + // Creating the public keys will be done asynchronously. + base::RunLoop().RunUntilIdle(); + + std::string public_key; + ASSERT_TRUE(GetRawPublicKey(*key, &public_key)); + ASSERT_GT(public_key.size(), 0u); + + ASSERT_NO_FATAL_FAILURE( + Encrypt(authorized_entity, public_key, auth_secret, kExampleMessage)); + EXPECT_EQ(GCMEncryptionResult::NO_KEYS, encryption_result()); +} + +TEST_F(GCMEncryptionProviderTest, EncryptionRoundTripGCMRegistration) { + // GCMEncryptionProvider::DecryptMessage should succeed when the message was + // sent to a non-InstanceID GCM registration (empty authorized_entity). + ASSERT_NO_FATAL_FAILURE(TestEncryptionRoundTrip( + kExampleAppId, "" /* empty authorized entity for non-InstanceID */, + GCMMessageCryptographer::Version::DRAFT_03)); +} + +TEST_F(GCMEncryptionProviderTest, EncryptionRoundTripInstanceIDToken) { + // GCMEncryptionProvider::DecryptMessage should succeed when the message was + // sent to an InstanceID token (non-empty authorized_entity). + ASSERT_NO_FATAL_FAILURE( + TestEncryptionRoundTrip(kExampleAppId, kExampleAuthorizedEntity, + GCMMessageCryptographer::Version::DRAFT_03)); +} + +TEST_F(GCMEncryptionProviderTest, EncryptionRoundTripDraft08) { + // GCMEncryptionProvider::DecryptMessage should succeed when the message was + // encrypted following raft-ietf-webpush-encryption-08. + ASSERT_NO_FATAL_FAILURE( + TestEncryptionRoundTrip(kExampleAppId, kExampleAuthorizedEntity, + GCMMessageCryptographer::Version::DRAFT_08)); +} + +TEST_F(GCMEncryptionProviderTest, EncryptionRoundTripDraft08InternalRawData) { + // GCMEncryptionProvider::DecryptMessage should succeed when the message was + // encrypted following raft-ietf-webpush-encryption-08 with raw_data base64 + // encoded in message data. + ASSERT_NO_FATAL_FAILURE( + TestEncryptionRoundTrip(kExampleAppId, kExampleAuthorizedEntity, + GCMMessageCryptographer::Version::DRAFT_08, + /*use_internal_raw_data_for_draft08=*/true)); +} + +TEST_F(GCMEncryptionProviderTest, EncryptionNoKeys) { + ASSERT_NO_FATAL_FAILURE( + TestEncryptionNoKeys(kExampleAppId, kExampleAuthorizedEntity)); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/crypto/gcm_encryption_result.h b/chromium/components/gcm_driver/crypto/gcm_encryption_result.h new file mode 100644 index 00000000000..f06e4e56f42 --- /dev/null +++ b/chromium/components/gcm_driver/crypto/gcm_encryption_result.h @@ -0,0 +1,33 @@ +// Copyright 2019 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 COMPONENTS_GCM_DRIVER_CRYPTO_GCM_ENCRYPTION_RESULT_H_ +#define COMPONENTS_GCM_DRIVER_CRYPTO_GCM_ENCRYPTION_RESULT_H_ + +namespace gcm { + +// Result of encrypting an outgoing message. The values of these reasons must +// not be changed as they are being recorded using UMA. When adding a value, +// please update GCMEncryptionResult in //tools/metrics/histograms/enums.xml. +enum class GCMEncryptionResult { + // The message had been successfully be encrypted. The encryption scheme used + // for the message was draft-ietf-webpush-encryption-08. + ENCRYPTED_DRAFT_08 = 0, + + // No public/private key-pair was associated with the app_id. + NO_KEYS = 1, + + // The shared secret cannot be derived from the keying material. + INVALID_SHARED_SECRET = 2, + + // The payload could not be encrypted as AES-128-GCM. + ENCRYPTION_FAILED = 3, + + // Should be one more than the otherwise highest value in this enumeration. + ENUM_SIZE = ENCRYPTION_FAILED + 1 +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_CRYPTO_GCM_ENCRYPTION_RESULT_H_ diff --git a/chromium/components/gcm_driver/crypto/gcm_key_store.cc b/chromium/components/gcm_driver/crypto/gcm_key_store.cc new file mode 100644 index 00000000000..5a3f572ea12 --- /dev/null +++ b/chromium/components/gcm_driver/crypto/gcm_key_store.cc @@ -0,0 +1,425 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/crypto/gcm_key_store.h" + +#include + +#include + +#include "base/bind.h" +#include "base/callback.h" +#include "base/callback_helpers.h" +#include "base/logging.h" +#include "base/metrics/histogram_macros.h" +#include "base/strings/string_util.h" +#include "base/task/sequenced_task_runner.h" +#include "components/gcm_driver/crypto/p256_key_util.h" +#include "components/leveldb_proto/public/proto_database_provider.h" +#include "components/leveldb_proto/public/shared_proto_database_client_list.h" +#include "crypto/random.h" +#include "third_party/leveldatabase/env_chromium.h" + +namespace gcm { + +namespace { + +using EntryVectorType = + leveldb_proto::ProtoDatabase::KeyEntryVector; + +// Number of cryptographically secure random bytes to generate as a key pair's +// authentication secret. Must be at least 16 bytes. +const size_t kAuthSecretBytes = 16; + +// Size cap for the leveldb log file before compression. +const size_t kDatabaseWriteBufferSizeBytes = 16 * 1024; + +std::string DatabaseKey(const std::string& app_id, + const std::string& authorized_entity) { + DCHECK_EQ(std::string::npos, app_id.find(',')); + DCHECK_EQ(std::string::npos, authorized_entity.find(',')); + DCHECK_NE("*", authorized_entity) << "Wildcards require special handling"; + return authorized_entity.empty() + ? app_id // No comma, for compatibility with existing keys. + : app_id + ',' + authorized_entity; +} + +leveldb_env::Options CreateLevelDbOptions() { + leveldb_env::Options options; + options.create_if_missing = true; + options.max_open_files = 0; // Use minimum. + options.write_buffer_size = kDatabaseWriteBufferSizeBytes; + return options; +} + +} // namespace + +enum class GCMKeyStore::State { + UNINITIALIZED, + INITIALIZING, + INITIALIZED, + FAILED +}; + +GCMKeyStore::GCMKeyStore( + const base::FilePath& key_store_path, + const scoped_refptr& blocking_task_runner) + : key_store_path_(key_store_path), + blocking_task_runner_(blocking_task_runner), + state_(State::UNINITIALIZED) { + DCHECK(blocking_task_runner); +} + +GCMKeyStore::~GCMKeyStore() {} + +void GCMKeyStore::GetKeys(const std::string& app_id, + const std::string& authorized_entity, + bool fallback_to_empty_authorized_entity, + KeysCallback callback) { + LazyInitialize( + base::BindOnce(&GCMKeyStore::GetKeysAfterInitialize, + weak_factory_.GetWeakPtr(), app_id, authorized_entity, + fallback_to_empty_authorized_entity, std::move(callback))); +} + +void GCMKeyStore::GetKeysAfterInitialize( + const std::string& app_id, + const std::string& authorized_entity, + bool fallback_to_empty_authorized_entity, + KeysCallback callback) { + DCHECK(state_ == State::INITIALIZED || state_ == State::FAILED); + bool success = false; + + if (state_ == State::INITIALIZED) { + auto outer_iter = key_data_.find(app_id); + if (outer_iter != key_data_.end()) { + const auto& inner_map = outer_iter->second; + auto inner_iter = inner_map.find(authorized_entity); + if (fallback_to_empty_authorized_entity && inner_iter == inner_map.end()) + inner_iter = inner_map.find(std::string()); + if (inner_iter != inner_map.end()) { + const auto& map_entry = inner_iter->second; + std::move(callback).Run(map_entry.first->Copy(), map_entry.second); + success = true; + } + } + } + + UMA_HISTOGRAM_BOOLEAN("GCM.Crypto.GetKeySuccessRate", success); + if (!success) + std::move(callback).Run(nullptr /* key */, std::string() /* auth_secret */); +} + +void GCMKeyStore::CreateKeys(const std::string& app_id, + const std::string& authorized_entity, + KeysCallback callback) { + LazyInitialize(base::BindOnce(&GCMKeyStore::CreateKeysAfterInitialize, + weak_factory_.GetWeakPtr(), app_id, + authorized_entity, std::move(callback))); +} + +void GCMKeyStore::CreateKeysAfterInitialize( + const std::string& app_id, + const std::string& authorized_entity, + KeysCallback callback) { + DCHECK(state_ == State::INITIALIZED || state_ == State::FAILED); + if (state_ != State::INITIALIZED) { + std::move(callback).Run(nullptr /* key */, std::string() /* auth_secret */); + return; + } + + // Only allow creating new keys if no keys currently exist. Multiple Instance + // ID tokens can share an app_id (with different authorized entities), but + // InstanceID tokens can't share an app_id with a non-InstanceID registration. + // This invariant is necessary for the fallback_to_empty_authorized_entity + // mode of GetKey (needed by GCMEncryptionProvider::DecryptMessage, which + // can't distinguish Instance ID tokens from non-InstanceID registrations). + DCHECK(!key_data_.count(app_id) || + (!authorized_entity.empty() && + !key_data_[app_id].count(authorized_entity) && + !key_data_[app_id].count(std::string()))) + << "Instance ID tokens cannot share an app_id with a non-InstanceID GCM " + "registration"; + + std::unique_ptr key(crypto::ECPrivateKey::Create()); + + if (!key) { + NOTREACHED() << "Unable to initialize a P-256 key pair."; + + std::move(callback).Run(nullptr /* key */, std::string() /* auth_secret */); + return; + } + + std::string auth_secret; + + // Create the authentication secret, which has to be a cryptographically + // secure random number of at least 128 bits (16 bytes). + crypto::RandBytes(base::WriteInto(&auth_secret, kAuthSecretBytes + 1), + kAuthSecretBytes); + + // Store the keys in a new EncryptionData object. + EncryptionData encryption_data; + encryption_data.set_app_id(app_id); + if (!authorized_entity.empty()) + encryption_data.set_authorized_entity(authorized_entity); + encryption_data.set_auth_secret(auth_secret); + + std::string private_key; + bool success = GetRawPrivateKey(*key, &private_key); + DCHECK(success); + encryption_data.set_private_key(private_key); + + // Write them immediately to our cache, so subsequent calls to + // {Get/Create/Remove}Keys can see them. + key_data_[app_id][authorized_entity] = {key->Copy(), auth_secret}; + + std::unique_ptr entries_to_save(new EntryVectorType()); + std::unique_ptr> keys_to_remove( + new std::vector()); + + entries_to_save->push_back( + std::make_pair(DatabaseKey(app_id, authorized_entity), encryption_data)); + + database_->UpdateEntries( + std::move(entries_to_save), std::move(keys_to_remove), + base::BindOnce(&GCMKeyStore::DidStoreKeys, weak_factory_.GetWeakPtr(), + std::move(key), auth_secret, std::move(callback))); +} + +void GCMKeyStore::DidStoreKeys(std::unique_ptr pair, + const std::string& auth_secret, + KeysCallback callback, + bool success) { + UMA_HISTOGRAM_BOOLEAN("GCM.Crypto.CreateKeySuccessRate", success); + + if (!success) { + DVLOG(1) << "Unable to store the created key in the GCM Key Store."; + + // Our cache is now inconsistent. Reject requests until restarted. + state_ = State::FAILED; + + std::move(callback).Run(nullptr /* key */, std::string() /* auth_secret */); + return; + } + + std::move(callback).Run(std::move(pair), auth_secret); +} + +void GCMKeyStore::RemoveKeys(const std::string& app_id, + const std::string& authorized_entity, + base::OnceClosure callback) { + LazyInitialize(base::BindOnce(&GCMKeyStore::RemoveKeysAfterInitialize, + weak_factory_.GetWeakPtr(), app_id, + authorized_entity, std::move(callback))); +} + +void GCMKeyStore::RemoveKeysAfterInitialize( + const std::string& app_id, + const std::string& authorized_entity, + base::OnceClosure callback) { + DCHECK(state_ == State::INITIALIZED || state_ == State::FAILED); + + const auto& outer_iter = key_data_.find(app_id); + if (outer_iter == key_data_.end() || state_ != State::INITIALIZED) { + std::move(callback).Run(); + return; + } + + std::unique_ptr entries_to_save(new EntryVectorType()); + std::unique_ptr> keys_to_remove( + new std::vector()); + + bool had_keys = false; + auto& inner_map = outer_iter->second; + for (auto it = inner_map.begin(); it != inner_map.end();) { + // Wildcard "*" matches all non-empty authorized entities (InstanceID only). + if (authorized_entity == "*" ? !it->first.empty() + : it->first == authorized_entity) { + had_keys = true; + + keys_to_remove->push_back(DatabaseKey(app_id, it->first)); + + // Clear keys immediately from our cache, so subsequent calls to + // {Get/Create/Remove}Keys don't see them. + it = inner_map.erase(it); + } else { + ++it; + } + } + if (!had_keys) { + std::move(callback).Run(); + return; + } + if (inner_map.empty()) + key_data_.erase(app_id); + + database_->UpdateEntries( + std::move(entries_to_save), std::move(keys_to_remove), + base::BindOnce(&GCMKeyStore::DidRemoveKeys, weak_factory_.GetWeakPtr(), + std::move(callback))); +} + +void GCMKeyStore::DidRemoveKeys(base::OnceClosure callback, bool success) { + UMA_HISTOGRAM_BOOLEAN("GCM.Crypto.RemoveKeySuccessRate", success); + + if (!success) { + DVLOG(1) << "Unable to delete a key from the GCM Key Store."; + + // Our cache is now inconsistent. Reject requests until restarted. + state_ = State::FAILED; + } + + std::move(callback).Run(); +} + +void GCMKeyStore::DidUpgradeDatabase(bool success) { + UMA_HISTOGRAM_BOOLEAN("GCM.Crypto.GCMDatabaseUpgradeResult", success); + if (!success) { + DVLOG(1) << "Unable to upgrade the GCM Key Store database."; + // Our cache is now inconsistent. Reject requests until restarted. + state_ = State::FAILED; + delayed_task_controller_.SetReady(); + return; + } + + database_->LoadEntries( + base::BindOnce(&GCMKeyStore::DidLoadKeys, weak_factory_.GetWeakPtr())); +} + +void GCMKeyStore::LazyInitialize(base::OnceClosure done_closure) { + if (delayed_task_controller_.CanRunTaskWithoutDelay()) { + std::move(done_closure).Run(); + return; + } + + delayed_task_controller_.AddTask(std::move(done_closure)); + if (state_ == State::INITIALIZING) + return; + + state_ = State::INITIALIZING; + + database_ = leveldb_proto::ProtoDatabaseProvider::GetUniqueDB( + leveldb_proto::ProtoDbType::GCM_KEY_STORE, key_store_path_, + blocking_task_runner_); + + database_->Init( + CreateLevelDbOptions(), + base::BindOnce(&GCMKeyStore::DidInitialize, weak_factory_.GetWeakPtr())); +} + +void GCMKeyStore::DidInitialize(leveldb_proto::Enums::InitStatus status) { + bool success = status == leveldb_proto::Enums::kOK; + UMA_HISTOGRAM_BOOLEAN("GCM.Crypto.InitKeyStoreSuccessRate", success); + if (!success) { + DVLOG(1) << "Unable to initialize the GCM Key Store."; + state_ = State::FAILED; + + delayed_task_controller_.SetReady(); + return; + } + + database_->LoadEntries( + base::BindOnce(&GCMKeyStore::DidLoadKeys, weak_factory_.GetWeakPtr())); +} + +void GCMKeyStore::UpgradeDatabase( + std::unique_ptr> entries) { + std::unique_ptr entries_to_save = + std::make_unique(); + std::unique_ptr> keys_to_remove = + std::make_unique>(); + + // Loop over entries, create list of database entries to overwrite. + for (EncryptionData& entry : *entries) { + if (!entry.keys_size()) + continue; + std::string decrypted_private_key; + if (!DecryptPrivateKey(entry.keys(0).private_key(), + &decrypted_private_key)) { + DVLOG(1) << "Unable to decrypt private key: " + << entry.keys(0).private_key(); + state_ = State::FAILED; + delayed_task_controller_.SetReady(); + UMA_HISTOGRAM_BOOLEAN("GCM.Crypto.GCMDatabaseUpgradeResult", + false /* sucess */); + return; + } + + entry.set_private_key(decrypted_private_key); + entry.clear_keys(); + entries_to_save->push_back(std::make_pair( + DatabaseKey(entry.app_id(), entry.authorized_entity()), entry)); + } + + database_->UpdateEntries(std::move(entries_to_save), + std::move(keys_to_remove), + base::BindOnce(&GCMKeyStore::DidUpgradeDatabase, + weak_factory_.GetWeakPtr())); +} + +void GCMKeyStore::DidLoadKeys( + bool success, + std::unique_ptr> entries) { + UMA_HISTOGRAM_BOOLEAN("GCM.Crypto.LoadKeyStoreSuccessRate", success); + if (!success) { + DVLOG(1) << "Unable to load entries into the GCM Key Store."; + state_ = State::FAILED; + + delayed_task_controller_.SetReady(); + return; + } + + for (const EncryptionData& entry : *entries) { + std::string authorized_entity; + if (entry.has_authorized_entity()) + authorized_entity = entry.authorized_entity(); + std::unique_ptr key; + + // The old format of EncryptionData has a KeyPair in it. Previously + // we used to cache the key pair and auth secret in key_data_. + // The new code adds the pair {ECPrivateKey, auth_secret} to + // key_data_ instead. + if (entry.keys_size()) { + if (state_ == State::FAILED) + return; + + // Old format of EncryptionData. Upgrade database so there are no such + // entries. We'll reload keys from the database once this is done. + UpgradeDatabase(std::move(entries)); + return; + } else { + std::string private_key_str = entry.private_key(); + if (private_key_str.empty()) + continue; + std::vector private_key(private_key_str.begin(), + private_key_str.end()); + key = crypto::ECPrivateKey::CreateFromPrivateKeyInfo(private_key); + } + + key_data_[entry.app_id()][authorized_entity] = + std::make_pair(std::move(key), entry.auth_secret()); + } + + state_ = State::INITIALIZED; + + delayed_task_controller_.SetReady(); +} + +bool GCMKeyStore::DecryptPrivateKey(const std::string& to_decrypt, + std::string* decrypted) { + DCHECK(decrypted); + std::vector to_decrypt_vector(to_decrypt.begin(), to_decrypt.end()); + std::unique_ptr key_to_decrypt = + crypto::ECPrivateKey::CreateFromEncryptedPrivateKeyInfo( + to_decrypt_vector); + if (!key_to_decrypt) + return false; + std::vector decrypted_vector; + if (!key_to_decrypt->ExportPrivateKey(&decrypted_vector)) + return false; + decrypted->assign(decrypted_vector.begin(), decrypted_vector.end()); + return true; +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/crypto/gcm_key_store.h b/chromium/components/gcm_driver/crypto/gcm_key_store.h new file mode 100644 index 00000000000..0c05923dc46 --- /dev/null +++ b/chromium/components/gcm_driver/crypto/gcm_key_store.h @@ -0,0 +1,150 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_GCM_DRIVER_CRYPTO_GCM_KEY_STORE_H_ +#define COMPONENTS_GCM_DRIVER_CRYPTO_GCM_KEY_STORE_H_ + +#include +#include +#include +#include +#include + +#include "base/callback_forward.h" +#include "base/files/file_path.h" +#include "base/memory/ref_counted.h" +#include "base/memory/weak_ptr.h" +#include "components/gcm_driver/crypto/proto/gcm_encryption_data.pb.h" +#include "components/gcm_driver/gcm_delayed_task_controller.h" +#include "components/leveldb_proto/public/proto_database.h" +#include "crypto/ec_private_key.h" + +namespace base { +class SequencedTaskRunner; +} + +namespace gcm { + +// Key storage for use with encrypted messages received from Google Cloud +// Messaging. It provides the ability to create and store a key-pair for a given +// app id + authorized entity pair, and to retrieve and delete key-pairs. +// +// This class is backed by a proto database and might end up doing file I/O on +// a background task runner. For this reason, all public APIs take a callback +// rather than returning the result. Do not rely on the timing of the callbacks. +class GCMKeyStore { + public: + using KeysCallback = + base::OnceCallback key, + const std::string& auth_secret)>; + + GCMKeyStore( + const base::FilePath& key_store_path, + const scoped_refptr& blocking_task_runner); + + GCMKeyStore(const GCMKeyStore&) = delete; + GCMKeyStore& operator=(const GCMKeyStore&) = delete; + + ~GCMKeyStore(); + + // Retrieves the public/private key-pair associated with the |app_id| + + // |authorized_entity| pair, and invokes |callback| when they are available, + // or when an error occurred. |authorized_entity| should be the InstanceID + // token's authorized entity, or "" for non-InstanceID GCM registrations. If + // |fallback_to_empty_authorized_entity| is true and the keys are not found, + // GetKeys will try again with an empty authorized entity; this can be used + // when it's not known whether or not the |app_id| is for an InstanceID. + void GetKeys(const std::string& app_id, + const std::string& authorized_entity, + bool fallback_to_empty_authorized_entity, + KeysCallback callback); + + // Creates a new public/private key-pair for the |app_id| + + // |authorized_entity| pair, and invokes |callback| when they are available, + // or when an error occurred. |authorized_entity| should be the InstanceID + // token's authorized entity, or "" for non-InstanceID GCM registrations. + // Simultaneously using the same |app_id| for both a non-InstanceID GCM + // registration and one or more InstanceID tokens is not supported. + void CreateKeys(const std::string& app_id, + const std::string& authorized_entity, + KeysCallback callback); + + // Removes the keys associated with the |app_id| + |authorized_entity| pair, + // and invokes |callback| when the operation has finished. |authorized_entity| + // should be the InstanceID token's authorized entity, or "*" to remove for + // all InstanceID tokens, or "" for non-InstanceID GCM registrations. + void RemoveKeys(const std::string& app_id, + const std::string& authorized_entity, + base::OnceClosure callback); + + private: + friend class GCMKeyStoreTest; + // Initializes the database if necessary, and runs |done_closure| when done. + void LazyInitialize(base::OnceClosure done_closure); + + // Upgrades the stored encryption keys from pairs including deprecated PKCS #8 + // EncryptedPrivateKeyInfo blocks, to storing a single PrivateKeyInfo block. + void UpgradeDatabase(std::unique_ptr> entries); + + void DidInitialize(leveldb_proto::Enums::InitStatus status); + void DidLoadKeys(bool success, + std::unique_ptr> entries); + void DidStoreKeys(std::unique_ptr key, + const std::string& auth_secret, + KeysCallback callback, + bool success); + void DidUpgradeDatabase(bool success); + + void DidRemoveKeys(base::OnceClosure callback, bool success); + + // Private implementations of the API that will be executed when the database + // has either been successfully loaded, or failed to load. + + void GetKeysAfterInitialize(const std::string& app_id, + const std::string& authorized_entity, + bool fallback_to_empty_authorized_entity, + KeysCallback callback); + void CreateKeysAfterInitialize(const std::string& app_id, + const std::string& authorized_entity, + KeysCallback callback); + void RemoveKeysAfterInitialize(const std::string& app_id, + const std::string& authorized_entity, + base::OnceClosure callback); + + // Converts private key from old deprecated format (where it is encrypted with + // and empty string) to the new format, where it's unencrypted. + bool DecryptPrivateKey(const std::string& to_decrypt, std::string* decrypted); + + // Path in which the key store database will be saved. + base::FilePath key_store_path_; + + // Blocking task runner which the database will do I/O operations on. + scoped_refptr blocking_task_runner_; + + // Instance of the ProtoDatabase backing the key store. + std::unique_ptr> database_; + + enum class State; + + // The current state of the database. It has to be initialized before use. + State state_; + + // Controller for tasks that should be executed once the key store has + // finished initializing. + GCMDelayedTaskController delayed_task_controller_; + + // Nested map from app_id to a map from authorized_entity to the loaded key + // pair and authentication secrets. + using KeyPairAndAuthSecret = + std::pair, std::string>; + std::unordered_map> + key_data_; + + base::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_CRYPTO_GCM_KEY_STORE_H_ diff --git a/chromium/components/gcm_driver/crypto/gcm_key_store_unittest.cc b/chromium/components/gcm_driver/crypto/gcm_key_store_unittest.cc new file mode 100644 index 00000000000..a2b98121982 --- /dev/null +++ b/chromium/components/gcm_driver/crypto/gcm_key_store_unittest.cc @@ -0,0 +1,775 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/crypto/gcm_key_store.h" + +#include +#include + +#include "base/base64url.h" +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/check_op.h" +#include "base/files/scoped_temp_dir.h" +#include "base/run_loop.h" +#include "base/strings/string_util.h" +#include "base/test/gtest_util.h" +#include "base/test/metrics/histogram_tester.h" +#include "base/test/task_environment.h" +#include "base/threading/thread_task_runner_handle.h" +#include "components/gcm_driver/crypto/p256_key_util.h" +#include "components/leveldb_proto/public/proto_database.h" +#include "crypto/random.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { + +namespace { + +using ECPrivateKeyUniquePtr = std::unique_ptr; +using EncryptDataVectorUniquePtr = std::unique_ptr>; +using EntryVectorType = + leveldb_proto::ProtoDatabase::KeyEntryVector; + +const char kFakeAppId[] = "my_app_id"; +const char kSecondFakeAppId[] = "my_other_app_id"; +const char kFakeAuthorizedEntity[] = "my_sender_id"; +const char kSecondFakeAuthorizedEntity[] = "my_other_sender_id"; +const char kPrivateEncrypted[] = + "MIGxMBwGCiqGSIb3DQEMAQMwDgQIh9aZ3UvuDloCAggABIGQZ-T8CJZe-no4mOTDgX1Gm986" + "Gsbe3mjJeABhA4KOmut_qJh5kt_DLqdNShiQr-afk3AdkX-fxLZdrcHiW9aWvBjnMAY65zg5" + "oHsuUaoEuG88Ksbku2u193OENWTQTsYaYE2O44qmRfsX773UNVcWXg_omwIbhbgf6tLZUZH_" + "dTC3YjzuxjbSP89HPEJ-eBXA"; +const char kPrivateDecrypted[] = + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgnCScek-QpEjmOOlT-rQ38nZz" + "vdPlqa00Zy0i6m2OJvahRANCAATaEQ22_OCRpvIOWeQhcbq0qrF1iddSLX1xFmFSxPOWOwmJ" + "A417CBHOGqsWGkNRvAapFwiegz6Q61rXVo_5roB1"; +const char kPublicKey[] = + "BNoRDbb84JGm8g5Z5CFxurSqsXWJ11ItfXEWYVLE85Y7CYkDjXsIEc4aqxYaQ1G8BqkXCJ6D" + "PpDrWtdWj_mugHU"; + +// Number of cryptographically secure random bytes to generate as a key pair's +// authentication secret. Must be at least 16 bytes. +const size_t kAuthSecretBytes = 16; + +} // namespace + +class GCMKeyStoreTest : public ::testing::Test { + public: + GCMKeyStoreTest() {} + ~GCMKeyStoreTest() override {} + + void SetUp() override { + ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir()); + CreateKeyStore(); + } + + void TearDown() override { + gcm_key_store_.reset(); + + // |gcm_key_store_| owns a ProtoDatabase whose destructor deletes the + // underlying LevelDB database on the task runner. + base::RunLoop().RunUntilIdle(); + } + + // Creates the GCM Key Store instance. May be called from within a test's body + // to re-create the key store, causing the database to re-open. + void CreateKeyStore() { + gcm_key_store_ = std::make_unique( + scoped_temp_dir_.GetPath(), + task_environment_.GetMainThreadTaskRunner()); + } + + // Callback to use with GCMKeyStore::{GetKeys, CreateKeys} calls. + void GotKeys(ECPrivateKeyUniquePtr* key_out, + std::string* auth_secret_out, + base::OnceClosure quit_closure, + ECPrivateKeyUniquePtr key, + const std::string& auth_secret) { + *key_out = std::move(key); + *auth_secret_out = auth_secret; + if (quit_closure) + std::move(quit_closure).Run(); + } + + void AddOldFormatEncryptionDataToKeyStoreDatabase( + const std::string& app_id, + const std::string& authorized_entity) { + EncryptionData encryption_data; + encryption_data.set_app_id(app_id); + encryption_data.set_authorized_entity(authorized_entity); + + // Create the authentication secret, which has to be a cryptographically + // secure random number of at least 128 bits (16 bytes). + std::string auth_secret; + crypto::RandBytes(base::WriteInto(&auth_secret, kAuthSecretBytes + 1), + kAuthSecretBytes); + encryption_data.set_auth_secret(auth_secret); + + // Add keys. + KeyPair* pair = encryption_data.add_keys(); + pair->set_type(KeyPair::ECDH_P256); + std::string private_key; + ASSERT_TRUE(base::Base64UrlDecode( + kPrivateEncrypted, base::Base64UrlDecodePolicy::IGNORE_PADDING, + &private_key)); + pair->set_private_key(private_key); + std::string public_key; + ASSERT_TRUE(base::Base64UrlDecode( + kPublicKey, base::Base64UrlDecodePolicy::IGNORE_PADDING, &public_key)); + pair->set_public_key(public_key); + + // Add this to database. + std::unique_ptr entries_to_save = + std::make_unique(); + std::unique_ptr> keys_to_remove = + std::make_unique>(); + entries_to_save->push_back(std::make_pair( + encryption_data.app_id() + ',' + encryption_data.authorized_entity(), + encryption_data)); + base::RunLoop run_loop; + gcm_key_store_->database_->UpdateEntries( + std::move(entries_to_save), std::move(keys_to_remove), + base::BindOnce(&GCMKeyStoreTest::UpdatedEntries, base::Unretained(this), + run_loop.QuitClosure())); + run_loop.Run(); + } + + protected: + GCMKeyStore* gcm_key_store() { return gcm_key_store_.get(); } + base::HistogramTester* histogram_tester() { return &histogram_tester_; } + + void UpdatedEntries(base::OnceClosure quit_closure, bool success) { + EXPECT_TRUE(success); + if (quit_closure) + std::move(quit_closure).Run(); + } + + private: + base::test::SingleThreadTaskEnvironment task_environment_; + base::ScopedTempDir scoped_temp_dir_; + base::HistogramTester histogram_tester_; + + std::unique_ptr gcm_key_store_; +}; + +TEST_F(GCMKeyStoreTest, EmptyByDefault) { + // The key store is initialized lazily, so this histogram confirms that + // calling the constructor does not in fact cause initialization. + histogram_tester()->ExpectTotalCount( + "GCM.Crypto.InitKeyStoreSuccessRate", 0); + + ECPrivateKeyUniquePtr key; + std::string auth_secret; + base::RunLoop run_loop; + gcm_key_store()->GetKeys( + kFakeAppId, kFakeAuthorizedEntity, + false /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key, + &auth_secret, run_loop.QuitClosure())); + + run_loop.Run(); + + ASSERT_FALSE(key); + EXPECT_EQ(0u, auth_secret.size()); + + histogram_tester()->ExpectBucketCount( + "GCM.Crypto.GetKeySuccessRate", 0, 1); // failure +} + +TEST_F(GCMKeyStoreTest, CreateAndGetKeys) { + ECPrivateKeyUniquePtr key; + std::string auth_secret; + base::RunLoop run_loop; + gcm_key_store()->CreateKeys( + kFakeAppId, kFakeAuthorizedEntity, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key, + &auth_secret, run_loop.QuitClosure())); + + run_loop.Run(); + + ASSERT_TRUE(key); + std::string public_key, private_key; + ASSERT_TRUE(GetRawPrivateKey(*key, &private_key)); + ASSERT_TRUE(GetRawPublicKey(*key, &public_key)); + + EXPECT_GT(public_key.size(), 0u); + EXPECT_GT(private_key.size(), 0u); + + ASSERT_GT(auth_secret.size(), 0u); + histogram_tester()->ExpectBucketCount( + "GCM.Crypto.CreateKeySuccessRate", 1, 1); // success + + ECPrivateKeyUniquePtr read_key; + std::string read_auth_secret; + base::RunLoop first_get_run_loop; + gcm_key_store()->GetKeys( + kFakeAppId, kFakeAuthorizedEntity, + false /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), + &read_key, &read_auth_secret, + first_get_run_loop.QuitClosure())); + + first_get_run_loop.Run(); + + ASSERT_TRUE(read_key); + std::string read_public_key, read_private_key; + ASSERT_TRUE(GetRawPrivateKey(*read_key, &read_private_key)); + ASSERT_TRUE(GetRawPublicKey(*read_key, &read_public_key)); + ASSERT_EQ(read_private_key, private_key); + ASSERT_EQ(read_public_key, public_key); + EXPECT_EQ(auth_secret, read_auth_secret); + + histogram_tester()->ExpectBucketCount("GCM.Crypto.GetKeySuccessRate", 1, + 1); // success + + // GetKey should also succeed if fallback_to_empty_authorized_entity is true + // (fallback should not occur, since an exact match is found). + base::RunLoop second_get_run_loop; + gcm_key_store()->GetKeys( + kFakeAppId, kFakeAuthorizedEntity, + true /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), + &read_key, &read_auth_secret, + second_get_run_loop.QuitClosure())); + + second_get_run_loop.Run(); + + ASSERT_TRUE(read_key); + + ASSERT_TRUE(GetRawPrivateKey(*read_key, &read_private_key)); + ASSERT_TRUE(GetRawPublicKey(*read_key, &read_public_key)); + ASSERT_EQ(read_private_key, private_key); + ASSERT_EQ(read_public_key, public_key); + EXPECT_EQ(auth_secret, read_auth_secret); + + histogram_tester()->ExpectBucketCount("GCM.Crypto.GetKeySuccessRate", 1, + 2); // another success +} + +TEST_F(GCMKeyStoreTest, GetKeysFallback) { + ECPrivateKeyUniquePtr key; + std::string auth_secret; + { + base::RunLoop run_loop; + gcm_key_store()->CreateKeys( + kFakeAppId, "" /* empty authorized entity for non-InstanceID */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key, + &auth_secret, run_loop.QuitClosure())); + + run_loop.Run(); + } + + ASSERT_TRUE(key); + + std::string public_key, private_key; + ASSERT_TRUE(GetRawPrivateKey(*key, &private_key)); + ASSERT_TRUE(GetRawPublicKey(*key, &public_key)); + + EXPECT_GT(public_key.size(), 0u); + EXPECT_GT(private_key.size(), 0u); + ASSERT_GT(auth_secret.size(), 0u); + + histogram_tester()->ExpectBucketCount("GCM.Crypto.CreateKeySuccessRate", 1, + 1); // success + + // GetKeys should fail when fallback_to_empty_authorized_entity is false, as + // there is not an exact match for kFakeAuthorizedEntity. + ECPrivateKeyUniquePtr read_key; + std::string read_auth_secret; + { + base::RunLoop run_loop; + gcm_key_store()->GetKeys( + kFakeAppId, kFakeAuthorizedEntity, + false /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), + &read_key, &read_auth_secret, run_loop.QuitClosure())); + + run_loop.Run(); + } + + ASSERT_FALSE(read_key); + EXPECT_EQ(0u, read_auth_secret.size()); + + histogram_tester()->ExpectBucketCount("GCM.Crypto.GetKeySuccessRate", 0, + 1); // failure + + // GetKey should succeed when fallback_to_empty_authorized_entity is true, as + // falling back to empty authorized entity will match the created key. + { + base::RunLoop run_loop; + gcm_key_store()->GetKeys( + kFakeAppId, kFakeAuthorizedEntity, + true /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), + &read_key, &read_auth_secret, run_loop.QuitClosure())); + + run_loop.Run(); + } + + ASSERT_TRUE(read_key); + + std::string read_public_key, read_private_key; + ASSERT_TRUE(GetRawPrivateKey(*key, &read_private_key)); + ASSERT_TRUE(GetRawPublicKey(*key, &read_public_key)); + EXPECT_EQ(private_key, read_private_key); + EXPECT_EQ(public_key, read_public_key); + + EXPECT_EQ(auth_secret, read_auth_secret); + + histogram_tester()->ExpectBucketCount("GCM.Crypto.GetKeySuccessRate", 1, + 1); // success +} + +TEST_F(GCMKeyStoreTest, KeysPersistenceBetweenInstances) { + ECPrivateKeyUniquePtr key; + std::string auth_secret; + { + base::RunLoop run_loop; + gcm_key_store()->CreateKeys( + kFakeAppId, kFakeAuthorizedEntity, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key, + &auth_secret, run_loop.QuitClosure())); + + run_loop.Run(); + } + + ASSERT_TRUE(key); + + histogram_tester()->ExpectBucketCount( + "GCM.Crypto.InitKeyStoreSuccessRate", 1, 1); // success + histogram_tester()->ExpectBucketCount( + "GCM.Crypto.LoadKeyStoreSuccessRate", 1, 1); // success + + // Create a new GCM Key Store instance. + CreateKeyStore(); + + ECPrivateKeyUniquePtr read_key; + std::string read_auth_secret; + { + base::RunLoop run_loop; + gcm_key_store()->GetKeys( + kFakeAppId, kFakeAuthorizedEntity, + false /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), + &read_key, &read_auth_secret, run_loop.QuitClosure())); + + run_loop.Run(); + } + + ASSERT_TRUE(read_key); + EXPECT_GT(read_auth_secret.size(), 0u); + + histogram_tester()->ExpectBucketCount( + "GCM.Crypto.InitKeyStoreSuccessRate", 1, 2); // success + histogram_tester()->ExpectBucketCount( + "GCM.Crypto.LoadKeyStoreSuccessRate", 1, 2); // success +} + +TEST_F(GCMKeyStoreTest, CreateAndRemoveKeys) { + ECPrivateKeyUniquePtr key; + std::string auth_secret; + { + base::RunLoop run_loop; + gcm_key_store()->CreateKeys( + kFakeAppId, kFakeAuthorizedEntity, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key, + &auth_secret, run_loop.QuitClosure())); + + run_loop.Run(); + } + + ASSERT_TRUE(key); + + ECPrivateKeyUniquePtr read_key; + std::string read_auth_secret; + { + base::RunLoop run_loop; + gcm_key_store()->GetKeys( + kFakeAppId, kFakeAuthorizedEntity, + false /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), + &read_key, &read_auth_secret, run_loop.QuitClosure())); + + run_loop.Run(); + } + + ASSERT_TRUE(read_key); + + gcm_key_store()->RemoveKeys(kFakeAppId, kFakeAuthorizedEntity, + base::DoNothing()); + + base::RunLoop().RunUntilIdle(); + + histogram_tester()->ExpectBucketCount( + "GCM.Crypto.RemoveKeySuccessRate", 1, 1); // success + + { + base::RunLoop run_loop; + gcm_key_store()->GetKeys( + kFakeAppId, kFakeAuthorizedEntity, + false /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), + &read_key, &read_auth_secret, run_loop.QuitClosure())); + + run_loop.Run(); + } + + ASSERT_FALSE(read_key); +} + +TEST_F(GCMKeyStoreTest, CreateGetAndRemoveKeysSynchronously) { + ECPrivateKeyUniquePtr key; + std::string auth_secret; + gcm_key_store()->CreateKeys( + kFakeAppId, kFakeAuthorizedEntity, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key, + &auth_secret, base::OnceClosure())); + + // Continue synchronously, without running RunUntilIdle first. + ECPrivateKeyUniquePtr key_after_create; + std::string auth_secret_after_create; + gcm_key_store()->GetKeys( + kFakeAppId, kFakeAuthorizedEntity, + false /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), + &key_after_create, &auth_secret_after_create, + base::OnceClosure())); + + // Continue synchronously, without running RunUntilIdle first. + gcm_key_store()->RemoveKeys(kFakeAppId, kFakeAuthorizedEntity, + base::DoNothing()); + + // Continue synchronously, without running RunUntilIdle first. + ECPrivateKeyUniquePtr key_after_remove; + std::string auth_secret_after_remove; + gcm_key_store()->GetKeys( + kFakeAppId, kFakeAuthorizedEntity, + false /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), + &key_after_remove, &auth_secret_after_remove, + base::OnceClosure())); + + base::RunLoop().RunUntilIdle(); + + histogram_tester()->ExpectBucketCount("GCM.Crypto.RemoveKeySuccessRate", 1, + 1); // success + + ECPrivateKeyUniquePtr key_after_idle; + std::string auth_secret_after_idle; + gcm_key_store()->GetKeys( + kFakeAppId, kFakeAuthorizedEntity, + false /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), + &key_after_idle, &auth_secret_after_idle, + base::OnceClosure())); + + base::RunLoop().RunUntilIdle(); + + ASSERT_TRUE(key); + ASSERT_TRUE(key_after_create); + EXPECT_FALSE(key_after_remove); + EXPECT_FALSE(key_after_idle); + + std::string public_key, public_key_after_create; + ASSERT_TRUE(GetRawPublicKey(*key, &public_key)); + ASSERT_TRUE(GetRawPublicKey(*key, &public_key_after_create)); + EXPECT_EQ(public_key, public_key_after_create); + + EXPECT_GT(auth_secret.size(), 0u); + EXPECT_EQ(auth_secret, auth_secret_after_create); + EXPECT_EQ("", auth_secret_after_remove); + EXPECT_EQ("", auth_secret_after_idle); +} + +TEST_F(GCMKeyStoreTest, RemoveKeysWildcardAuthorizedEntity) { + ECPrivateKeyUniquePtr key1, key2, key3; + std::string auth_secret1, auth_secret2, auth_secret3; + gcm_key_store()->CreateKeys( + kFakeAppId, kFakeAuthorizedEntity, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key1, + &auth_secret1, base::OnceClosure())); + gcm_key_store()->CreateKeys( + kFakeAppId, kSecondFakeAuthorizedEntity, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key2, + &auth_secret2, base::OnceClosure())); + gcm_key_store()->CreateKeys( + kSecondFakeAppId, kFakeAuthorizedEntity, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key3, + &auth_secret3, base::OnceClosure())); + + base::RunLoop().RunUntilIdle(); + + ASSERT_TRUE(key1); + ASSERT_TRUE(key2); + ASSERT_TRUE(key3); + + ECPrivateKeyUniquePtr read_key1, read_key2, read_key3; + std::string read_auth_secret1, read_auth_secret2, read_auth_secret3; + gcm_key_store()->GetKeys( + kFakeAppId, kFakeAuthorizedEntity, + false /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), + &read_key1, &read_auth_secret1, base::OnceClosure())); + gcm_key_store()->GetKeys( + kFakeAppId, kSecondFakeAuthorizedEntity, + false /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), + &read_key2, &read_auth_secret2, base::OnceClosure())); + gcm_key_store()->GetKeys( + kSecondFakeAppId, kFakeAuthorizedEntity, + false /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), + &read_key3, &read_auth_secret3, base::OnceClosure())); + + base::RunLoop().RunUntilIdle(); + + ASSERT_TRUE(read_key1); + ASSERT_TRUE(read_key2); + ASSERT_TRUE(read_key3); + + gcm_key_store()->RemoveKeys(kFakeAppId, "*" /* authorized_entity */, + base::DoNothing()); + + base::RunLoop().RunUntilIdle(); + + histogram_tester()->ExpectBucketCount("GCM.Crypto.RemoveKeySuccessRate", 1, + 1); // success + + gcm_key_store()->GetKeys( + kFakeAppId, kFakeAuthorizedEntity, + false /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), + &read_key1, &read_auth_secret1, base::OnceClosure())); + gcm_key_store()->GetKeys( + kFakeAppId, kSecondFakeAuthorizedEntity, + false /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), + &read_key2, &read_auth_secret2, base::OnceClosure())); + gcm_key_store()->GetKeys( + kSecondFakeAppId, kFakeAuthorizedEntity, + false /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), + &read_key3, &read_auth_secret3, base::OnceClosure())); + + base::RunLoop().RunUntilIdle(); + + EXPECT_FALSE(read_key1); + EXPECT_FALSE(read_key2); + ASSERT_TRUE(read_key3); +} + +TEST_F(GCMKeyStoreTest, GetKeysMultipleAppIds) { + ECPrivateKeyUniquePtr key; + std::string auth_secret; + { + base::RunLoop run_loop; + gcm_key_store()->CreateKeys( + kFakeAppId, kFakeAuthorizedEntity, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key, + &auth_secret, run_loop.QuitClosure())); + + run_loop.Run(); + } + + ASSERT_TRUE(key); + + { + base::RunLoop run_loop; + gcm_key_store()->CreateKeys( + kSecondFakeAppId, kSecondFakeAuthorizedEntity, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key, + &auth_secret, run_loop.QuitClosure())); + + run_loop.Run(); + } + + ASSERT_TRUE(key); + + ECPrivateKeyUniquePtr read_key; + std::string read_auth_secret; + { + base::RunLoop run_loop; + gcm_key_store()->GetKeys( + kFakeAppId, kFakeAuthorizedEntity, + false /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), + &read_key, &read_auth_secret, run_loop.QuitClosure())); + + run_loop.Run(); + } + + ASSERT_TRUE(read_key); +} + +TEST_F(GCMKeyStoreTest, SuccessiveCallsBeforeInitialization) { + ECPrivateKeyUniquePtr key; + std::string auth_secret; + gcm_key_store()->CreateKeys( + kFakeAppId, kFakeAuthorizedEntity, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key, + &auth_secret, base::OnceClosure())); + + // Deliberately do not run the message loop, so that the callback has not + // been resolved yet. The following EXPECT() ensures this. + EXPECT_FALSE(key); + + ECPrivateKeyUniquePtr read_key; + std::string read_auth_secret; + gcm_key_store()->GetKeys( + kFakeAppId, kFakeAuthorizedEntity, + false /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), + &read_key, &read_auth_secret, base::OnceClosure())); + + EXPECT_FALSE(read_key); + + // Now run the message loop. Both tasks should have finished executing. Due + // to the asynchronous nature of operations, however, we can't rely on the + // write to have finished before the read begins. + base::RunLoop().RunUntilIdle(); + + EXPECT_TRUE(key); +} + +TEST_F(GCMKeyStoreTest, CannotShareAppIdFromGCMToInstanceID) { + ECPrivateKeyUniquePtr key_unused; + std::string auth_secret_unused; + { + base::RunLoop run_loop; + gcm_key_store()->CreateKeys( + kFakeAppId, "" /* empty authorized entity for non-InstanceID */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), + &key_unused, &auth_secret_unused, + run_loop.QuitClosure())); + + run_loop.Run(); + } + + EXPECT_DCHECK_DEATH({ + base::RunLoop run_loop; + gcm_key_store()->CreateKeys( + kFakeAppId, kFakeAuthorizedEntity, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), + &key_unused, &auth_secret_unused, + run_loop.QuitClosure())); + + run_loop.Run(); + }); +} + +TEST_F(GCMKeyStoreTest, CannotShareAppIdFromInstanceIDToGCM) { + ECPrivateKeyUniquePtr key_unused; + std::string auth_secret_unused; + { + base::RunLoop run_loop; + gcm_key_store()->CreateKeys( + kFakeAppId, kFakeAuthorizedEntity, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), + &key_unused, &auth_secret_unused, + run_loop.QuitClosure())); + + run_loop.Run(); + } + + { + base::RunLoop run_loop; + gcm_key_store()->CreateKeys( + kFakeAppId, kSecondFakeAuthorizedEntity, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), + &key_unused, &auth_secret_unused, + run_loop.QuitClosure())); + + run_loop.Run(); + } + + EXPECT_DCHECK_DEATH({ + base::RunLoop run_loop; + gcm_key_store()->CreateKeys( + kFakeAppId, "" /* empty authorized entity for non-InstanceID */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), + &key_unused, &auth_secret_unused, + run_loop.QuitClosure())); + + run_loop.Run(); + }); +} + +TEST_F(GCMKeyStoreTest, TestUpgradePathForKeyStorageDeprecation) { + // Expect Upgrade count to be 0. + histogram_tester()->ExpectTotalCount("GCM.Crypto.GCMDatabaseUpgradeResult", + 0); + // Initialize GCM store and the underlying levelDB database by trying + // to fetch keys. + ECPrivateKeyUniquePtr key; + std::string auth_secret; + { + base::RunLoop run_loop; + gcm_key_store()->GetKeys( + kFakeAppId, kFakeAuthorizedEntity, + false /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key, + &auth_secret, run_loop.QuitClosure())); + + run_loop.Run(); + } + ASSERT_FALSE(key); + histogram_tester()->ExpectTotalCount("GCM.Crypto.GCMDatabaseUpgradeResult", + 0); + + // Add old format Encryption Data. + ASSERT_NO_FATAL_FAILURE(AddOldFormatEncryptionDataToKeyStoreDatabase( + kFakeAppId, kFakeAuthorizedEntity)); + + // Create a new GCM Key Store instance, so we can initialize again. + CreateKeyStore(); + + // GetKeys again, verify private key is decrypted and we have upgraded + // database exactly once + { + base::RunLoop run_loop; + gcm_key_store()->GetKeys( + kFakeAppId, kFakeAuthorizedEntity, + false /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key, + &auth_secret, run_loop.QuitClosure())); + run_loop.Run(); + } + + histogram_tester()->ExpectBucketCount("GCM.Crypto.GCMDatabaseUpgradeResult", + 1, 1); + ASSERT_TRUE(key); + ASSERT_GT(auth_secret.size(), 0u); + + // Verify also that the private key is decrypted. + std::string read_private_key; + ASSERT_TRUE(GetRawPrivateKey(*key, &read_private_key)); + std::string decrypted_private_key; + ASSERT_TRUE(base::Base64UrlDecode(kPrivateDecrypted, + base::Base64UrlDecodePolicy::IGNORE_PADDING, + &decrypted_private_key)); + ASSERT_EQ(decrypted_private_key, read_private_key); + + // AddOldFormatEncryptionDataToKeyStoreDatabase() again, different keys + ASSERT_NO_FATAL_FAILURE(AddOldFormatEncryptionDataToKeyStoreDatabase( + kSecondFakeAppId, kSecondFakeAuthorizedEntity)); + + // GetKeys on this one, should return nullptr + { + base::RunLoop run_loop; + gcm_key_store()->GetKeys( + kSecondFakeAppId, kSecondFakeAuthorizedEntity, + false /* fallback_to_empty_authorized_entity */, + base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key, + &auth_secret, run_loop.QuitClosure())); + run_loop.Run(); + } + ASSERT_FALSE(key); + ASSERT_EQ(auth_secret.size(), 0u); + // GCMDatabaseUpgradeResult should not have increased. + histogram_tester()->ExpectBucketCount("GCM.Crypto.GCMDatabaseUpgradeResult", + 1, 1); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/crypto/gcm_message_cryptographer.cc b/chromium/components/gcm_driver/crypto/gcm_message_cryptographer.cc new file mode 100644 index 00000000000..73256da1a8d --- /dev/null +++ b/chromium/components/gcm_driver/crypto/gcm_message_cryptographer.cc @@ -0,0 +1,477 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/crypto/gcm_message_cryptographer.h" + +#include +#include + +#include +#include + +#include "base/logging.h" +#include "base/notreached.h" +#include "base/numerics/ostream_operators.h" +#include "base/numerics/safe_math.h" +#include "base/strings/strcat.h" +#include "base/strings/string_util.h" +#include "base/sys_byteorder.h" +#include "crypto/hkdf.h" +#include "third_party/boringssl/src/include/openssl/aead.h" + +namespace gcm { + +namespace { + +// Size, in bytes, of the nonce for a record. This must be at least the size +// of a uint64_t, which is used to indicate the record sequence number. +const uint64_t kNonceSize = 12; + +// The default record size as defined by httpbis-encryption-encoding-06. +const size_t kDefaultRecordSize = 4096; + +// Key size, in bytes, of a valid AEAD_AES_128_GCM key. +const size_t kContentEncryptionKeySize = 16; + +// The BoringSSL functions used to seal (encrypt) and open (decrypt) a payload +// follow the same prototype, declared as follows. +using EVP_AEAD_CTX_TransformFunction = + int(const EVP_AEAD_CTX *ctx, uint8_t *out, size_t *out_len, + size_t max_out_len, const uint8_t *nonce, size_t nonce_len, + const uint8_t *in, size_t in_len, const uint8_t *ad, size_t ad_len); + +// Implementation of draft 03 of the Web Push Encryption standard: +// https://tools.ietf.org/html/draft-ietf-webpush-encryption-03 +// https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-02 +class WebPushEncryptionDraft03 + : public GCMMessageCryptographer::EncryptionScheme { + public: + WebPushEncryptionDraft03() = default; + + WebPushEncryptionDraft03(const WebPushEncryptionDraft03&) = delete; + WebPushEncryptionDraft03& operator=(const WebPushEncryptionDraft03&) = delete; + + ~WebPushEncryptionDraft03() override = default; + + // GCMMessageCryptographer::EncryptionScheme implementation. + std::string DerivePseudoRandomKey( + const base::StringPiece& /* recipient_public_key */, + const base::StringPiece& /* sender_public_key */, + const base::StringPiece& ecdh_shared_secret, + const base::StringPiece& auth_secret) override { + const char kInfo[] = "Content-Encoding: auth"; + + // This deliberately copies over the NUL terminus. + base::StringPiece info(kInfo, sizeof(kInfo)); + + return crypto::HkdfSha256(ecdh_shared_secret, auth_secret, info, 32); + } + + // Creates the info parameter for an HKDF value for the given + // |content_encoding| in accordance with draft-ietf-webpush-encryption-03. + // + // cek_info = "Content-Encoding: aesgcm" || 0x00 || context + // nonce_info = "Content-Encoding: nonce" || 0x00 || context + // + // context = "P-256" || 0x00 || + // length(recipient_public) || recipient_public || + // length(sender_public) || sender_public + // + // The length of the public keys must be written as a two octet unsigned + // integer in network byte order (big endian). + std::string GenerateInfoForContentEncoding( + EncodingType type, + const base::StringPiece& recipient_public_key, + const base::StringPiece& sender_public_key) override { + std::stringstream info_stream; + info_stream << "Content-Encoding: "; + + switch (type) { + case EncodingType::CONTENT_ENCRYPTION_KEY: + info_stream << "aesgcm"; + break; + case EncodingType::NONCE: + info_stream << "nonce"; + break; + } + + info_stream << '\x00' << "P-256" << '\x00'; + + uint16_t local_len = + base::HostToNet16(static_cast(recipient_public_key.size())); + info_stream.write(reinterpret_cast(&local_len), sizeof(local_len)); + info_stream << recipient_public_key; + + uint16_t peer_len = + base::HostToNet16(static_cast(sender_public_key.size())); + info_stream.write(reinterpret_cast(&peer_len), sizeof(peer_len)); + info_stream << sender_public_key; + + return info_stream.str(); + } + + // draft-ietf-webpush-encryption-03 defines that the padding is included at + // the beginning of the message. The first two bytes, in network byte order, + // contain the length of the included padding. Then that exact number of bytes + // must follow as padding, all of which must have a zero value. + // + // TODO(peter): Add support for message padding if the GCMMessageCryptographer + // starts encrypting payloads for reasons other than testing. + std::string CreateRecord(const base::StringPiece& plaintext) override { + std::string record; + record.reserve(sizeof(uint16_t) + plaintext.size()); + record.append(sizeof(uint16_t), '\x00'); + record.append(plaintext.data(), plaintext.size()); + return record; + } + + // The |ciphertext| must be at least of size kAuthenticationTagBytes with two + // padding bytes, which is the case for an empty message with zero padding. + // The |record_size| must be large enough to use only one record. + // https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-03#section-2 + bool ValidateCiphertextSize(size_t ciphertext_size, + size_t record_size) override { + return ciphertext_size >= + sizeof(uint16_t) + + GCMMessageCryptographer::kAuthenticationTagBytes && + ciphertext_size <= + record_size + GCMMessageCryptographer::kAuthenticationTagBytes; + } + + // The record padding in draft-ietf-webpush-encryption-03 is included at the + // beginning of the record. The first two bytes indicate the length of the + // padding. All padding bytes immediately follow, and must be set to zero. + bool ValidateAndRemovePadding(base::StringPiece& record) override { + // Records must be at least two octets in size (to hold the padding). + // Records that are smaller, i.e. a single octet, are invalid. + if (record.size() < sizeof(uint16_t)) + return false; + + // Records contain a two-byte, big-endian padding length followed by zero to + // 65535 bytes of padding. Padding bytes must be zero but, since AES-GCM + // authenticates the plaintext, checking and removing padding need not be + // done in constant-time. + uint16_t padding_length = (static_cast(record[0]) << 8) | + static_cast(record[1]); + record.remove_prefix(sizeof(uint16_t)); + + if (padding_length > record.size()) { + return false; + } + + for (size_t i = 0; i < padding_length; ++i) { + if (record[i] != 0) + return false; + } + + record.remove_prefix(padding_length); + return true; + } +}; + +// Implementation of draft 08 of the Web Push Encryption standard: +// https://tools.ietf.org/html/draft-ietf-webpush-encryption-08 +// https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-07 +class WebPushEncryptionDraft08 + : public GCMMessageCryptographer::EncryptionScheme { + public: + WebPushEncryptionDraft08() = default; + + WebPushEncryptionDraft08(const WebPushEncryptionDraft08&) = delete; + WebPushEncryptionDraft08& operator=(const WebPushEncryptionDraft08&) = delete; + + ~WebPushEncryptionDraft08() override = default; + + // GCMMessageCryptographer::EncryptionScheme implementation. + std::string DerivePseudoRandomKey( + const base::StringPiece& recipient_public_key, + const base::StringPiece& sender_public_key, + const base::StringPiece& ecdh_shared_secret, + const base::StringPiece& auth_secret) override { + DCHECK_EQ(recipient_public_key.size(), 65u); + DCHECK_EQ(sender_public_key.size(), 65u); + + const char kInfo[] = "WebPush: info"; + + // This deliberately copies over the NUL terminus. + std::string info = base::StrCat({base::StringPiece(kInfo, sizeof(kInfo)), + recipient_public_key, sender_public_key}); + + return crypto::HkdfSha256(ecdh_shared_secret, auth_secret, info, 32); + } + + // The info string used for generating the content encryption key and the + // nonce was simplified in draft-ietf-webpush-encryption-08, because the + // public keys of both the recipient and the sender are now in the PRK. + std::string GenerateInfoForContentEncoding( + EncodingType type, + const base::StringPiece& /* recipient_public_key */, + const base::StringPiece& /* sender_public_key */) override { + std::stringstream info_stream; + info_stream << "Content-Encoding: "; + + switch (type) { + case EncodingType::CONTENT_ENCRYPTION_KEY: + info_stream << "aes128gcm"; + break; + case EncodingType::NONCE: + info_stream << "nonce"; + break; + } + + info_stream << '\x00'; + return info_stream.str(); + } + + // draft-ietf-webpush-encryption-08 defines that the padding follows the + // plaintext of a message. A delimiter byte (0x02 for the final record) will + // be added, and then zero or more bytes of padding. + // + // TODO(peter): Add support for message padding if the GCMMessageCryptographer + // starts encrypting payloads for reasons other than testing. + std::string CreateRecord(const base::StringPiece& plaintext) override { + std::string record; + record.reserve(plaintext.size() + sizeof(uint8_t)); + record.append(plaintext.data(), plaintext.size()); + record.append(sizeof(uint8_t), '\x02'); + return record; + } + + // The |ciphertext| must be at least of size kAuthenticationTagBytes with one + // padding delimiter, which is the case for an empty message with minimal + // padding. The |record_size| must be large enough to use only one record. + // https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-08#section-2 + bool ValidateCiphertextSize(size_t ciphertext_size, + size_t record_size) override { + return ciphertext_size >= + sizeof(uint8_t) + + GCMMessageCryptographer::kAuthenticationTagBytes && + ciphertext_size <= + record_size + GCMMessageCryptographer::kAuthenticationTagBytes; + } + + // The record padding in draft-ietf-webpush-encryption-08 is included at the + // end of the record. The length is not defined, but all padding bytes must be + // zero until the delimiter (0x02) is found. + bool ValidateAndRemovePadding(base::StringPiece& record) override { + DCHECK_GE(record.size(), 1u); + + size_t padding_length = 1; + for (; padding_length <= record.size(); ++padding_length) { + size_t offset = record.size() - padding_length; + + if (record[offset] == 0x02 /* padding delimiter octet */) + break; + + if (record[offset] != 0x00 /* valid padding byte */) + return false; + } + + record.remove_suffix(padding_length); + return true; + } +}; + +} // namespace + +const size_t GCMMessageCryptographer::kAuthenticationTagBytes = 16; +const size_t GCMMessageCryptographer::kSaltSize = 16; + +GCMMessageCryptographer::GCMMessageCryptographer(Version version) { + switch (version) { + case Version::DRAFT_03: + encryption_scheme_ = std::make_unique(); + return; + case Version::DRAFT_08: + encryption_scheme_ = std::make_unique(); + return; + } + + NOTREACHED(); +} + +GCMMessageCryptographer::~GCMMessageCryptographer() = default; + +bool GCMMessageCryptographer::Encrypt( + const base::StringPiece& recipient_public_key, + const base::StringPiece& sender_public_key, + const base::StringPiece& ecdh_shared_secret, + const base::StringPiece& auth_secret, + const base::StringPiece& salt, + const base::StringPiece& plaintext, + size_t* record_size, + std::string* ciphertext) const { + DCHECK_EQ(recipient_public_key.size(), 65u); + DCHECK_EQ(sender_public_key.size(), 65u); + DCHECK_EQ(ecdh_shared_secret.size(), 32u); + DCHECK_EQ(auth_secret.size(), 16u); + DCHECK_EQ(salt.size(), 16u); + DCHECK(record_size); + DCHECK(ciphertext); + + std::string prk = encryption_scheme_->DerivePseudoRandomKey( + recipient_public_key, sender_public_key, ecdh_shared_secret, auth_secret); + + std::string content_encryption_key = DeriveContentEncryptionKey( + recipient_public_key, sender_public_key, prk, salt); + std::string nonce = + DeriveNonce(recipient_public_key, sender_public_key, prk, salt); + + std::string record = encryption_scheme_->CreateRecord(plaintext); + std::string encrypted_record; + + if (!TransformRecord(Direction::ENCRYPT, record, content_encryption_key, + nonce, &encrypted_record)) { + return false; + } + + // The advertised record size must be at least one more than the padded + // plaintext to ensure only one record. + *record_size = std::max(kDefaultRecordSize, record.size() + 1); + + ciphertext->swap(encrypted_record); + return true; +} + +bool GCMMessageCryptographer::Decrypt( + const base::StringPiece& recipient_public_key, + const base::StringPiece& sender_public_key, + const base::StringPiece& ecdh_shared_secret, + const base::StringPiece& auth_secret, + const base::StringPiece& salt, + const base::StringPiece& ciphertext, + size_t record_size, + std::string* plaintext) const { + DCHECK_EQ(recipient_public_key.size(), 65u); + DCHECK_EQ(sender_public_key.size(), 65u); + DCHECK_EQ(ecdh_shared_secret.size(), 32u); + DCHECK_EQ(auth_secret.size(), 16u); + DCHECK_EQ(salt.size(), 16u); + DCHECK(plaintext); + + if (record_size <= 1) { + LOG(ERROR) << "Invalid record size passed."; + return false; + } + + std::string prk = encryption_scheme_->DerivePseudoRandomKey( + recipient_public_key, sender_public_key, ecdh_shared_secret, auth_secret); + + std::string content_encryption_key = DeriveContentEncryptionKey( + recipient_public_key, sender_public_key, prk, salt); + + std::string nonce = + DeriveNonce(recipient_public_key, sender_public_key, prk, salt); + + if (!encryption_scheme_->ValidateCiphertextSize(ciphertext.size(), + record_size)) { + LOG(ERROR) << "Invalid ciphertext size passed."; + return false; + } + + std::string decrypted_record_string; + if (!TransformRecord(Direction::DECRYPT, ciphertext, content_encryption_key, + nonce, &decrypted_record_string)) { + LOG(ERROR) << "Unable to transform the record."; + return false; + } + + DCHECK(!decrypted_record_string.empty()); + + base::StringPiece decrypted_record(decrypted_record_string); + if (!encryption_scheme_->ValidateAndRemovePadding(decrypted_record)) { + LOG(ERROR) << "Padding could not be validated or removed."; + return false; + } + + plaintext->assign(decrypted_record.data(), decrypted_record.size()); + return true; +} + +bool GCMMessageCryptographer::TransformRecord(Direction direction, + const base::StringPiece& input, + const base::StringPiece& key, + const base::StringPiece& nonce, + std::string* output) const { + DCHECK(output); + + const EVP_AEAD* aead = EVP_aead_aes_128_gcm(); + + EVP_AEAD_CTX context; + if (!EVP_AEAD_CTX_init(&context, aead, + reinterpret_cast(key.data()), + key.size(), EVP_AEAD_DEFAULT_TAG_LENGTH, nullptr)) { + return false; + } + + base::CheckedNumeric maximum_output_length(input.size()); + if (direction == Direction::ENCRYPT) + maximum_output_length += kAuthenticationTagBytes; + + // WriteInto requires the buffer to finish with a NULL-byte. + maximum_output_length += 1; + + size_t output_length = 0; + uint8_t* raw_output = reinterpret_cast( + base::WriteInto(output, maximum_output_length.ValueOrDie())); + + EVP_AEAD_CTX_TransformFunction* transform_function = + direction == Direction::ENCRYPT ? EVP_AEAD_CTX_seal : EVP_AEAD_CTX_open; + + if (!transform_function( + &context, raw_output, &output_length, output->size(), + reinterpret_cast(nonce.data()), nonce.size(), + reinterpret_cast(input.data()), input.size(), + nullptr, 0)) { + EVP_AEAD_CTX_cleanup(&context); + return false; + } + + EVP_AEAD_CTX_cleanup(&context); + + base::CheckedNumeric expected_output_length(input.size()); + if (direction == Direction::ENCRYPT) + expected_output_length += kAuthenticationTagBytes; + else + expected_output_length -= kAuthenticationTagBytes; + + DCHECK_EQ(expected_output_length.ValueOrDie(), output_length); + + output->resize(output_length); + return true; +} + +std::string GCMMessageCryptographer::DeriveContentEncryptionKey( + const base::StringPiece& recipient_public_key, + const base::StringPiece& sender_public_key, + const base::StringPiece& ecdh_shared_secret, + const base::StringPiece& salt) const { + std::string content_encryption_key_info = + encryption_scheme_->GenerateInfoForContentEncoding( + EncryptionScheme::EncodingType::CONTENT_ENCRYPTION_KEY, + recipient_public_key, sender_public_key); + + return crypto::HkdfSha256(ecdh_shared_secret, salt, + content_encryption_key_info, + kContentEncryptionKeySize); +} + +std::string GCMMessageCryptographer::DeriveNonce( + const base::StringPiece& recipient_public_key, + const base::StringPiece& sender_public_key, + const base::StringPiece& ecdh_shared_secret, + const base::StringPiece& salt) const { + std::string nonce_info = encryption_scheme_->GenerateInfoForContentEncoding( + EncryptionScheme::EncodingType::NONCE, recipient_public_key, + sender_public_key); + + // https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-02 + // defines that the result should be XOR'ed with the record's sequence number, + // however, Web Push encryption is limited to a single record per + // https://tools.ietf.org/html/draft-ietf-webpush-encryption-03. + + return crypto::HkdfSha256(ecdh_shared_secret, salt, nonce_info, kNonceSize); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/crypto/gcm_message_cryptographer.h b/chromium/components/gcm_driver/crypto/gcm_message_cryptographer.h new file mode 100644 index 00000000000..1332708e0d9 --- /dev/null +++ b/chromium/components/gcm_driver/crypto/gcm_message_cryptographer.h @@ -0,0 +1,167 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_GCM_DRIVER_CRYPTO_GCM_MESSAGE_CRYPTOGRAPHER_H_ +#define COMPONENTS_GCM_DRIVER_CRYPTO_GCM_MESSAGE_CRYPTOGRAPHER_H_ + +#include +#include +#include +#include + +#include "base/compiler_specific.h" +#include "base/gtest_prod_util.h" +#include "base/strings/string_piece.h" + +namespace gcm { + +// Messages delivered through GCM may be encrypted according to the IETF Web +// Push protocol. We support two versions of ietf-webpush-encryption. The user +// of this class must pass in the version to use when constructing an instance. +// +// https://tools.ietf.org/html/draft-ietf-webpush-encryption-03 +// https://tools.ietf.org/html/draft-ietf-webpush-encryption-08 (WGLC) +// +// This class implements the ability to encrypt or decrypt such messages using +// AEAD_AES_128_GCM with a 16-octet authentication tag. The encrypted payload +// will be stored in a single record. +// +// Note that while this class is not responsible for creating or storing the +// actual keys, it uses a key derivation function for the actual message +// encryption/decryption, thus allowing for the safe re-use of keys in multiple +// messages provided that a cryptographically-strong random salt is used. +class GCMMessageCryptographer { + public: + // Size, in bytes, of the authentication tag included in the messages. + static const size_t kAuthenticationTagBytes; + + // Salt size, in bytes, that will be used together with the key to create a + // unique content encryption key for a given message. + static const size_t kSaltSize; + + // Version of the encryption scheme desired by the consumer. + enum class Version { + // https://tools.ietf.org/html/draft-ietf-webpush-encryption-03 + DRAFT_03, + + // https://tools.ietf.org/html/draft-ietf-webpush-encryption-08 (WGLC) + DRAFT_08 + }; + + // Interface that different versions of the encryption scheme must implement. + class EncryptionScheme { + public: + virtual ~EncryptionScheme() {} + + // Type of encoding to produce in GenerateInfoForContentEncoding(). + enum class EncodingType { CONTENT_ENCRYPTION_KEY, NONCE }; + + // Derives the pseudo random key (PRK) to use for deriving the content + // encryption key and the nonce. + virtual std::string DerivePseudoRandomKey( + const base::StringPiece& recipient_public_key, + const base::StringPiece& sender_public_key, + const base::StringPiece& ecdh_shared_secret, + const base::StringPiece& auth_secret) = 0; + + // Generates the info string used for generating the content encryption key + // and the nonce used for the cryptographic transformation. + virtual std::string GenerateInfoForContentEncoding( + EncodingType type, + const base::StringPiece& recipient_public_key, + const base::StringPiece& sender_public_key) = 0; + + // Creates an encryption record to contain the given |plaintext|. + virtual std::string CreateRecord(const base::StringPiece& plaintext) = 0; + + // Validates that the |ciphertext_size| is valid following the scheme. + virtual bool ValidateCiphertextSize(size_t ciphertext_size, + size_t record_size) = 0; + + // Verifies that the padding included in |record| is valid and removes it + // from the StringPiece. Returns whether the padding was valid. + virtual bool ValidateAndRemovePadding(base::StringPiece& record) = 0; + }; + + // Creates a new cryptographer for |version| of the encryption scheme. + explicit GCMMessageCryptographer(Version version); + ~GCMMessageCryptographer(); + + // Encrypts the |plaintext| in accordance with the Web Push Encryption scheme + // this cryptographer represents, storing the result in |*record_size| and + // |*ciphertext|. Returns whether encryption was successful. + // + // |recipient_public_key|: Recipient's key as an uncompressed P-256 EC point. + // |sender_public_key|: Sender's key as an uncompressed P-256 EC point. + // |ecdh_shared_secret|: 32-byte shared secret between the key pairs. + // |auth_secret|: 16-byte prearranged secret between recipient and sender. + // |salt|: 16-byte cryptographically secure salt unique to the message. + // |plaintext|: The plaintext that is to be encrypted. + // |*record_size|: Out parameter in which the record size will be written. + // |*ciphertext|: Out parameter in which the ciphertext will be written. + bool Encrypt(const base::StringPiece& recipient_public_key, + const base::StringPiece& sender_public_key, + const base::StringPiece& ecdh_shared_secret, + const base::StringPiece& auth_secret, + const base::StringPiece& salt, + const base::StringPiece& plaintext, + size_t* record_size, + std::string* ciphertext) const WARN_UNUSED_RESULT; + + // Decrypts the |ciphertext| in accordance with the Web Push Encryption scheme + // this cryptographer represents, storing the result in |*plaintext|. Returns + // whether decryption was successful. + // + // |recipient_public_key|: Recipient's key as an uncompressed P-256 EC point. + // |sender_public_key|: Sender's key as an uncompressed P-256 EC point. + // |ecdh_shared_secret|: 32-byte shared secret between the key pairs. + // |auth_secret|: 16-byte prearranged secret between recipient and sender. + // |salt|: 16-byte cryptographically secure salt unique to the message. + // |ciphertext|: The ciphertext that is to be decrypted. + // |record_size|: Size of a single record. Must be larger than or equal to + // len(plaintext) plus the ciphertext's overhead (18 bytes). + // |*plaintext|: Out parameter in which the plaintext will be written. + bool Decrypt(const base::StringPiece& recipient_public_key, + const base::StringPiece& sender_public_key, + const base::StringPiece& ecdh_shared_secret, + const base::StringPiece& auth_secret, + const base::StringPiece& salt, + const base::StringPiece& ciphertext, + size_t record_size, + std::string* plaintext) const WARN_UNUSED_RESULT; + + private: + FRIEND_TEST_ALL_PREFIXES(GCMMessageCryptographerTest, AuthSecretAffectsPRK); + FRIEND_TEST_ALL_PREFIXES(GCMMessageCryptographerTest, InvalidRecordPadding); + + enum class Direction { ENCRYPT, DECRYPT }; + + // Derives the content encryption key from |ecdh_shared_secret| and |salt|. + std::string DeriveContentEncryptionKey( + const base::StringPiece& recipient_public_key, + const base::StringPiece& sender_public_key, + const base::StringPiece& ecdh_shared_secret, + const base::StringPiece& salt) const; + + // Derives the nonce from |ecdh_shared_secret| and |salt|. + std::string DeriveNonce(const base::StringPiece& recipient_public_key, + const base::StringPiece& sender_public_key, + const base::StringPiece& ecdh_shared_secret, + const base::StringPiece& salt) const; + + // Private implementation of the encryption and decryption routines. + bool TransformRecord(Direction direction, + const base::StringPiece& input, + const base::StringPiece& key, + const base::StringPiece& nonce, + std::string* output) const; + + // Implementation of the encryption scheme. Set in the constructor depending + // on the version requested by the consumer. + std::unique_ptr encryption_scheme_; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_CRYPTO_GCM_MESSAGE_CRYPTOGRAPHER_H_ diff --git a/chromium/components/gcm_driver/crypto/gcm_message_cryptographer_unittest.cc b/chromium/components/gcm_driver/crypto/gcm_message_cryptographer_unittest.cc new file mode 100644 index 00000000000..380e166e682 --- /dev/null +++ b/chromium/components/gcm_driver/crypto/gcm_message_cryptographer_unittest.cc @@ -0,0 +1,906 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/crypto/gcm_message_cryptographer.h" + +#include + +#include "base/base64url.h" +#include "base/big_endian.h" +#include "base/cxx17_backports.h" +#include "base/logging.h" +#include "base/strings/string_piece.h" +#include "base/strings/string_util.h" +#include "components/gcm_driver/crypto/message_payload_parser.h" +#include "components/gcm_driver/crypto/p256_key_util.h" +#include "crypto/ec_private_key.h" +#include "crypto/random.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { + +namespace { + +// Example plaintext data to use in the tests. +const char kExamplePlaintext[] = "Example plaintext"; + +// Expected sizes of the different input given to the cryptographer. +constexpr size_t kEcdhSharedSecretSize = 32; +constexpr size_t kAuthSecretSize = 16; +constexpr size_t kSaltSize = 16; + +// Keying material for both parties as P-256 EC points. Used to make sure that +// the test vectors are reproducible. +const unsigned char kCommonSenderPublicKey[] = { + 0x04, 0x05, 0x3C, 0xA1, 0xB9, 0xA5, 0xAB, 0xB8, 0x2D, 0x88, 0x48, + 0x82, 0xC9, 0x49, 0x19, 0x91, 0xD5, 0xFD, 0xD1, 0x92, 0xDB, 0xA7, + 0x7E, 0x70, 0x48, 0x37, 0x41, 0xCD, 0x90, 0x05, 0x80, 0xDF, 0x65, + 0x9A, 0xA1, 0x1A, 0x04, 0xF1, 0x98, 0x25, 0xF2, 0xC2, 0x13, 0x5D, + 0xD9, 0x72, 0x35, 0x75, 0x24, 0xF9, 0xFF, 0x25, 0xD1, 0xBC, 0x84, + 0x46, 0x4E, 0x88, 0x08, 0x55, 0x70, 0x9F, 0xA7, 0x07, 0xD9}; +static_assert(base::size(kCommonSenderPublicKey) == 65, + "Raw P-256 public keys must be 65 bytes in size."); + +const unsigned char kCommonRecipientPublicKey[] = { + 0x04, 0x35, 0x02, 0x67, 0xB9, 0x10, 0x8F, 0x9B, 0xF1, 0x85, 0xF5, + 0x1B, 0xD7, 0xA4, 0xEF, 0xBD, 0x28, 0xB3, 0x11, 0x40, 0xBA, 0xD0, + 0xEE, 0xB2, 0x97, 0xDA, 0x6A, 0x93, 0x2D, 0x26, 0x45, 0xBD, 0xB2, + 0x9A, 0x9F, 0xB8, 0x19, 0xD8, 0x21, 0x6F, 0x66, 0xE3, 0xF6, 0x0B, + 0x74, 0xB2, 0x28, 0x38, 0xDC, 0xA7, 0x8A, 0x58, 0x0D, 0x56, 0x47, + 0x3E, 0xD0, 0x5B, 0x5C, 0x93, 0x4E, 0xB3, 0x89, 0x87, 0x64}; +static_assert(base::size(kCommonRecipientPublicKey) == 65, + "Raw P-256 public keys must be 65 bytes in size."); + +const unsigned char kCommonRecipientPrivateKey[] = { + 0x30, 0x81, 0x87, 0x02, 0x01, 0x00, 0x30, 0x13, 0x06, 0x07, 0x2A, 0x86, + 0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, + 0x03, 0x01, 0x07, 0x04, 0x6D, 0x30, 0x6B, 0x02, 0x01, 0x01, 0x04, 0x20, + 0x16, 0xCC, 0xB4, 0x37, 0xA3, 0x04, 0x0C, 0x28, 0xDE, 0x56, 0x77, 0x27, + 0x0B, 0xD8, 0x1E, 0x82, 0xD7, 0x7F, 0x07, 0xA6, 0x43, 0x6E, 0x70, 0xDD, + 0x9C, 0x3C, 0xF1, 0x2C, 0x93, 0xE3, 0x37, 0xD1, 0xA1, 0x44, 0x03, 0x42, + 0x00, 0x04, 0x35, 0x02, 0x67, 0xB9, 0x10, 0x8F, 0x9B, 0xF1, 0x85, 0xF5, + 0x1B, 0xD7, 0xA4, 0xEF, 0xBD, 0x28, 0xB3, 0x11, 0x40, 0xBA, 0xD0, 0xEE, + 0xB2, 0x97, 0xDA, 0x6A, 0x93, 0x2D, 0x26, 0x45, 0xBD, 0xB2, 0x9A, 0x9F, + 0xB8, 0x19, 0xD8, 0x21, 0x6F, 0x66, 0xE3, 0xF6, 0x0B, 0x74, 0xB2, 0x28, + 0x38, 0xDC, 0xA7, 0x8A, 0x58, 0x0D, 0x56, 0x47, 0x3E, 0xD0, 0x5B, 0x5C, + 0x93, 0x4E, 0xB3, 0x89, 0x87, 0x64}; + +const unsigned char kCommonAuthSecret[] = {0x25, 0xF2, 0xC2, 0xB8, 0x19, 0xD8, + 0xFD, 0x35, 0x97, 0xDF, 0xFB, 0x5E, + 0xF6, 0x0B, 0xD7, 0xA4}; +static_assert(base::size(kCommonAuthSecret) == 16, + "Auth secrets must be 16 bytes in size."); + +// Test vectors containing reference input for draft-ietf-webpush-encryption +// that was created using an separate JavaScript implementation of the draft. +struct TestVector { + const char* const input; + const unsigned char ecdh_shared_secret[kEcdhSharedSecretSize]; + const unsigned char auth_secret[kAuthSecretSize]; + const unsigned char salt[kSaltSize]; + size_t record_size; + const char* const output; +}; + +const TestVector kEncryptionTestVectorsDraft03[] = { + // Simple message. + {"Hello, world!", + {0x0B, 0x32, 0xE2, 0xD1, 0x6A, 0xBF, 0x4F, 0x2C, 0x49, 0xEA, 0xF7, + 0x5D, 0x71, 0x7D, 0x89, 0xA9, 0xA7, 0x5E, 0x21, 0xB2, 0xB5, 0x51, + 0xE6, 0x4C, 0x08, 0x68, 0xD3, 0x6F, 0x8F, 0x72, 0x7E, 0x14}, + {0xD3, 0xF2, 0x78, 0xBD, 0x8D, 0xDD, 0x84, 0x99, 0x66, 0x08, 0xD7, 0x0F, + 0xBA, 0x9B, 0x60, 0xFC}, + {0x15, 0x4A, 0xD7, 0x73, 0x92, 0xBD, 0x3B, 0xCF, 0x6F, 0x98, 0xDC, 0x9B, + 0x8B, 0x56, 0xFB, 0xBD}, + 4096, + "T4SXCyj84drA6wRaBNLGDMzeyOEBWjsIEkS2ros6Aw"}, + // Empty message. + {"", + {0x3F, 0xD8, 0x95, 0x2C, 0xA2, 0x11, 0xBD, 0x7B, 0x57, 0xB2, 0x00, + 0xBD, 0x57, 0x68, 0x3F, 0xF0, 0x14, 0x57, 0x5F, 0xB1, 0x9F, 0x15, + 0x4F, 0x11, 0xF0, 0x4D, 0xA2, 0xE8, 0x4C, 0xEA, 0x74, 0x3B}, + {0xB1, 0xE1, 0xC7, 0x32, 0x4C, 0xAA, 0x56, 0x32, 0x68, 0x20, 0x0F, 0x26, + 0x3F, 0x48, 0x4D, 0x99}, + {0xE9, 0x39, 0x45, 0xBC, 0x96, 0x96, 0x88, 0x76, 0xFC, 0xA1, 0xAD, 0xE4, + 0x9D, 0x28, 0xF3, 0x73}, + 4096, + "8s-Tzq8Cn_eobL6uEcNDXL7K"}}; + +const TestVector kEncryptionTestVectorsDraft08[] = { + // Simple message. + {"Hello, world!", + {0x0B, 0x32, 0xE2, 0xD1, 0x6A, 0xBF, 0x4F, 0x2C, 0x49, 0xEA, 0xF7, + 0x5D, 0x71, 0x7D, 0x89, 0xA9, 0xA7, 0x5E, 0x21, 0xB2, 0xB5, 0x51, + 0xE6, 0x4C, 0x08, 0x68, 0xD3, 0x6F, 0x8F, 0x72, 0x7E, 0x14}, + {0xD3, 0xF2, 0x78, 0xBD, 0x8D, 0xDD, 0x84, 0x99, 0x66, 0x08, 0xD7, 0x0F, + 0xBA, 0x9B, 0x60, 0xFC}, + {0x15, 0x4A, 0xD7, 0x73, 0x92, 0xBD, 0x3B, 0xCF, 0x6F, 0x98, 0xDC, 0x9B, + 0x8B, 0x56, 0xFB, 0xBD}, + 4096, + "3biYN3Aa30D30bKJMdGlEyYPrz7Wg293NYc31rb6"}, + // Empty message. + {"", + {0x3F, 0xD8, 0x95, 0x2C, 0xA2, 0x11, 0xBD, 0x7B, 0x57, 0xB2, 0x00, + 0xBD, 0x57, 0x68, 0x3F, 0xF0, 0x14, 0x57, 0x5F, 0xB1, 0x9F, 0x15, + 0x4F, 0x11, 0xF0, 0x4D, 0xA2, 0xE8, 0x4C, 0xEA, 0x74, 0x3B}, + {0xB1, 0xE1, 0xC7, 0x32, 0x4C, 0xAA, 0x56, 0x32, 0x68, 0x20, 0x0F, 0x26, + 0x3F, 0x48, 0x4D, 0x99}, + {0xE9, 0x39, 0x45, 0xBC, 0x96, 0x96, 0x88, 0x76, 0xFC, 0xA1, 0xAD, 0xE4, + 0x9D, 0x28, 0xF3, 0x73}, + 4096, + "5OXY345WYPyIvsF7hx4swuA"}}; + +const TestVector kDecryptionTestVectorsDraft03[] = { + // Simple message. + {"lsemWwzlFoJzoidHCnVuxRiJpotTcYokJHKzmQ2FsA", + {0x4D, 0x3A, 0x6C, 0xBA, 0xD8, 0x1D, 0x8E, 0x68, 0x8B, 0xE6, 0x76, + 0xA7, 0xFF, 0x60, 0xC7, 0xFE, 0x77, 0xE2, 0x6D, 0x37, 0xF6, 0x12, + 0x44, 0xE2, 0x25, 0xFE, 0xE1, 0xD8, 0xCF, 0x8A, 0xA8, 0x33}, + {0x62, 0x36, 0xAC, 0xCA, 0x74, 0xD4, 0x49, 0x49, 0x6B, 0x27, 0xB4, 0xF7, + 0xC1, 0xE5, 0x30, 0x9A}, + {0x1C, 0xA7, 0xFD, 0x98, 0x1A, 0xE4, 0xA7, 0x92, 0xE1, 0xB6, 0xA1, 0xE3, + 0x41, 0x63, 0x87, 0x76}, + 4096, + "Hello, world!"}, + // Simple message with 16 bytes of padding. + {"VQB6Ds-q9xRqyM1tj_gksSgc78vCWEhphZ-NF1E7_yMfPuRRZlC_Xt9_2NsX3SU", + {0x8B, 0x38, 0x8E, 0x22, 0xD5, 0xC4, 0xFD, 0x65, 0x8A, 0xBB, 0xD9, + 0x58, 0xBD, 0xF5, 0xFF, 0x79, 0xCF, 0x9D, 0xBD, 0x87, 0x16, 0x7E, + 0x93, 0x84, 0x20, 0x8E, 0x8D, 0x49, 0x41, 0x7D, 0x8E, 0x8F}, + {0x3E, 0x65, 0xC7, 0x1F, 0x75, 0x7A, 0x43, 0xC4, 0x78, 0x6C, 0x64, 0x99, + 0x49, 0xA0, 0xC4, 0xB2}, + {0x43, 0x4D, 0x30, 0x8E, 0xE4, 0x76, 0xB5, 0xD0, 0x87, 0xFC, 0x04, 0xD1, + 0x2E, 0x35, 0x75, 0x63}, + 4096, + "Hello, world!"}, + // Empty message. + {"xU8a499UHB_-YSV4VOm-JZnT", + {0x68, 0x72, 0x3D, 0x13, 0xE7, 0x50, 0xFA, 0x3E, 0xA0, 0x59, 0x33, + 0xF1, 0x73, 0xA8, 0xE8, 0xCD, 0x8D, 0xD4, 0x3C, 0xDC, 0xDE, 0x06, + 0x35, 0x5F, 0x51, 0xBB, 0xB2, 0x57, 0x97, 0x72, 0x9D, 0xFB}, + {0x84, 0xB2, 0x2A, 0xE7, 0xC6, 0xC0, 0xCE, 0x5F, 0xAD, 0x37, 0x06, 0x7F, + 0xD1, 0xFD, 0x10, 0x87}, + {0x9B, 0xC5, 0x8D, 0x5F, 0xD6, 0xD2, 0xA6, 0xBD, 0xAF, 0x4B, 0xD9, 0x60, + 0xC6, 0xB4, 0x50, 0x0F}, + 4096, + ""}, + // Message with an invalid record size. + {"gfB-_edj7qEVokyVHpkDJN6FVKHnlWs1RCDw5bmrwQ", + {0x5F, 0xE1, 0x7C, 0x4B, 0xFF, 0x04, 0xBF, 0x2C, 0x70, 0x67, 0xFA, + 0xF8, 0xB0, 0x07, 0x4F, 0xF6, 0x3C, 0x03, 0x6F, 0xBE, 0xA1, 0x1F, + 0x4B, 0x99, 0x25, 0x4F, 0xB9, 0x5F, 0xC4, 0x78, 0x76, 0xDE}, + {0x59, 0xAB, 0x45, 0xFC, 0x6A, 0xF5, 0xB3, 0xE0, 0xF5, 0x40, 0xD7, 0x98, + 0x0F, 0xF0, 0xA4, 0xCB}, + {0xDB, 0xA0, 0xF2, 0x91, 0x8D, 0x50, 0x42, 0xE0, 0x17, 0x68, 0x5B, 0x9B, + 0xF2, 0xA2, 0xC3, 0xF9}, + 7, + nullptr}, + // Message with four bytes of invalid, non-zero padding. + {"2FJmrF95yVU8Q8cYQy9OoOwCb59ZoRlxazPE0T-MNOSMbr0", + {0x6B, 0x82, 0x92, 0xD3, 0x71, 0x9A, 0x97, 0x76, 0x45, 0x11, 0x99, + 0x6D, 0xBF, 0x56, 0xCC, 0x81, 0x98, 0x56, 0x80, 0xF5, 0x78, 0x36, + 0xD6, 0x43, 0x95, 0x68, 0xDB, 0x0F, 0x23, 0x39, 0xF3, 0x6E}, + {0x02, 0x16, 0xDC, 0xC3, 0xDE, 0x2C, 0xB5, 0x08, 0x89, 0xDB, 0xD8, 0x18, + 0x68, 0x83, 0x1C, 0xDB}, + {0xB7, 0x85, 0x5D, 0x8E, 0x84, 0xC3, 0x2D, 0x61, 0x9B, 0x78, 0x3B, 0x60, + 0x0E, 0x70, 0x84, 0xF3}, + 4096, + nullptr}, + // Message with multiple (2) records. + {"reI6sW6y67FI8Kxk-x9GNwiu77His_f5GioDBiKS7IzjDQ", + {0xC6, 0x16, 0x6F, 0xAF, 0xE1, 0xB6, 0x8F, 0x2B, 0x0F, 0x67, 0x5A, + 0xC7, 0xAC, 0x7E, 0xF6, 0x7C, 0x33, 0xA2, 0xA1, 0x11, 0xB0, 0xB0, + 0xAB, 0xAC, 0x37, 0x61, 0xF4, 0xCB, 0x98, 0xFF, 0x00, 0x51}, + {0xAE, 0xDA, 0x86, 0xDF, 0x6B, 0x03, 0x88, 0xDE, 0x90, 0xBB, 0xB7, 0xA0, + 0x78, 0x91, 0x3A, 0x36}, + {0x4C, 0x4E, 0x2A, 0x8D, 0x88, 0x82, 0xCF, 0xC2, 0xF9, 0x8A, 0xFD, 0x31, + 0xF8, 0xD1, 0xF6, 0xB5}, + 8, + nullptr}}; + +const TestVector kDecryptionTestVectorsDraft08[] = { + // Simple message. + {"baIDPDv-Do_x1RVtlFDex2uCvd3Ugrv-gJG3sWeg", + {0x4D, 0x3A, 0x6C, 0xBA, 0xD8, 0x1D, 0x8E, 0x68, 0x8B, 0xE6, 0x76, + 0xA7, 0xFF, 0x60, 0xC7, 0xFE, 0x77, 0xE2, 0x6D, 0x37, 0xF6, 0x12, + 0x44, 0xE2, 0x25, 0xFE, 0xE1, 0xD8, 0xCF, 0x8A, 0xA8, 0x33}, + {0x62, 0x36, 0xAC, 0xCA, 0x74, 0xD4, 0x49, 0x49, 0x6B, 0x27, 0xB4, 0xF7, + 0xC1, 0xE5, 0x30, 0x9A}, + {0x1C, 0xA7, 0xFD, 0x98, 0x1A, 0xE4, 0xA7, 0x92, 0xE1, 0xB6, 0xA1, 0xE3, + 0x41, 0x63, 0x87, 0x76}, + 4096, + "Hello, world!"}, + // Simple message with 16 bytes of padding. + {"6Zq7GKQ7zRxeOWoYR71Nx7xJzCZUUNhz6bhV1-ZIg6dVra0x1uWXms5gHp6F6A", + {0x8B, 0x38, 0x8E, 0x22, 0xD5, 0xC4, 0xFD, 0x65, 0x8A, 0xBB, 0xD9, + 0x58, 0xBD, 0xF5, 0xFF, 0x79, 0xCF, 0x9D, 0xBD, 0x87, 0x16, 0x7E, + 0x93, 0x84, 0x20, 0x8E, 0x8D, 0x49, 0x41, 0x7D, 0x8E, 0x8F}, + {0x3E, 0x65, 0xC7, 0x1F, 0x75, 0x7A, 0x43, 0xC4, 0x78, 0x6C, 0x64, 0x99, + 0x49, 0xA0, 0xC4, 0xB2}, + {0x43, 0x4D, 0x30, 0x8E, 0xE4, 0x76, 0xB5, 0xD0, 0x87, 0xFC, 0x04, 0xD1, + 0x2E, 0x35, 0x75, 0x63}, + 4096, + "Hello, world!"}, + // Empty message. + {"bHU7ponA7WAGB0onUybG9nQ", + {0x68, 0x72, 0x3D, 0x13, 0xE7, 0x50, 0xFA, 0x3E, 0xA0, 0x59, 0x33, + 0xF1, 0x73, 0xA8, 0xE8, 0xCD, 0x8D, 0xD4, 0x3C, 0xDC, 0xDE, 0x06, + 0x35, 0x5F, 0x51, 0xBB, 0xB2, 0x57, 0x97, 0x72, 0x9D, 0xFB}, + {0x84, 0xB2, 0x2A, 0xE7, 0xC6, 0xC0, 0xCE, 0x5F, 0xAD, 0x37, 0x06, 0x7F, + 0xD1, 0xFD, 0x10, 0x87}, + {0x9B, 0xC5, 0x8D, 0x5F, 0xD6, 0xD2, 0xA6, 0xBD, 0xAF, 0x4B, 0xD9, 0x60, + 0xC6, 0xB4, 0x50, 0x0F}, + 4096, + ""}}; + +// Computes the shared secret between the sender and the receiver. The sender +// must have a ASN.1-encoded PKCS #8 EncryptedPrivateKeyInfo block, whereas +// the receiver must have a public key in uncompressed EC point format. +bool ComputeSharedP256SecretFromPrivateKeyStr( + const base::StringPiece& private_key, + const base::StringPiece& peer_public_key, + std::string* out_shared_secret) { + DCHECK(out_shared_secret); + std::unique_ptr local_key( + crypto::ECPrivateKey::CreateFromPrivateKeyInfo(std::vector( + private_key.data(), private_key.data() + private_key.size()))); + if (!local_key) { + DLOG(ERROR) << "Unable to create the local key"; + return false; + } + + return ComputeSharedP256Secret(*local_key, peer_public_key, + out_shared_secret); +} + +void ComputeSharedSecret( + const base::StringPiece& encoded_sender_private_key, + const base::StringPiece& encoded_receiver_public_key, + std::string* shared_secret) { + std::string sender_private_key, receiver_public_key; + ASSERT_TRUE(base::Base64UrlDecode( + encoded_sender_private_key, + base::Base64UrlDecodePolicy::IGNORE_PADDING, &sender_private_key)); + ASSERT_TRUE(base::Base64UrlDecode( + encoded_receiver_public_key, + base::Base64UrlDecodePolicy::IGNORE_PADDING, &receiver_public_key)); + + ASSERT_TRUE(ComputeSharedP256SecretFromPrivateKeyStr( + sender_private_key, receiver_public_key, + shared_secret)); +} + +} // namespace + +class GCMMessageCryptographerTestBase : public ::testing::Test { + public: + void SetUp() override { + recipient_public_key_.assign( + kCommonRecipientPublicKey, + kCommonRecipientPublicKey + base::size(kCommonRecipientPublicKey)); + sender_public_key_.assign( + kCommonSenderPublicKey, + kCommonSenderPublicKey + base::size(kCommonSenderPublicKey)); + + std::string recipient_private_key( + kCommonRecipientPrivateKey, + kCommonRecipientPrivateKey + base::size(kCommonRecipientPrivateKey)); + std::vector recipient_private_key_vec( + recipient_private_key.begin(), recipient_private_key.end()); + std::unique_ptr recipient_key = + crypto::ECPrivateKey::CreateFromPrivateKeyInfo(recipient_private_key_vec); + ASSERT_TRUE(recipient_key); + ASSERT_TRUE(ComputeSharedP256Secret( + *recipient_key, sender_public_key_, &ecdh_shared_secret_)); + + auth_secret_.assign(kCommonAuthSecret, + kCommonAuthSecret + base::size(kCommonAuthSecret)); + } + + protected: + // Public keys of the recipient and sender as uncompressed P-256 EC points. + std::string recipient_public_key_; + std::string sender_public_key_; + + // Shared secret to use in transformations. Associated with the keys above. + std::string ecdh_shared_secret_; + + // Authentication secret to use in tests where no specific value is expected. + std::string auth_secret_; +}; + +class GCMMessageCryptographerTest + : public GCMMessageCryptographerTestBase, + public testing::WithParamInterface { + public: + void SetUp() override { + GCMMessageCryptographerTestBase::SetUp(); + + cryptographer_ = std::make_unique(GetParam()); + } + + protected: + // Generates a cryptographically secure random salt of 16-octets in size, the + // required length as expected by the HKDF. + std::string GenerateRandomSalt() { + std::string salt; + + crypto::RandBytes(base::WriteInto(&salt, kSaltSize + 1), kSaltSize); + return salt; + } + + // The GCMMessageCryptographer instance to use for the tests. + std::unique_ptr cryptographer_; +}; + +TEST_P(GCMMessageCryptographerTest, RoundTrip) { + const std::string salt = GenerateRandomSalt(); + + size_t record_size = 0; + + std::string ciphertext, plaintext; + ASSERT_TRUE(cryptographer_->Encrypt( + recipient_public_key_, sender_public_key_, ecdh_shared_secret_, + auth_secret_, salt, kExamplePlaintext, &record_size, &ciphertext)); + + EXPECT_GT(record_size, ciphertext.size() - 16); + EXPECT_GT(ciphertext.size(), 0u); + + ASSERT_TRUE(cryptographer_->Decrypt(recipient_public_key_, sender_public_key_, + ecdh_shared_secret_, auth_secret_, salt, + ciphertext, record_size, &plaintext)); + + EXPECT_EQ(kExamplePlaintext, plaintext); +} + +TEST_P(GCMMessageCryptographerTest, RoundTripEmptyMessage) { + const std::string salt = GenerateRandomSalt(); + const std::string message; + + size_t record_size = 0; + + std::string ciphertext, plaintext; + ASSERT_TRUE(cryptographer_->Encrypt(recipient_public_key_, sender_public_key_, + ecdh_shared_secret_, auth_secret_, salt, + message, &record_size, &ciphertext)); + + EXPECT_GT(record_size, ciphertext.size() - 16); + EXPECT_GT(ciphertext.size(), 0u); + + ASSERT_TRUE(cryptographer_->Decrypt(recipient_public_key_, sender_public_key_, + ecdh_shared_secret_, auth_secret_, salt, + ciphertext, record_size, &plaintext)); + + EXPECT_EQ(message, plaintext); +} + +TEST_P(GCMMessageCryptographerTest, InvalidRecordSize) { + const std::string salt = GenerateRandomSalt(); + + size_t record_size = 0; + + std::string ciphertext, plaintext; + ASSERT_TRUE(cryptographer_->Encrypt( + recipient_public_key_, sender_public_key_, ecdh_shared_secret_, + auth_secret_, salt, kExamplePlaintext, &record_size, &ciphertext)); + + EXPECT_GT(record_size, ciphertext.size() - 16); + + EXPECT_FALSE(cryptographer_->Decrypt( + recipient_public_key_, sender_public_key_, ecdh_shared_secret_, + auth_secret_, salt, ciphertext, 0 /* record_size */, &plaintext)); + + EXPECT_FALSE(cryptographer_->Decrypt( + recipient_public_key_, sender_public_key_, ecdh_shared_secret_, + auth_secret_, salt, ciphertext, ciphertext.size() - 17, &plaintext)); + + EXPECT_TRUE(cryptographer_->Decrypt( + recipient_public_key_, sender_public_key_, ecdh_shared_secret_, + auth_secret_, salt, ciphertext, ciphertext.size() - 16, &plaintext)); +} + +TEST_P(GCMMessageCryptographerTest, InvalidRecordPadding) { + std::string message; + switch (GetParam()) { + case GCMMessageCryptographer::Version::DRAFT_03: + message.append(sizeof(uint8_t), '\00'); // padding length octets + message.append(sizeof(uint8_t), '\01'); + + message.append(sizeof(uint8_t), '\00'); // padding octet + message.append(kExamplePlaintext); + break; + case GCMMessageCryptographer::Version::DRAFT_08: + message.append(kExamplePlaintext); + message.append(sizeof(uint8_t), '\x02'); // padding delimiter octet + message.append(sizeof(uint8_t), '\x00'); // padding octet + break; + } + + const std::string salt = GenerateRandomSalt(); + + const std::string prk = + cryptographer_->encryption_scheme_->DerivePseudoRandomKey( + recipient_public_key_, sender_public_key_, ecdh_shared_secret_, + auth_secret_); + const std::string content_encryption_key = + cryptographer_->DeriveContentEncryptionKey(recipient_public_key_, + sender_public_key_, prk, salt); + const std::string nonce = cryptographer_->DeriveNonce( + recipient_public_key_, sender_public_key_, prk, salt); + + ASSERT_GT(message.size(), 2u); + const size_t record_size = message.size() + 1; + + std::string ciphertext, plaintext; + ASSERT_TRUE(cryptographer_->TransformRecord( + GCMMessageCryptographer::Direction::ENCRYPT, message, + content_encryption_key, nonce, &ciphertext)); + + ASSERT_TRUE(cryptographer_->Decrypt(recipient_public_key_, sender_public_key_, + ecdh_shared_secret_, auth_secret_, salt, + ciphertext, record_size, &plaintext)); + + // Note that GCMMessageCryptographer::Decrypt removes the padding. + EXPECT_EQ(kExamplePlaintext, plaintext); + + // Now run the same steps again, but have invalid padding length indicators. + // (Only applicable to draft-ietf-webpush-encryption-03.) + if (GetParam() == GCMMessageCryptographer::Version::DRAFT_03) { + // Padding that will spill over in the payload. + { + message[1] = 4; + + ASSERT_TRUE(cryptographer_->TransformRecord( + GCMMessageCryptographer::Direction::ENCRYPT, message, + content_encryption_key, nonce, &ciphertext)); + + ASSERT_FALSE(cryptographer_->Decrypt( + recipient_public_key_, sender_public_key_, ecdh_shared_secret_, + auth_secret_, salt, ciphertext, record_size, &plaintext)); + } + + // More padding octets than the length of the message. + { + message[1] = 64; + + ASSERT_TRUE(cryptographer_->TransformRecord( + GCMMessageCryptographer::Direction::ENCRYPT, message, + content_encryption_key, nonce, &ciphertext)); + + ASSERT_FALSE(cryptographer_->Decrypt( + recipient_public_key_, sender_public_key_, ecdh_shared_secret_, + auth_secret_, salt, ciphertext, record_size, &plaintext)); + } + + // Correct the |message| to be valid again. (A single byte of padding.) + message[1] = 1; + } + + // Run tests for a missing delimiter in the record. + // (Only applicable to draft-ietf-webpush-encryption-03.) + if (GetParam() == GCMMessageCryptographer::Version::DRAFT_08) { + message[message.size() - 2] = 0x00; + + ASSERT_TRUE(cryptographer_->TransformRecord( + GCMMessageCryptographer::Direction::ENCRYPT, message, + content_encryption_key, nonce, &ciphertext)); + + ASSERT_FALSE(cryptographer_->Decrypt( + recipient_public_key_, sender_public_key_, ecdh_shared_secret_, + auth_secret_, salt, ciphertext, record_size, &plaintext)); + + // Correct the |message| to be valid again. (Proper padding delimiter.) + message[message.size() - 2] = 0x02; + } + + // Finally run a test to make sure that we validate that all padding bytes are + // set to zeros. The position of the padding byte depends on the version. + switch (GetParam()) { + case GCMMessageCryptographer::Version::DRAFT_03: + message[2] = 0x13; + break; + case GCMMessageCryptographer::Version::DRAFT_08: + message[message.size() - 1] = 0x13; + break; + } + + ASSERT_TRUE(cryptographer_->TransformRecord( + GCMMessageCryptographer::Direction::ENCRYPT, message, + content_encryption_key, nonce, &ciphertext)); + + ASSERT_FALSE(cryptographer_->Decrypt( + recipient_public_key_, sender_public_key_, ecdh_shared_secret_, + auth_secret_, salt, ciphertext, record_size, &plaintext)); +} + +TEST_P(GCMMessageCryptographerTest, AuthSecretAffectsPRK) { + std::string first_auth_secret, second_auth_secret; + + crypto::RandBytes(base::WriteInto(&first_auth_secret, kAuthSecretSize + 1), + kAuthSecretSize); + crypto::RandBytes(base::WriteInto(&second_auth_secret, kAuthSecretSize + 1), + kAuthSecretSize); + + ASSERT_NE(cryptographer_->encryption_scheme_->DerivePseudoRandomKey( + recipient_public_key_, sender_public_key_, ecdh_shared_secret_, + first_auth_secret), + cryptographer_->encryption_scheme_->DerivePseudoRandomKey( + recipient_public_key_, sender_public_key_, ecdh_shared_secret_, + second_auth_secret)); + + std::string salt = GenerateRandomSalt(); + + // Verify that the IKM actually gets used by the transformations. + size_t first_record_size, second_record_size; + std::string first_ciphertext, second_ciphertext; + + ASSERT_TRUE(cryptographer_->Encrypt(recipient_public_key_, sender_public_key_, + ecdh_shared_secret_, first_auth_secret, + salt, kExamplePlaintext, + &first_record_size, &first_ciphertext)); + + ASSERT_TRUE(cryptographer_->Encrypt(recipient_public_key_, sender_public_key_, + ecdh_shared_secret_, second_auth_secret, + salt, kExamplePlaintext, + &second_record_size, &second_ciphertext)); + + // If the ciphertexts differ despite the same key and salt, it got used. + ASSERT_NE(first_ciphertext, second_ciphertext); + EXPECT_EQ(first_record_size, second_record_size); + + // Verify that the different ciphertexts can also be translated back to the + // plaintext content. This will fail if the auth secret isn't considered. + std::string first_plaintext, second_plaintext; + + ASSERT_TRUE(cryptographer_->Decrypt(recipient_public_key_, sender_public_key_, + ecdh_shared_secret_, first_auth_secret, + salt, first_ciphertext, first_record_size, + &first_plaintext)); + + ASSERT_TRUE(cryptographer_->Decrypt(recipient_public_key_, sender_public_key_, + ecdh_shared_secret_, second_auth_secret, + salt, second_ciphertext, + second_record_size, &second_plaintext)); + + EXPECT_EQ(kExamplePlaintext, first_plaintext); + EXPECT_EQ(kExamplePlaintext, second_plaintext); +} + +INSTANTIATE_TEST_SUITE_P( + GCMMessageCryptographerTestBase, + GCMMessageCryptographerTest, + ::testing::Values(GCMMessageCryptographer::Version::DRAFT_03, + GCMMessageCryptographer::Version::DRAFT_08)); + +class GCMMessageCryptographerTestVectorTest + : public GCMMessageCryptographerTestBase {}; + +TEST_F(GCMMessageCryptographerTestVectorTest, EncryptionVectorsDraft03) { + GCMMessageCryptographer cryptographer( + GCMMessageCryptographer::Version::DRAFT_03); + + std::string ecdh_shared_secret, auth_secret, salt, ciphertext, output; + size_t record_size = 0; + + for (size_t i = 0; i < base::size(kEncryptionTestVectorsDraft03); ++i) { + SCOPED_TRACE(i); + + ecdh_shared_secret.assign( + kEncryptionTestVectorsDraft03[i].ecdh_shared_secret, + kEncryptionTestVectorsDraft03[i].ecdh_shared_secret + + kEcdhSharedSecretSize); + + auth_secret.assign( + kEncryptionTestVectorsDraft03[i].auth_secret, + kEncryptionTestVectorsDraft03[i].auth_secret + kAuthSecretSize); + + salt.assign(kEncryptionTestVectorsDraft03[i].salt, + kEncryptionTestVectorsDraft03[i].salt + kSaltSize); + + ASSERT_TRUE(cryptographer.Encrypt(recipient_public_key_, sender_public_key_, + ecdh_shared_secret, auth_secret, salt, + kEncryptionTestVectorsDraft03[i].input, + &record_size, &ciphertext)); + + base::Base64UrlEncode(ciphertext, base::Base64UrlEncodePolicy::OMIT_PADDING, + &output); + + EXPECT_EQ(kEncryptionTestVectorsDraft03[i].record_size, record_size); + EXPECT_EQ(kEncryptionTestVectorsDraft03[i].output, output); + } +} + +TEST_F(GCMMessageCryptographerTestVectorTest, DecryptionVectorsDraft03) { + GCMMessageCryptographer cryptographer( + GCMMessageCryptographer::Version::DRAFT_03); + + std::string input, ecdh_shared_secret, auth_secret, salt, plaintext; + for (size_t i = 0; i < base::size(kDecryptionTestVectorsDraft03); ++i) { + SCOPED_TRACE(i); + + ASSERT_TRUE(base::Base64UrlDecode( + kDecryptionTestVectorsDraft03[i].input, + base::Base64UrlDecodePolicy::IGNORE_PADDING, &input)); + + ecdh_shared_secret.assign( + kDecryptionTestVectorsDraft03[i].ecdh_shared_secret, + kDecryptionTestVectorsDraft03[i].ecdh_shared_secret + + kEcdhSharedSecretSize); + + auth_secret.assign( + kDecryptionTestVectorsDraft03[i].auth_secret, + kDecryptionTestVectorsDraft03[i].auth_secret + kAuthSecretSize); + + salt.assign(kDecryptionTestVectorsDraft03[i].salt, + kDecryptionTestVectorsDraft03[i].salt + kSaltSize); + + const bool has_output = kDecryptionTestVectorsDraft03[i].output; + const bool result = cryptographer.Decrypt( + recipient_public_key_, sender_public_key_, ecdh_shared_secret, + auth_secret, salt, input, kDecryptionTestVectorsDraft03[i].record_size, + &plaintext); + + if (!has_output) { + EXPECT_FALSE(result); + continue; + } + + EXPECT_TRUE(result); + EXPECT_EQ(kDecryptionTestVectorsDraft03[i].output, plaintext); + } +} + +TEST_F(GCMMessageCryptographerTestVectorTest, EncryptionVectorsDraft08) { + GCMMessageCryptographer cryptographer( + GCMMessageCryptographer::Version::DRAFT_08); + + std::string ecdh_shared_secret, auth_secret, salt, ciphertext, output; + size_t record_size = 0; + + for (size_t i = 0; i < base::size(kEncryptionTestVectorsDraft08); ++i) { + SCOPED_TRACE(i); + + ecdh_shared_secret.assign( + kEncryptionTestVectorsDraft08[i].ecdh_shared_secret, + kEncryptionTestVectorsDraft08[i].ecdh_shared_secret + + kEcdhSharedSecretSize); + + auth_secret.assign( + kEncryptionTestVectorsDraft08[i].auth_secret, + kEncryptionTestVectorsDraft08[i].auth_secret + kAuthSecretSize); + + salt.assign(kEncryptionTestVectorsDraft08[i].salt, + kEncryptionTestVectorsDraft08[i].salt + kSaltSize); + + ASSERT_TRUE(cryptographer.Encrypt(recipient_public_key_, sender_public_key_, + ecdh_shared_secret, auth_secret, salt, + kEncryptionTestVectorsDraft08[i].input, + &record_size, &ciphertext)); + + base::Base64UrlEncode(ciphertext, base::Base64UrlEncodePolicy::OMIT_PADDING, + &output); + + EXPECT_EQ(kEncryptionTestVectorsDraft08[i].record_size, record_size); + EXPECT_EQ(kEncryptionTestVectorsDraft08[i].output, output); + } +} + +TEST_F(GCMMessageCryptographerTestVectorTest, DecryptionVectorsDraft08) { + GCMMessageCryptographer cryptographer( + GCMMessageCryptographer::Version::DRAFT_08); + + std::string input, ecdh_shared_secret, auth_secret, salt, plaintext; + + for (size_t i = 0; i < base::size(kDecryptionTestVectorsDraft08); ++i) { + SCOPED_TRACE(i); + + ASSERT_TRUE(base::Base64UrlDecode( + kDecryptionTestVectorsDraft08[i].input, + base::Base64UrlDecodePolicy::IGNORE_PADDING, &input)); + + ecdh_shared_secret.assign( + kDecryptionTestVectorsDraft08[i].ecdh_shared_secret, + kDecryptionTestVectorsDraft08[i].ecdh_shared_secret + + kEcdhSharedSecretSize); + + auth_secret.assign( + kDecryptionTestVectorsDraft08[i].auth_secret, + kDecryptionTestVectorsDraft08[i].auth_secret + kAuthSecretSize); + + salt.assign(kDecryptionTestVectorsDraft08[i].salt, + kDecryptionTestVectorsDraft08[i].salt + kSaltSize); + + const bool has_output = kDecryptionTestVectorsDraft08[i].output; + const bool result = cryptographer.Decrypt( + recipient_public_key_, sender_public_key_, ecdh_shared_secret, + auth_secret, salt, input, kDecryptionTestVectorsDraft08[i].record_size, + &plaintext); + + if (!has_output) { + EXPECT_FALSE(result); + continue; + } + + EXPECT_TRUE(result); + EXPECT_EQ(kDecryptionTestVectorsDraft08[i].output, plaintext); + } +} + +class GCMMessageCryptographerReferenceTest : public ::testing::Test {}; + +// Reference test included for the Version::DRAFT_03 implementation. +// https://tools.ietf.org/html/draft-ietf-webpush-encryption-03 +// https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-02 +TEST_F(GCMMessageCryptographerReferenceTest, ReferenceDraft03) { + // The 16-byte salt unique to the message. + const char kSalt[] = "lngarbyKfMoi9Z75xYXmkg"; + + // The 16-byte prearranged secret between the sender and receiver. + const char kAuthSecret[] = "R29vIGdvbyBnJyBqb29iIQ"; + + // The keying material used by the sender to encrypt the |kCiphertext|. + const char kSenderPrivate[] = + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgnCScek-QpEjmOOlT-rQ38nZz" + "vdPlqa00Zy0i6m2OJvahRANCAATaEQ22_OCRpvIOWeQhcbq0qrF1iddSLX1xFmFSxPOWOwmJ" + "A417CBHOGqsWGkNRvAapFwiegz6Q61rXVo_5roB1"; + const char kSenderPublicKeyUncompressed[] = + "BNoRDbb84JGm8g5Z5CFxurSqsXWJ11ItfXEWYVLE85Y7CYkDjXsIEc4aqxYaQ1G8BqkXCJ6D" + "PpDrWtdWj_mugHU"; + + // The keying material used by the recipient to decrypt the |kCiphertext|. + const char kRecipientPrivate[] = + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg9FWl15_QUQAWDaD3k3l50ZBZ" + "QJ4au27F1V4F0uLSD_OhRANCAAQhJAY8y_GdwvqItkO6BObdjafqe6LIxi4Pd6lD9ML6kU9t" + "RBFsn9HEA0HGpEDKs-IUCmDkN4pdpzWXLeB4AFEF"; + const char kRecipientPublicKeyUncompressed[] = + "BCEkBjzL8Z3C-oi2Q7oE5t2Np-p7osjGLg93qUP0wvqRT21EEWyf0cQDQcakQMqz4hQKYOQ3" + "il2nNZct4HgAUQU"; + + // The ciphertext and associated plaintext of the message. + const char kCiphertext[] = "6nqAQUME8hNqw5J3kl8cpVVJylXKYqZOeseZG8UueKpA"; + const char kPlaintext[] = "I am the walrus"; + + std::string sender_shared_secret, receiver_shared_secret; + + // Compute the shared secrets between the sender and receiver's keys. + ASSERT_NO_FATAL_FAILURE(ComputeSharedSecret( + kSenderPrivate, kRecipientPublicKeyUncompressed, &sender_shared_secret)); + ASSERT_NO_FATAL_FAILURE(ComputeSharedSecret(kRecipientPrivate, + kSenderPublicKeyUncompressed, + &receiver_shared_secret)); + + ASSERT_GT(sender_shared_secret.size(), 0u); + ASSERT_EQ(sender_shared_secret, receiver_shared_secret); + + // Decode the public keys of both parties, the auth secret and the salt. + std::string recipient_public_key, sender_public_key, auth_secret, salt; + ASSERT_TRUE(base::Base64UrlDecode(kRecipientPublicKeyUncompressed, + base::Base64UrlDecodePolicy::IGNORE_PADDING, + &recipient_public_key)); + ASSERT_TRUE(base::Base64UrlDecode(kSenderPublicKeyUncompressed, + base::Base64UrlDecodePolicy::IGNORE_PADDING, + &sender_public_key)); + ASSERT_TRUE(base::Base64UrlDecode( + kAuthSecret, base::Base64UrlDecodePolicy::IGNORE_PADDING, &auth_secret)); + ASSERT_TRUE(base::Base64UrlDecode( + kSalt, base::Base64UrlDecodePolicy::IGNORE_PADDING, &salt)); + + std::string encoded_ciphertext, ciphertext, plaintext; + size_t record_size = 0; + + // Now verify that encrypting a message with the given information yields the + // expected ciphertext given the defined input. + GCMMessageCryptographer cryptographer( + GCMMessageCryptographer::Version::DRAFT_03); + + ASSERT_TRUE(cryptographer.Encrypt(recipient_public_key, sender_public_key, + sender_shared_secret, auth_secret, salt, + kPlaintext, &record_size, &ciphertext)); + + base::Base64UrlEncode(ciphertext, base::Base64UrlEncodePolicy::OMIT_PADDING, + &encoded_ciphertext); + ASSERT_EQ(kCiphertext, encoded_ciphertext); + + // And verify that decrypting the message yields the plaintext again. + ASSERT_TRUE(cryptographer.Decrypt(recipient_public_key, sender_public_key, + sender_shared_secret, auth_secret, salt, + ciphertext, record_size, &plaintext)); + + ASSERT_EQ(kPlaintext, plaintext); +} + +// Reference test included for the Version::DRAFT_08 implementation. +// https://tools.ietf.org/html/draft-ietf-webpush-encryption-08 +// https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-07 +TEST_F(GCMMessageCryptographerReferenceTest, ReferenceDraft08) { + // The 16-byte prearranged secret between the sender and receiver. + const char kAuthSecret[] = "BTBZMqHH6r4Tts7J_aSIgg"; + + // The keying material used by the sender to encrypt the |kCiphertext|. + const char kSenderPrivate[] = + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgyfWPiYE-n46HLnH0KqZOF1fJ" + "JU3MYrct3AELtAQ-oRyhRANCAAT-M_SrDepxkU21WCP3O1SUj0EwbZIHMtu5pZpTKGSCIA5Z" + "ent7wmC6HCJ5mFgJkuk5cwAvMBKiiujwa7t45ewP"; + + // The keying material used by the recipient to decrypt the |kCiphertext|. +const char kRecipientPrivate[] = + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgq1dXpw3UpT5VOmu_cf_v6ih0" + "7Aems3njxI-JWgLcM96hRANCAAQlcbK-zf3jYFUarx7Q9M02bBHOvlVfiby3sYalMzkXMWjs" + "4uvgGFl70wR5uG48j47O1XfKWRh-kkaZDbaCAIsO"; + const char kRecipientPublicKeyUncompressed[] = + "BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcxaOzi6-AYWXvTBHm4bjyPjs7Vd8pZ" + "GH6SRpkNtoIAiw4"; + + // The plain text of the message, as well as the encrypted reference message. + const char kPlaintext[] = "When I grow up, I want to be a watermelon"; + const char kReferenceMessage[] = + "DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_" + "c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A_" + "yl95bQpu6cVPTpK4Mqgkf1CXztLVBSt2Ks3oZwbuwXPXLWyouBWLVWGNWQexSgSxsj_" + "Qulcy4a-fN"; + + std::string message; + ASSERT_TRUE(base::Base64UrlDecode(kReferenceMessage, + base::Base64UrlDecodePolicy::IGNORE_PADDING, + &message)); + + MessagePayloadParser message_parser(message); + ASSERT_TRUE(message_parser.IsValid()); + + base::StringPiece salt = message_parser.salt(); + uint32_t record_size = message_parser.record_size(); + base::StringPiece sender_public_key = message_parser.public_key(); + base::StringPiece ciphertext = message_parser.ciphertext(); + + std::string sender_shared_secret, receiver_shared_secret; + + // Compute the shared secrets between the sender and receiver's keys. + ASSERT_NO_FATAL_FAILURE(ComputeSharedSecret( + kSenderPrivate, kRecipientPublicKeyUncompressed, &sender_shared_secret)); + + // Compute the shared secret based on the sender's public key, which isn't a + // constant but instead is included in the message's binary header. + std::string recipient_private_key; + ASSERT_TRUE(base::Base64UrlDecode(kRecipientPrivate, + base::Base64UrlDecodePolicy::IGNORE_PADDING, + &recipient_private_key)); + ASSERT_NO_FATAL_FAILURE(ComputeSharedP256SecretFromPrivateKeyStr( + recipient_private_key, sender_public_key, + &receiver_shared_secret)); + + ASSERT_GT(sender_shared_secret.size(), 0u); + ASSERT_EQ(sender_shared_secret, receiver_shared_secret); + + // Decode the public keys of both parties and the auth secret. + std::string recipient_public_key, auth_secret; + ASSERT_TRUE(base::Base64UrlDecode(kRecipientPublicKeyUncompressed, + base::Base64UrlDecodePolicy::IGNORE_PADDING, + &recipient_public_key)); + ASSERT_TRUE(base::Base64UrlDecode( + kAuthSecret, base::Base64UrlDecodePolicy::IGNORE_PADDING, &auth_secret)); + + // Attempt to decrypt the message using a GCMMessageCryptographer for this + // version of the draft, and then re-encrypt it agian to make sure it matches. + GCMMessageCryptographer cryptographer( + GCMMessageCryptographer::Version::DRAFT_08); + + std::string plaintext; + + ASSERT_TRUE(cryptographer.Decrypt(recipient_public_key, sender_public_key, + sender_shared_secret, auth_secret, salt, + ciphertext, record_size, &plaintext)); + ASSERT_EQ(kPlaintext, plaintext); + + size_t record_size2; + std::string ciphertext2; + + ASSERT_TRUE(cryptographer.Encrypt(recipient_public_key, sender_public_key, + sender_shared_secret, auth_secret, salt, + kPlaintext, &record_size2, &ciphertext2)); + + EXPECT_GE(record_size2, record_size); + EXPECT_EQ(ciphertext2, ciphertext); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/crypto/message_payload_parser.cc b/chromium/components/gcm_driver/crypto/message_payload_parser.cc new file mode 100644 index 00000000000..175c2d8ebd8 --- /dev/null +++ b/chromium/components/gcm_driver/crypto/message_payload_parser.cc @@ -0,0 +1,77 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/crypto/message_payload_parser.h" + +#include "base/big_endian.h" +#include "base/strings/string_piece.h" +#include "components/gcm_driver/crypto/gcm_decryption_result.h" + +namespace gcm { + +namespace { + +// Size, in bytes, of the salt included in the message header. +constexpr size_t kSaltSize = 16; + +// Size, in bytes, of the uncompressed point included in the message header. +constexpr size_t kUncompressedPointSize = 65; + +// Size, in bytes, of the smallest allowable record_size value. +constexpr size_t kMinimumRecordSize = 18; + +// Size, in bytes, of an empty message with the minimum amount of padding. +constexpr size_t kMinimumMessageSize = + kSaltSize + sizeof(uint32_t) + sizeof(uint8_t) + kUncompressedPointSize + + kMinimumRecordSize; + +} // namespace + +MessagePayloadParser::MessagePayloadParser(base::StringPiece message) { + if (message.size() < kMinimumMessageSize) { + failure_reason_ = GCMDecryptionResult::INVALID_BINARY_HEADER_PAYLOAD_LENGTH; + return; + } + + salt_ = std::string(message.substr(0, kSaltSize)); + message.remove_prefix(kSaltSize); + + base::ReadBigEndian(reinterpret_cast(message.data()), + &record_size_); + message.remove_prefix(sizeof(record_size_)); + + if (record_size_ < kMinimumRecordSize) { + failure_reason_ = GCMDecryptionResult::INVALID_BINARY_HEADER_RECORD_SIZE; + return; + } + + uint8_t public_key_length; + base::ReadBigEndian(reinterpret_cast(message.data()), + &public_key_length); + message.remove_prefix(sizeof(public_key_length)); + + if (public_key_length != kUncompressedPointSize) { + failure_reason_ = + GCMDecryptionResult::INVALID_BINARY_HEADER_PUBLIC_KEY_LENGTH; + return; + } + + if (message[0] != 0x04) { + failure_reason_ = + GCMDecryptionResult::INVALID_BINARY_HEADER_PUBLIC_KEY_FORMAT; + return; + } + + public_key_ = std::string(message.substr(0, kUncompressedPointSize)); + message.remove_prefix(kUncompressedPointSize); + + ciphertext_ = std::string(message); + DCHECK_GE(ciphertext_.size(), kMinimumRecordSize); + + is_valid_ = true; +} + +MessagePayloadParser::~MessagePayloadParser() = default; + +} // namespace gcm diff --git a/chromium/components/gcm_driver/crypto/message_payload_parser.h b/chromium/components/gcm_driver/crypto/message_payload_parser.h new file mode 100644 index 00000000000..2810385e048 --- /dev/null +++ b/chromium/components/gcm_driver/crypto/message_payload_parser.h @@ -0,0 +1,97 @@ +// Copyright 2017 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 COMPONENTS_GCM_DRIVER_CRYPTO_MESSAGE_PAYLOAD_PARSER_H_ +#define COMPONENTS_GCM_DRIVER_CRYPTO_MESSAGE_PAYLOAD_PARSER_H_ + +#include + +#include "base/check.h" +#include "base/strings/string_piece.h" +#include "third_party/abseil-cpp/absl/types/optional.h" + +namespace gcm { + +enum class GCMDecryptionResult; + +// Parses and validates the binary message payload included in messages that +// are encrypted per draft-ietf-webpush-encryption-08: +// +// https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-08#section-2.1 +// +// In summary, such messages start with a binary header block that includes the +// parameters needed to decrypt the content, other than the key. All content +// following this binary header is considered the ciphertext. +// +// +-----------+--------+-----------+-----------------+ +// | salt (16) | rs (4) | idlen (1) | public_key (65) | +// +-----------+--------+-----------+-----------------+ +// +// Specific to Web Push encryption, the `public_key` parameter of this header +// must be set to the ECDH public key of the sender. This is a point on the +// P-256 elliptic curve in uncompressed form, 65 bytes long starting with 0x04. +// +// https://tools.ietf.org/html/draft-ietf-webpush-encryption-08#section-3.1 +class MessagePayloadParser { + public: + explicit MessagePayloadParser(base::StringPiece message); + + MessagePayloadParser(const MessagePayloadParser&) = delete; + MessagePayloadParser& operator=(const MessagePayloadParser&) = delete; + + ~MessagePayloadParser(); + + // Returns whether the parser represents a valid message. + bool IsValid() const { return is_valid_; } + + // Returns the failure reason when the given payload could not be parsed. Must + // only be called when IsValid() returns false. + GCMDecryptionResult GetFailureReason() const { + DCHECK(failure_reason_.has_value()); + return failure_reason_.value(); + } + + // Returns the 16-byte long salt for the message. Must only be called after + // validity of the message has been verified. + const std::string& salt() const { + CHECK(is_valid_); + return salt_; + } + + // Returns the record size for the message. Must only be called after validity + // of the message has been verified. + uint32_t record_size() const { + CHECK(is_valid_); + return record_size_; + } + + // Returns the sender's ECDH public key for the message. This will be a point + // on the P-256 elliptic curve in uncompressed form. Must only be called after + // validity of the message has been verified. + const std::string& public_key() const { + CHECK(is_valid_); + return public_key_; + } + + // Returns the ciphertext for the message. This will be at least the size of + // a single record, which is 18 octets. Must only be called after validity of + // the message has been verified. + const std::string& ciphertext() const { + CHECK(is_valid_); + return ciphertext_; + } + + private: + bool is_valid_ = false; + absl::optional failure_reason_; + + std::string salt_; + uint32_t record_size_ = 0; + std::string public_key_; + std::string ciphertext_; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_CRYPTO_MESSAGE_PAYLOAD_PARSER_H_ diff --git a/chromium/components/gcm_driver/crypto/message_payload_parser_unittest.cc b/chromium/components/gcm_driver/crypto/message_payload_parser_unittest.cc new file mode 100644 index 00000000000..f9d72032a62 --- /dev/null +++ b/chromium/components/gcm_driver/crypto/message_payload_parser_unittest.cc @@ -0,0 +1,124 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/crypto/message_payload_parser.h" + +#include "base/big_endian.h" +#include "base/cxx17_backports.h" +#include "components/gcm_driver/crypto/gcm_decryption_result.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { + +namespace { + +constexpr size_t kSaltSize = 16; +constexpr size_t kPublicKeySize = 65; +constexpr size_t kCiphertextSize = 18; + +const uint8_t kValidMessage[] = { + // salt (16 bytes, kSaltSize) + 0x59, 0xFD, 0x35, 0x97, 0x3B, 0xF3, 0x66, 0xA7, 0xEB, 0x8D, 0x44, 0x1E, + 0xCB, 0x4D, 0xFC, 0xD8, + // rs (4 bytes, in network byte order) + 0x00, 0x00, 0x00, 0x12, + // idlen (1 byte) + 0x41, + // public key (65 bytes, kPublicKeySize, must start with 0x04) + 0x04, 0x35, 0x02, 0x67, 0xB9, 0x10, 0x8F, 0x9B, 0xF1, 0x85, 0xF5, 0x1B, + 0xD7, 0xA4, 0xEF, 0xBD, 0x28, 0xB3, 0x11, 0x40, 0xBA, 0xD0, 0xEE, 0xB2, + 0x97, 0xDA, 0x6A, 0x93, 0x2D, 0x26, 0x45, 0xBD, 0xB2, 0x9A, 0x9F, 0xB8, + 0x19, 0xD8, 0x21, 0x6F, 0x66, 0xE3, 0xF6, 0x0B, 0x74, 0xB2, 0x28, 0x38, + 0xDC, 0xA7, 0x8A, 0x58, 0x0D, 0x56, 0x47, 0x3E, 0xD0, 0x5B, 0x5C, 0x93, + 0x4E, 0xB3, 0x89, 0x87, 0x64, + // payload (18 bytes, kCiphertextSize) + 0x3F, 0xD8, 0x95, 0x2C, 0xA2, 0x11, 0xBD, 0x7B, 0x57, 0xB2, 0x00, 0xBD, + 0x57, 0x68, 0x3F, 0xF0, 0x14, 0x57}; + +static_assert(base::size(kValidMessage) == 104, + "The smallest valid message is 104 bytes in size."); + +// Creates an std::string for the |kValidMessage| constant. +std::string CreateMessageString() { + return std::string(reinterpret_cast(kValidMessage), + base::size(kValidMessage)); +} + +TEST(MessagePayloadParserTest, ValidMessage) { + MessagePayloadParser parser(CreateMessageString()); + ASSERT_TRUE(parser.IsValid()); + + const uint8_t* salt = kValidMessage; + + ASSERT_EQ(parser.salt().size(), kSaltSize); + EXPECT_EQ(parser.salt(), std::string(salt, salt + kSaltSize)); + + ASSERT_EQ(parser.record_size(), 18u); + + const uint8_t* public_key = + kValidMessage + kSaltSize + sizeof(uint32_t) + sizeof(uint8_t); + + ASSERT_EQ(parser.public_key().size(), kPublicKeySize); + EXPECT_EQ(parser.public_key(), + std::string(public_key, public_key + kPublicKeySize)); + + const uint8_t* ciphertext = kValidMessage + kSaltSize + sizeof(uint32_t) + + sizeof(uint8_t) + kPublicKeySize; + + ASSERT_EQ(parser.ciphertext().size(), kCiphertextSize); + EXPECT_EQ(parser.ciphertext(), + std::string(ciphertext, ciphertext + kCiphertextSize)); +} + +TEST(MessagePayloadParserTest, MinimumMessageSize) { + std::string message = CreateMessageString(); + message.resize(base::size(kValidMessage) / 2); + + MessagePayloadParser parser(message); + EXPECT_FALSE(parser.IsValid()); + EXPECT_EQ(parser.GetFailureReason(), + GCMDecryptionResult::INVALID_BINARY_HEADER_PAYLOAD_LENGTH); +} + +TEST(MessagePayloadParserTest, MinimumRecordSize) { + std::string message = CreateMessageString(); + + uint32_t invalid_record_size = 11; + base::WriteBigEndian(&message[0] + 16 /* salt */, invalid_record_size); + + MessagePayloadParser parser(message); + EXPECT_FALSE(parser.IsValid()); + EXPECT_EQ(parser.GetFailureReason(), + GCMDecryptionResult::INVALID_BINARY_HEADER_RECORD_SIZE); +} + +TEST(MessagePayloadParserTest, InvalidPublicKeyLength) { + std::string message = CreateMessageString(); + + uint8_t invalid_public_key_size = 42; + base::WriteBigEndian(&message[0] + 16 /* salt */ + 4 /* rs */, + invalid_public_key_size); + + MessagePayloadParser parser(message); + EXPECT_FALSE(parser.IsValid()); + EXPECT_EQ(parser.GetFailureReason(), + GCMDecryptionResult::INVALID_BINARY_HEADER_PUBLIC_KEY_LENGTH); +} + +TEST(MessagePayloadParserTest, InvalidPublicKeyFormat) { + std::string message = CreateMessageString(); + + uint8_t invalid_p256_uncompressed_key_prefix = 0x42; + base::WriteBigEndian(&message[0] + 16 /* salt */ + 4 /* rs */ + 1 /* idlen */, + invalid_p256_uncompressed_key_prefix); + + MessagePayloadParser parser(message); + EXPECT_FALSE(parser.IsValid()); + EXPECT_EQ(parser.GetFailureReason(), + GCMDecryptionResult::INVALID_BINARY_HEADER_PUBLIC_KEY_FORMAT); +} + +} // namespace + +} // namespace gcm diff --git a/chromium/components/gcm_driver/crypto/p256_key_util.cc b/chromium/components/gcm_driver/crypto/p256_key_util.cc new file mode 100644 index 00000000000..5954d20186f --- /dev/null +++ b/chromium/components/gcm_driver/crypto/p256_key_util.cc @@ -0,0 +1,95 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/crypto/p256_key_util.h" + +#include +#include + +#include +#include + +#include "base/logging.h" +#include "base/strings/string_util.h" +#include "crypto/ec_private_key.h" +#include "third_party/boringssl/src/include/openssl/ec.h" +#include "third_party/boringssl/src/include/openssl/ecdh.h" +#include "third_party/boringssl/src/include/openssl/evp.h" + +namespace gcm { + +namespace { + +// A P-256 field element consists of 32 bytes. +const size_t kFieldBytes = 32; + +// A P-256 point in uncompressed form consists of 0x04 (to denote that the point +// is uncompressed per SEC1 2.3.3) followed by two, 32-byte field elements. +const size_t kUncompressedPointBytes = 1 + 2 * kFieldBytes; + +} // namespace + +bool GetRawPublicKey(const crypto::ECPrivateKey& key, std::string* public_key) { + DCHECK(public_key); + std::string candidate_public_key; + + // ECPrivateKey::ExportRawPublicKey() returns the EC point in the uncompressed + // point format. + if (!key.ExportRawPublicKey(&candidate_public_key) || + candidate_public_key.size() != kUncompressedPointBytes) { + DLOG(ERROR) << "Unable to export the public key."; + return false; + } + public_key->erase(); + public_key->reserve(kUncompressedPointBytes); + public_key->append(candidate_public_key); + return true; +} + +// TODO(peter): Get rid of this once all key management code has been updated +// to use ECPrivateKey instead of std::string. +bool GetRawPrivateKey(const crypto::ECPrivateKey& key, + std::string* private_key) { + DCHECK(private_key); + std::vector private_key_vector; + if (!key.ExportPrivateKey(&private_key_vector)) + return false; + private_key->assign(private_key_vector.begin(), private_key_vector.end()); + return true; +} + +bool ComputeSharedP256Secret(crypto::ECPrivateKey& key, + const base::StringPiece& peer_public_key, + std::string* out_shared_secret) { + DCHECK(out_shared_secret); + + EC_KEY* ec_private_key = EVP_PKEY_get0_EC_KEY(key.key()); + if (!ec_private_key || !EC_KEY_check_key(ec_private_key)) { + DLOG(ERROR) << "The private key is invalid."; + return false; + } + + bssl::UniquePtr point( + EC_POINT_new(EC_KEY_get0_group(ec_private_key))); + + if (!point || !EC_POINT_oct2point( + EC_KEY_get0_group(ec_private_key), point.get(), + reinterpret_cast(peer_public_key.data()), + peer_public_key.size(), nullptr)) { + DLOG(ERROR) << "Can't convert peer public value to curve point."; + return false; + } + + uint8_t result[kFieldBytes]; + if (ECDH_compute_key(result, sizeof(result), point.get(), ec_private_key, + nullptr) != sizeof(result)) { + DLOG(ERROR) << "Unable to compute the ECDH shared secret."; + return false; + } + + out_shared_secret->assign(reinterpret_cast(result), sizeof(result)); + return true; +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/crypto/p256_key_util.h b/chromium/components/gcm_driver/crypto/p256_key_util.h new file mode 100644 index 00000000000..05bd36c6e9c --- /dev/null +++ b/chromium/components/gcm_driver/crypto/p256_key_util.h @@ -0,0 +1,43 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_GCM_DRIVER_CRYPTO_P256_KEY_UTIL_H_ +#define COMPONENTS_GCM_DRIVER_CRYPTO_P256_KEY_UTIL_H_ + +#include + +#include "base/compiler_specific.h" +#include "base/strings/string_piece.h" + +namespace crypto { +class ECPrivateKey; +} + +namespace gcm { + +// Writes the public key associated with the |key| to |*public_key| in +// uncompressed point format. That is, a 65-octet sequence that starts with a +// 0x04 octet. Returns whether the public key could be extracted successfully. +bool GetRawPublicKey(const crypto::ECPrivateKey& key, + std::string* public_key) WARN_UNUSED_RESULT; + +// Writes the private key associated with the |key| to |*private_key| as a PKCS +// #8 PrivateKeyInfo block. Returns whether the private key could be extracted +// successfully. +bool GetRawPrivateKey(const crypto::ECPrivateKey& key, + std::string* private_key) WARN_UNUSED_RESULT; + +// Computes the shared secret between |key| and |peer_public_key|.The +// |peer_public_key| must be an octet string in uncompressed form per +// SEC1 2.3.3. +// +// Returns whether the secret could be computed, and was written to the out +// argument. +bool ComputeSharedP256Secret(crypto::ECPrivateKey& key, + const base::StringPiece& peer_public_key, + std::string* out_shared_secret) WARN_UNUSED_RESULT; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_CRYPTO_P256_KEY_UTIL_H_ diff --git a/chromium/components/gcm_driver/crypto/p256_key_util_unittest.cc b/chromium/components/gcm_driver/crypto/p256_key_util_unittest.cc new file mode 100644 index 00000000000..27c01758984 --- /dev/null +++ b/chromium/components/gcm_driver/crypto/p256_key_util_unittest.cc @@ -0,0 +1,158 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/crypto/p256_key_util.h" + +#include + +#include + +#include "base/base64.h" +#include "crypto/ec_private_key.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { + +namespace { + +// A P-256 point in uncompressed form consists of 0x04 (to denote that the point +// is uncompressed per SEC1 2.3.3) followed by two, 32-byte field elements. +const size_t kUncompressedPointBytes = 1 + 2 * 32; + +// Precomputed private/public key-pair. Keys are stored on disk, so previously +// created values must continue to be usable for computing shared secrets. +const char kBobPrivateKey[] = + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgS8wRbDOWz0lKExvIVQiRKtPAP8" + "dgHUHAw5gyOd5d4jKhRANCAARZb49Va5MD/KcWtc0oiWc2e8njBDtQzj0mzcOl1fDSt16Pvu6p" + "fTU3MTWnImDNnkPxtXm58K7Uax8jFxA4TeXJ"; +const char kBobPublicKey[] = + "BFlvj1VrkwP8pxa1zSiJZzZ7yeMEO1DOPSbNw6XV8NK3Xo++7ql9NTcxNaciYM2eQ/G1ebnwrt" + "RrHyMXEDhN5ck="; + +const char kCarolPrivateKey[] = + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgmqy/ighwCm+RBP4Kct3rzaFEJ" + "CZhokknro3KYsriurChRANCAAScr5sTsqmlP8SqiI+8fzxVLr1pby2HyG5mC5J0WSpYVIpMNS" + "C16k1qcxqOJ4fiv8Ya47FYw/MIS7X1kobK27mP"; +const char kCarolPublicKey[] = + "BJyvmxOyqaU/xKqIj7x/PFUuvWlvLYfIbmYLknRZKlhUikw1ILXqTWpzGo4nh+K/xhrjsVjD8" + "whLtfWShsrbuY8="; + +// The shared secret between Bob and Carol. +const char kBobCarolSharedSecret[] = + "AUNmKkgLLVLf6j/VnA9Eg1CiPSPfQHGirQj79n4vOyw="; + +TEST(P256KeyUtilTest, UniqueKeyPairGeneration) { + // Canary for determining that no key repetitions are found in few iterations. + std::set seen_private_keys; + std::set seen_public_keys; + + for (int iteration = 0; iteration < 10; ++iteration) { + SCOPED_TRACE(iteration); + + std::string private_key, public_key; + std::unique_ptr key(crypto::ECPrivateKey::Create()); + ASSERT_TRUE(key); + ASSERT_TRUE(GetRawPublicKey(*key, &public_key)); + ASSERT_TRUE(GetRawPrivateKey(*key, &private_key)); + + EXPECT_NE(private_key, public_key); + EXPECT_GT(private_key.size(), 0u); + EXPECT_EQ(public_key.size(), kUncompressedPointBytes); + + EXPECT_EQ(0u, seen_private_keys.count(private_key)); + EXPECT_EQ(0u, seen_public_keys.count(public_key)); + + seen_private_keys.insert(private_key); + seen_public_keys.insert(public_key); + } +} + +TEST(P256KeyUtilTest, SharedSecretCalculation) { + std::unique_ptr bob_key = + crypto::ECPrivateKey::Create(); + std::unique_ptr alice_key = + crypto::ECPrivateKey::Create(); + + std::string alice_public_key, bob_public_key, alice_private_key, + bob_private_key; + ASSERT_TRUE(GetRawPublicKey(*bob_key, &bob_public_key)); + ASSERT_TRUE(GetRawPublicKey(*alice_key, &alice_public_key)); + ASSERT_TRUE(GetRawPrivateKey(*bob_key, &bob_private_key)); + ASSERT_TRUE(GetRawPrivateKey(*alice_key, &alice_private_key)); + ASSERT_NE(bob_public_key, alice_public_key); + ASSERT_NE(bob_private_key, alice_private_key); + + std::string bob_shared_secret, alice_shared_secret; + ASSERT_TRUE( + ComputeSharedP256Secret(*bob_key, alice_public_key, &bob_shared_secret)); + ASSERT_TRUE(ComputeSharedP256Secret(*alice_key, bob_public_key, + &alice_shared_secret)); + + EXPECT_GT(bob_shared_secret.size(), 0u); + EXPECT_EQ(bob_shared_secret, alice_shared_secret); + + std::string unused_shared_secret; + + // Empty and too short peer public values should be considered invalid. + ASSERT_FALSE(ComputeSharedP256Secret(*bob_key, "", &unused_shared_secret)); + ASSERT_FALSE(ComputeSharedP256Secret(*bob_key, bob_public_key.substr(1), + &unused_shared_secret)); +} + +TEST(P256KeyUtilTest, SharedSecretWithPreExistingKey) { + std::string bob_private_key, bob_public_key; + ASSERT_TRUE(base::Base64Decode(kBobPrivateKey, &bob_private_key)); + ASSERT_TRUE(base::Base64Decode(kBobPublicKey, &bob_public_key)); + + std::vector bob_private_key_vec( + bob_private_key.begin(), bob_private_key.end()); + std::unique_ptr bob_key = + crypto::ECPrivateKey::CreateFromPrivateKeyInfo(bob_private_key_vec); + ASSERT_TRUE(bob_key); + // First verify against a newly created, ephemeral key-pair. + std::unique_ptr alice_key( + crypto::ECPrivateKey::Create()); + std::string alice_public_key; + ASSERT_TRUE(GetRawPublicKey(*alice_key, &alice_public_key)); + std::string bob_shared_secret, alice_shared_secret; + + ASSERT_TRUE(ComputeSharedP256Secret(*bob_key, alice_public_key, + &bob_shared_secret)); + ASSERT_TRUE(ComputeSharedP256Secret(*alice_key, bob_public_key, + &alice_shared_secret)); + + EXPECT_GT(bob_shared_secret.size(), 0u); + EXPECT_EQ(bob_shared_secret, alice_shared_secret); + + std::string carol_private_key, carol_public_key; + ASSERT_TRUE(base::Base64Decode(kCarolPrivateKey, &carol_private_key)); + ASSERT_TRUE(base::Base64Decode(kCarolPublicKey, &carol_public_key)); + + std::vector carol_private_key_vec( + carol_private_key.begin(), carol_private_key.end()); + std::unique_ptr carol_key = + crypto::ECPrivateKey::CreateFromPrivateKeyInfo(carol_private_key_vec); + ASSERT_TRUE(carol_key); + bob_shared_secret.clear(); + std::string carol_shared_secret; + + // Then verify against another stored key-pair and shared secret. + ASSERT_TRUE(ComputeSharedP256Secret(*bob_key, carol_public_key, + &bob_shared_secret)); + ASSERT_TRUE(ComputeSharedP256Secret(*carol_key, bob_public_key, + &carol_shared_secret)); + + EXPECT_GT(carol_shared_secret.size(), 0u); + EXPECT_EQ(carol_shared_secret, bob_shared_secret); + + std::string bob_carol_shared_secret; + ASSERT_TRUE(base::Base64Decode( + kBobCarolSharedSecret, &bob_carol_shared_secret)); + + EXPECT_EQ(carol_shared_secret, bob_carol_shared_secret); +} + +} // namespace + +} // namespace gcm diff --git a/chromium/components/gcm_driver/crypto/proto/gcm_encryption_data.proto b/chromium/components/gcm_driver/crypto/proto/gcm_encryption_data.proto new file mode 100644 index 00000000000..d4ceb230894 --- /dev/null +++ b/chromium/components/gcm_driver/crypto/proto/gcm_encryption_data.proto @@ -0,0 +1,58 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +syntax = "proto2"; + +option optimize_for = LITE_RUNTIME; + +package gcm; + +// Stores a public/private key-pair. +// Next tag: 5 +message KeyPair { + // The type of key used for key agreement. Currently only the ECDH key + // agreement scheme is supported, using NIST P-256. + enum KeyType { + ECDH_P256 = 0; + } + + required KeyType type = 1; + + // The private key matching the size requirements of |type|. + optional bytes private_key = 2; + + reserved 3; // public_key_x509, now deleted. + + // The public key as an uncompressed EC point according to SEC 2.3.3. + optional bytes public_key = 4; +} + +// Stores a vector of public/private key-pairs associated with an app id and +// optionally the authorized entity of an instance id token. +// +// In the current implementation, each (app_id, authorized_entity) pair will +// have a single encryption key-pair associated with it at most. The message +// allows for multiple key pairs in case we need to force-cycle all keys, +// allowing the old keys to remain valid for a period of time enabling the web +// app to update. +// +// Next tag: 6 +message EncryptionData { + // The app id to whom this encryption data belongs. + required string app_id = 1; + + // The sender id of the instance id token that this encryption data belongs + // to. Must not be empty. Must be omitted for non-InstanceID registrations. + optional string authorized_entity = 4; + + // DEPRECATED: The actual public/private key-pairs. + repeated KeyPair keys = 2; + + // P-256 private key stored as a PKCS #8 PrivateKeyInfo block. + optional string private_key = 5; + + // The authentication secret associated with the subscription. Must be a + // cryptographically secure number of at least 12 bytes. + optional bytes auth_secret = 3; +} diff --git a/chromium/components/gcm_driver/fake_gcm_app_handler.cc b/chromium/components/gcm_driver/fake_gcm_app_handler.cc new file mode 100644 index 00000000000..3eabaa4ed44 --- /dev/null +++ b/chromium/components/gcm_driver/fake_gcm_app_handler.cc @@ -0,0 +1,85 @@ +// 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 "components/gcm_driver/fake_gcm_app_handler.h" + +#include + +#include "base/run_loop.h" + +namespace gcm { + +FakeGCMAppHandler::FakeGCMAppHandler() : received_event_(NO_EVENT) {} + +FakeGCMAppHandler::~FakeGCMAppHandler() = default; + +void FakeGCMAppHandler::WaitForNotification() { + run_loop_ = std::make_unique(); + run_loop_->Run(); + run_loop_.reset(); +} + +void FakeGCMAppHandler::ShutdownHandler() {} + +void FakeGCMAppHandler::OnStoreReset() {} + +void FakeGCMAppHandler::OnMessage(const std::string& app_id, + const IncomingMessage& message) { + ClearResults(); + received_event_ = MESSAGE_EVENT; + app_id_ = app_id; + message_ = message; + if (run_loop_) + run_loop_->Quit(); +} + +void FakeGCMAppHandler::OnMessagesDeleted(const std::string& app_id) { + ClearResults(); + received_event_ = MESSAGES_DELETED_EVENT; + app_id_ = app_id; + if (run_loop_) + run_loop_->Quit(); +} + +void FakeGCMAppHandler::OnSendError( + const std::string& app_id, + const GCMClient::SendErrorDetails& send_error_details) { + ClearResults(); + received_event_ = SEND_ERROR_EVENT; + app_id_ = app_id; + send_error_details_ = send_error_details; + if (run_loop_) + run_loop_->Quit(); +} + +void FakeGCMAppHandler::OnSendAcknowledged( + const std::string& app_id, + const std::string& message_id) { + ClearResults(); + app_id_ = app_id; + acked_message_id_ = message_id; + if (run_loop_) + run_loop_->Quit(); +} + +void FakeGCMAppHandler::OnMessageDecryptionFailed( + const std::string& app_id, + const std::string& message_id, + const std::string& error_message) { + ClearResults(); + received_event_ = DECRYPTION_FAILED_EVENT; + app_id_ = app_id; + if (run_loop_) + run_loop_->Quit(); +} + +void FakeGCMAppHandler::ClearResults() { + received_event_ = NO_EVENT; + app_id_.clear(); + acked_message_id_.clear(); + message_ = IncomingMessage(); + send_error_details_ = GCMClient::SendErrorDetails(); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/fake_gcm_app_handler.h b/chromium/components/gcm_driver/fake_gcm_app_handler.h new file mode 100644 index 00000000000..a7c7edde29d --- /dev/null +++ b/chromium/components/gcm_driver/fake_gcm_app_handler.h @@ -0,0 +1,75 @@ +// 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 COMPONENTS_GCM_DRIVER_FAKE_GCM_APP_HANDLER_H_ +#define COMPONENTS_GCM_DRIVER_FAKE_GCM_APP_HANDLER_H_ + +#include + +#include "base/compiler_specific.h" +#include "components/gcm_driver/gcm_app_handler.h" + +namespace base { +class RunLoop; +} + +namespace gcm { + +class FakeGCMAppHandler : public GCMAppHandler { + public: + enum Event { + NO_EVENT, + MESSAGE_EVENT, + MESSAGES_DELETED_EVENT, + SEND_ERROR_EVENT, + DECRYPTION_FAILED_EVENT, + }; + + FakeGCMAppHandler(); + + FakeGCMAppHandler(const FakeGCMAppHandler&) = delete; + FakeGCMAppHandler& operator=(const FakeGCMAppHandler&) = delete; + + ~FakeGCMAppHandler() override; + + const Event& received_event() const { return received_event_; } + const std::string& app_id() const { return app_id_; } + const std::string& acked_message_id() const { return acked_message_id_; } + const IncomingMessage& message() const { return message_; } + const GCMClient::SendErrorDetails& send_error_details() const { + return send_error_details_; + } + + void WaitForNotification(); + + // GCMAppHandler implementation. + void ShutdownHandler() override; + void OnStoreReset() override; + void OnMessage(const std::string& app_id, + const IncomingMessage& message) override; + void OnMessagesDeleted(const std::string& app_id) override; + void OnSendError( + const std::string& app_id, + const GCMClient::SendErrorDetails& send_error_details) override; + void OnMessageDecryptionFailed(const std::string& app_id, + const std::string& message_id, + const std::string& error_message) override; + void OnSendAcknowledged(const std::string& app_id, + const std::string& message_id) override; + + private: + void ClearResults(); + + std::unique_ptr run_loop_; + + Event received_event_; + std::string app_id_; + std::string acked_message_id_; + IncomingMessage message_; + GCMClient::SendErrorDetails send_error_details_; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_FAKE_GCM_APP_HANDLER_H_ diff --git a/chromium/components/gcm_driver/fake_gcm_client.cc b/chromium/components/gcm_driver/fake_gcm_client.cc new file mode 100644 index 00000000000..69d6d8693a6 --- /dev/null +++ b/chromium/components/gcm_driver/fake_gcm_client.cc @@ -0,0 +1,335 @@ +// 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 "components/gcm_driver/fake_gcm_client.h" + +#include + +#include + +#include "base/bind.h" +#include "base/check.h" +#include "base/location.h" +#include "base/strings/string_number_conversions.h" +#include "base/sys_byteorder.h" +#include "base/task/sequenced_task_runner.h" +#include "base/task/single_thread_task_runner.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "base/timer/timer.h" +#include "google_apis/gcm/base/encryptor.h" +#include "google_apis/gcm/engine/account_mapping.h" +#include "net/base/ip_endpoint.h" + +namespace gcm { + +// static +std::string FakeGCMClient::GenerateGCMRegistrationID( + const std::vector& sender_ids) { + // GCMService normalizes the sender IDs by making them sorted. + std::vector normalized_sender_ids = sender_ids; + std::sort(normalized_sender_ids.begin(), normalized_sender_ids.end()); + + // Simulate the registration_id by concaternating all sender IDs. + // Set registration_id to empty to denote an error if sender_ids contains a + // hint. + std::string registration_id; + if (sender_ids.size() != 1 || + sender_ids[0].find("error") == std::string::npos) { + for (size_t i = 0; i < normalized_sender_ids.size(); ++i) { + if (i > 0) + registration_id += ","; + registration_id += normalized_sender_ids[i]; + } + } + return registration_id; +} + +// static +std::string FakeGCMClient::GenerateInstanceIDToken( + const std::string& authorized_entity, const std::string& scope) { + if (authorized_entity.find("error") != std::string::npos) + return ""; + std::string token(authorized_entity); + token += ","; + token += scope; + return token; +} + +FakeGCMClient::FakeGCMClient( + const scoped_refptr& ui_thread, + const scoped_refptr& io_thread) + : delegate_(nullptr), + started_(false), + start_mode_(DELAYED_START), + start_mode_overridding_(RESPECT_START_MODE), + ui_thread_(ui_thread), + io_thread_(io_thread) {} + +FakeGCMClient::~FakeGCMClient() { +} + +void FakeGCMClient::Initialize( + const ChromeBuildInfo& chrome_build_info, + const base::FilePath& store_path, + bool remove_account_mappings_with_email_key, + const scoped_refptr& blocking_task_runner, + scoped_refptr io_task_runner, + base::RepeatingCallback)> + get_socket_factory_callback, + const scoped_refptr& url_loader_factory, + network::NetworkConnectionTracker* network_connection_tracker, + std::unique_ptr encryptor, + Delegate* delegate) { + product_category_for_subtypes_ = + chrome_build_info.product_category_for_subtypes; + delegate_ = delegate; +} + +void FakeGCMClient::Start(StartMode start_mode) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + if (started_) + return; + + if (start_mode == IMMEDIATE_START) + start_mode_ = IMMEDIATE_START; + if (start_mode_ == DELAYED_START || + start_mode_overridding_ == FORCE_TO_ALWAYS_DELAY_START_GCM) { + return; + } + + DoStart(); +} + +void FakeGCMClient::DoStart() { + started_ = true; + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(&FakeGCMClient::Started, weak_ptr_factory_.GetWeakPtr())); +} + +void FakeGCMClient::Stop() { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + started_ = false; + delegate_->OnDisconnected(); +} + +void FakeGCMClient::Register( + scoped_refptr registration_info) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + std::string registration_id; + + GCMRegistrationInfo* gcm_registration_info = + GCMRegistrationInfo::FromRegistrationInfo(registration_info.get()); + if (gcm_registration_info) { + registration_id = GenerateGCMRegistrationID( + gcm_registration_info->sender_ids); + } + + InstanceIDTokenInfo* instance_id_token_info = + InstanceIDTokenInfo::FromRegistrationInfo(registration_info.get()); + if (instance_id_token_info) { + registration_id = GenerateInstanceIDToken( + instance_id_token_info->authorized_entity, + instance_id_token_info->scope); + } + + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(&FakeGCMClient::RegisterFinished, + weak_ptr_factory_.GetWeakPtr(), + std::move(registration_info), registration_id)); +} + +bool FakeGCMClient::ValidateRegistration( + scoped_refptr registration_info, + const std::string& registration_id) { + return true; +} + +void FakeGCMClient::Unregister( + scoped_refptr registration_info) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(&FakeGCMClient::UnregisterFinished, + weak_ptr_factory_.GetWeakPtr(), registration_info)); +} + +void FakeGCMClient::Send(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(&FakeGCMClient::SendFinished, + weak_ptr_factory_.GetWeakPtr(), app_id, message)); +} + +void FakeGCMClient::RecordDecryptionFailure(const std::string& app_id, + GCMDecryptionResult result) { + recorder_.RecordDecryptionFailure(app_id, result); +} + +void FakeGCMClient::SetRecording(bool recording) { + recorder_.set_is_recording(recording); +} + +void FakeGCMClient::ClearActivityLogs() { + recorder_.Clear(); +} + +GCMClient::GCMStatistics FakeGCMClient::GetStatistics() const { + GCMClient::GCMStatistics statistics; + statistics.is_recording = recorder_.is_recording(); + + recorder_.CollectActivities(&statistics.recorded_activities); + return statistics; +} + +void FakeGCMClient::SetAccountTokens( + const std::vector& account_tokens) { +} + +void FakeGCMClient::UpdateAccountMapping( + const AccountMapping& account_mapping) { +} + +void FakeGCMClient::RemoveAccountMapping(const CoreAccountId& account_id) {} + +void FakeGCMClient::SetLastTokenFetchTime(const base::Time& time) { +} + +void FakeGCMClient::UpdateHeartbeatTimer( + std::unique_ptr timer) {} + +void FakeGCMClient::AddInstanceIDData(const std::string& app_id, + const std::string& instance_id, + const std::string& extra_data) { + instance_id_data_[app_id] = make_pair(instance_id, extra_data); +} + +void FakeGCMClient::RemoveInstanceIDData(const std::string& app_id) { + instance_id_data_.erase(app_id); +} + +void FakeGCMClient::GetInstanceIDData(const std::string& app_id, + std::string* instance_id, + std::string* extra_data) { + auto iter = instance_id_data_.find(app_id); + if (iter == instance_id_data_.end()) { + instance_id->clear(); + extra_data->clear(); + return; + } + + *instance_id = iter->second.first; + *extra_data = iter->second.second; +} + +void FakeGCMClient::AddHeartbeatInterval(const std::string& scope, + int interval_ms) { +} + +void FakeGCMClient::RemoveHeartbeatInterval(const std::string& scope) { +} + +void FakeGCMClient::PerformDelayedStart() { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + io_thread_->PostTask( + FROM_HERE, + base::BindOnce(&FakeGCMClient::DoStart, weak_ptr_factory_.GetWeakPtr())); +} + +void FakeGCMClient::ReceiveMessage(const std::string& app_id, + const IncomingMessage& message) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + io_thread_->PostTask( + FROM_HERE, + base::BindOnce(&FakeGCMClient::MessageReceived, + weak_ptr_factory_.GetWeakPtr(), app_id, message)); +} + +void FakeGCMClient::DeleteMessages(const std::string& app_id) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + io_thread_->PostTask(FROM_HERE, + base::BindOnce(&FakeGCMClient::MessagesDeleted, + weak_ptr_factory_.GetWeakPtr(), app_id)); +} + +void FakeGCMClient::Started() { + delegate_->OnGCMReady(std::vector(), base::Time()); + delegate_->OnConnected(net::IPEndPoint()); +} + +void FakeGCMClient::RegisterFinished( + scoped_refptr registration_info, + const std::string& registrion_id) { + delegate_->OnRegisterFinished(std::move(registration_info), registrion_id, + registrion_id.empty() ? SERVER_ERROR : SUCCESS); +} + +void FakeGCMClient::UnregisterFinished( + scoped_refptr registration_info) { + delegate_->OnUnregisterFinished(std::move(registration_info), + GCMClient::SUCCESS); +} + +void FakeGCMClient::SendFinished(const std::string& app_id, + const OutgoingMessage& message) { + delegate_->OnSendFinished(app_id, message.id, SUCCESS); + + // Simulate send error if message id contains a hint. + if (message.id.find("error") != std::string::npos) { + SendErrorDetails send_error_details; + send_error_details.message_id = message.id; + send_error_details.result = NETWORK_ERROR; + send_error_details.additional_data = message.data; + base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, + base::BindOnce(&FakeGCMClient::MessageSendError, + weak_ptr_factory_.GetWeakPtr(), app_id, + send_error_details), + base::Milliseconds(200)); + } else if(message.id.find("ack") != std::string::npos) { + base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, + base::BindOnce(&FakeGCMClient::SendAcknowledgement, + weak_ptr_factory_.GetWeakPtr(), app_id, message.id), + base::Milliseconds(200)); + } +} + +void FakeGCMClient::MessageReceived(const std::string& app_id, + const IncomingMessage& message) { + if (delegate_) + delegate_->OnMessageReceived(app_id, message); +} + +void FakeGCMClient::MessagesDeleted(const std::string& app_id) { + if (delegate_) + delegate_->OnMessagesDeleted(app_id); +} + +void FakeGCMClient::MessageSendError( + const std::string& app_id, + const GCMClient::SendErrorDetails& send_error_details) { + if (delegate_) + delegate_->OnMessageSendError(app_id, send_error_details); +} + +void FakeGCMClient::SendAcknowledgement(const std::string& app_id, + const std::string& message_id) { + if (delegate_) + delegate_->OnSendAcknowledged(app_id, message_id); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/fake_gcm_client.h b/chromium/components/gcm_driver/fake_gcm_client.h new file mode 100644 index 00000000000..dcc50751400 --- /dev/null +++ b/chromium/components/gcm_driver/fake_gcm_client.h @@ -0,0 +1,139 @@ +// 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 COMPONENTS_GCM_DRIVER_FAKE_GCM_CLIENT_H_ +#define COMPONENTS_GCM_DRIVER_FAKE_GCM_CLIENT_H_ + +#include + +#include "base/compiler_specific.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/timer/timer.h" +#include "components/gcm_driver/gcm_client.h" +#include "components/gcm_driver/gcm_stats_recorder_impl.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" + +namespace base { +class SequencedTaskRunner; +} + +namespace gcm { + +class FakeGCMClient : public GCMClient { + public: + // For testing purpose. + enum StartModeOverridding { + // No change to how delay start is handled. + RESPECT_START_MODE, + // Force to delay start GCM until PerformDelayedStart is called. + FORCE_TO_ALWAYS_DELAY_START_GCM, + }; + + // Generate and return the registration ID/token based on parameters for + // testing verification. + static std::string GenerateGCMRegistrationID( + const std::vector& sender_ids); + static std::string GenerateInstanceIDToken( + const std::string& authorized_entity, const std::string& scope); + + FakeGCMClient(const scoped_refptr& ui_thread, + const scoped_refptr& io_thread); + + FakeGCMClient(const FakeGCMClient&) = delete; + FakeGCMClient& operator=(const FakeGCMClient&) = delete; + + ~FakeGCMClient() override; + + // Overridden from GCMClient: + // Called on IO thread. + void Initialize( + const ChromeBuildInfo& chrome_build_info, + const base::FilePath& store_path, + bool remove_account_mappings_with_email_key, + const scoped_refptr& blocking_task_runner, + scoped_refptr io_task_runner, + base::RepeatingCallback)> + get_socket_factory_callback, + const scoped_refptr& url_loader_factory, + network::NetworkConnectionTracker* network_connection_tracker, + std::unique_ptr encryptor, + Delegate* delegate) override; + void Start(StartMode start_mode) override; + void Stop() override; + void Register(scoped_refptr registration_info) override; + bool ValidateRegistration(scoped_refptr registration_info, + const std::string& registration_id) override; + void Unregister(scoped_refptr registration_info) override; + void Send(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message) override; + void RecordDecryptionFailure(const std::string& app_id, + GCMDecryptionResult result) override; + void SetRecording(bool recording) override; + void ClearActivityLogs() override; + GCMStatistics GetStatistics() const override; + void SetAccountTokens( + const std::vector& account_tokens) override; + void UpdateAccountMapping(const AccountMapping& account_mapping) override; + void RemoveAccountMapping(const CoreAccountId& account_id) override; + void SetLastTokenFetchTime(const base::Time& time) override; + void UpdateHeartbeatTimer( + std::unique_ptr timer) override; + void AddInstanceIDData(const std::string& app_id, + const std::string& instance_id, + const std::string& extra_data) override; + void RemoveInstanceIDData(const std::string& app_id) override; + void GetInstanceIDData(const std::string& app_id, + std::string* instance_id, + std::string* extra_data) override; + void AddHeartbeatInterval(const std::string& scope, int interval_ms) override; + void RemoveHeartbeatInterval(const std::string& scope) override; + + // Initiate the start that has been delayed. + // Called on UI thread. + void PerformDelayedStart(); + + // Simulate receiving something from the server. + // Called on UI thread. + void ReceiveMessage(const std::string& app_id, + const IncomingMessage& message); + void DeleteMessages(const std::string& app_id); + + void set_start_mode_overridding(StartModeOverridding overridding) { + start_mode_overridding_ = overridding; + } + + private: + // Called on IO thread. + void DoStart(); + void Started(); + void RegisterFinished(scoped_refptr registration_info, + const std::string& registrion_id); + void UnregisterFinished(scoped_refptr registration_info); + void SendFinished(const std::string& app_id, const OutgoingMessage& message); + void MessageReceived(const std::string& app_id, + const IncomingMessage& message); + void MessagesDeleted(const std::string& app_id); + void MessageSendError(const std::string& app_id, + const SendErrorDetails& send_error_details); + void SendAcknowledgement(const std::string& app_id, + const std::string& message_id); + + raw_ptr delegate_; + std::string product_category_for_subtypes_; + bool started_; + StartMode start_mode_; + StartModeOverridding start_mode_overridding_; + scoped_refptr ui_thread_; + scoped_refptr io_thread_; + std::map> instance_id_data_; + GCMStatsRecorderImpl recorder_; + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_FAKE_GCM_CLIENT_H_ diff --git a/chromium/components/gcm_driver/fake_gcm_client_factory.cc b/chromium/components/gcm_driver/fake_gcm_client_factory.cc new file mode 100644 index 00000000000..78f6ff1cd06 --- /dev/null +++ b/chromium/components/gcm_driver/fake_gcm_client_factory.cc @@ -0,0 +1,28 @@ +// 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 "components/gcm_driver/fake_gcm_client_factory.h" + +#include + +#include "base/task/sequenced_task_runner.h" +#include "components/gcm_driver/gcm_client.h" + +namespace gcm { + +FakeGCMClientFactory::FakeGCMClientFactory( + const scoped_refptr& ui_thread, + const scoped_refptr& io_thread) + : ui_thread_(ui_thread), + io_thread_(io_thread) { +} + +FakeGCMClientFactory::~FakeGCMClientFactory() { +} + +std::unique_ptr FakeGCMClientFactory::BuildInstance() { + return std::unique_ptr(new FakeGCMClient(ui_thread_, io_thread_)); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/fake_gcm_client_factory.h b/chromium/components/gcm_driver/fake_gcm_client_factory.h new file mode 100644 index 00000000000..a20c990f87c --- /dev/null +++ b/chromium/components/gcm_driver/fake_gcm_client_factory.h @@ -0,0 +1,41 @@ +// 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 COMPONENTS_GCM_DRIVER_FAKE_GCM_CLIENT_FACTORY_H_ +#define COMPONENTS_GCM_DRIVER_FAKE_GCM_CLIENT_FACTORY_H_ + +#include "base/compiler_specific.h" +#include "components/gcm_driver/fake_gcm_client.h" +#include "components/gcm_driver/gcm_client_factory.h" + +namespace base { +class SequencedTaskRunner; +} + +namespace gcm { + +class GCMClient; + +class FakeGCMClientFactory : public GCMClientFactory { + public: + FakeGCMClientFactory( + const scoped_refptr& ui_thread, + const scoped_refptr& io_thread); + + FakeGCMClientFactory(const FakeGCMClientFactory&) = delete; + FakeGCMClientFactory& operator=(const FakeGCMClientFactory&) = delete; + + ~FakeGCMClientFactory() override; + + // GCMClientFactory: + std::unique_ptr BuildInstance() override; + + private: + scoped_refptr ui_thread_; + scoped_refptr io_thread_; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_FAKE_GCM_CLIENT_FACTORY_H_ diff --git a/chromium/components/gcm_driver/fake_gcm_driver.cc b/chromium/components/gcm_driver/fake_gcm_driver.cc new file mode 100644 index 00000000000..81f6318ab63 --- /dev/null +++ b/chromium/components/gcm_driver/fake_gcm_driver.cc @@ -0,0 +1,110 @@ +// 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 "components/gcm_driver/fake_gcm_driver.h" + +#include "base/bind.h" +#include "base/files/file_path.h" +#include "base/task/sequenced_task_runner.h" +#include "base/threading/thread_task_runner_handle.h" + +namespace gcm { + +FakeGCMDriver::FakeGCMDriver() : GCMDriver(base::FilePath(), nullptr) {} + +FakeGCMDriver::FakeGCMDriver( + const scoped_refptr& blocking_task_runner) + : GCMDriver(base::FilePath(), blocking_task_runner) {} + +FakeGCMDriver::~FakeGCMDriver() = default; + +void FakeGCMDriver::ValidateRegistration( + const std::string& app_id, + const std::vector& sender_ids, + const std::string& registration_id, + ValidateRegistrationCallback callback) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), true /* is_valid */)); +} + +void FakeGCMDriver::OnSignedIn() { +} + +void FakeGCMDriver::OnSignedOut() { +} + +void FakeGCMDriver::AddConnectionObserver(GCMConnectionObserver* observer) { +} + +void FakeGCMDriver::RemoveConnectionObserver(GCMConnectionObserver* observer) { +} + +GCMClient* FakeGCMDriver::GetGCMClientForTesting() const { + return nullptr; +} + +bool FakeGCMDriver::IsStarted() const { + return true; +} + +bool FakeGCMDriver::IsConnected() const { + return true; +} + +void FakeGCMDriver::GetGCMStatistics(GetGCMStatisticsCallback callback, + ClearActivityLogs clear_logs) {} + +void FakeGCMDriver::SetGCMRecording( + const GCMStatisticsRecordingCallback& callback, + bool recording) {} + +GCMClient::Result FakeGCMDriver::EnsureStarted( + GCMClient::StartMode start_mode) { + return GCMClient::SUCCESS; +} + +void FakeGCMDriver::RegisterImpl(const std::string& app_id, + const std::vector& sender_ids) { +} + +void FakeGCMDriver::UnregisterImpl(const std::string& app_id) { +} + +void FakeGCMDriver::SendImpl(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message) { +} + +void FakeGCMDriver::RecordDecryptionFailure(const std::string& app_id, + GCMDecryptionResult result) {} + +void FakeGCMDriver::SetAccountTokens( + const std::vector& account_tokens) { +} + +void FakeGCMDriver::UpdateAccountMapping( + const AccountMapping& account_mapping) { +} + +void FakeGCMDriver::RemoveAccountMapping(const CoreAccountId& account_id) {} + +base::Time FakeGCMDriver::GetLastTokenFetchTime() { + return base::Time(); +} + +void FakeGCMDriver::SetLastTokenFetchTime(const base::Time& time) { +} + +InstanceIDHandler* FakeGCMDriver::GetInstanceIDHandlerInternal() { + return nullptr; +} + +void FakeGCMDriver::AddHeartbeatInterval(const std::string& scope, + int interval_ms) { +} + +void FakeGCMDriver::RemoveHeartbeatInterval(const std::string& scope) { +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/fake_gcm_driver.h b/chromium/components/gcm_driver/fake_gcm_driver.h new file mode 100644 index 00000000000..91c1701b7da --- /dev/null +++ b/chromium/components/gcm_driver/fake_gcm_driver.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 COMPONENTS_GCM_DRIVER_FAKE_GCM_DRIVER_H_ +#define COMPONENTS_GCM_DRIVER_FAKE_GCM_DRIVER_H_ + +#include "base/compiler_specific.h" +#include "base/memory/ref_counted.h" +#include "components/gcm_driver/gcm_driver.h" + +namespace base { +class SequencedTaskRunner; +} + +namespace gcm { + +class FakeGCMDriver : public GCMDriver { + public: + FakeGCMDriver(); + explicit FakeGCMDriver( + const scoped_refptr& blocking_task_runner); + + FakeGCMDriver(const FakeGCMDriver&) = delete; + FakeGCMDriver& operator=(const FakeGCMDriver&) = delete; + + ~FakeGCMDriver() override; + + // GCMDriver overrides: + void ValidateRegistration(const std::string& app_id, + const std::vector& sender_ids, + const std::string& registration_id, + ValidateRegistrationCallback callback) override; + void OnSignedIn() override; + void OnSignedOut() override; + void AddConnectionObserver(GCMConnectionObserver* observer) override; + void RemoveConnectionObserver(GCMConnectionObserver* observer) override; + GCMClient* GetGCMClientForTesting() const override; + bool IsStarted() const override; + bool IsConnected() const override; + void GetGCMStatistics(GetGCMStatisticsCallback callback, + ClearActivityLogs clear_logs) override; + void SetGCMRecording(const GCMStatisticsRecordingCallback& callback, + bool recording) override; + void SetAccountTokens( + const std::vector& account_tokens) override; + void UpdateAccountMapping(const AccountMapping& account_mapping) override; + void RemoveAccountMapping(const CoreAccountId& account_id) override; + base::Time GetLastTokenFetchTime() override; + void SetLastTokenFetchTime(const base::Time& time) override; + InstanceIDHandler* GetInstanceIDHandlerInternal() override; + void AddHeartbeatInterval(const std::string& scope, int interval_ms) override; + void RemoveHeartbeatInterval(const std::string& scope) override; + + protected: + // GCMDriver implementation: + GCMClient::Result EnsureStarted( + GCMClient::StartMode start_mode) override; + void RegisterImpl(const std::string& app_id, + const std::vector& sender_ids) override; + void UnregisterImpl(const std::string& app_id) override; + void SendImpl(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message) override; + void RecordDecryptionFailure(const std::string& app_id, + GCMDecryptionResult result) override; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_FAKE_GCM_DRIVER_H_ diff --git a/chromium/components/gcm_driver/fake_gcm_profile_service.cc b/chromium/components/gcm_driver/fake_gcm_profile_service.cc new file mode 100644 index 00000000000..5d9c0354d3c --- /dev/null +++ b/chromium/components/gcm_driver/fake_gcm_profile_service.cc @@ -0,0 +1,246 @@ +// 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 "components/gcm_driver/fake_gcm_profile_service.h" + +#include + +#include "base/bind.h" +#include "base/format_macros.h" +#include "base/location.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/stringprintf.h" +#include "base/task/single_thread_task_runner.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "components/gcm_driver/crypto/gcm_encryption_result.h" +#include "components/gcm_driver/fake_gcm_client_factory.h" +#include "components/gcm_driver/gcm_driver.h" +#include "components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.h" + +namespace gcm { + +class FakeGCMProfileService::CustomFakeGCMDriver + : public instance_id::FakeGCMDriverForInstanceID { + public: + explicit CustomFakeGCMDriver(FakeGCMProfileService* service); + + CustomFakeGCMDriver(const CustomFakeGCMDriver&) = delete; + CustomFakeGCMDriver& operator=(const CustomFakeGCMDriver&) = delete; + + ~CustomFakeGCMDriver() override; + + void OnRegisterFinished(const std::string& app_id, + const std::string& registration_id, + GCMClient::Result result); + void OnSendFinished(const std::string& app_id, + const std::string& message_id, + GCMClient::Result result); + + void OnDispatchMessage(const std::string& app_id, + const IncomingMessage& message); + + // GCMDriver overrides: + void EncryptMessage(const std::string& app_id, + const std::string& authorized_entity, + const std::string& p256dh, + const std::string& auth_secret, + const std::string& message, + EncryptMessageCallback callback) override; + + protected: + // FakeGCMDriver overrides: + void RegisterImpl(const std::string& app_id, + const std::vector& sender_ids) override; + void UnregisterImpl(const std::string& app_id) override; + void UnregisterWithSenderIdImpl(const std::string& app_id, + const std::string& sender_id) override; + void SendImpl(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message) override; + + // FakeGCMDriverForInstanceID overrides: + void GetToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + base::TimeDelta time_to_live, + GetTokenCallback callback) override; + void DeleteToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + DeleteTokenCallback callback) override; + + private: + void DoRegister(const std::string& app_id, + const std::vector& sender_ids, + const std::string& registration_id); + void DoSend(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message); + + raw_ptr service_; + + // Used to give each registration a unique registration id. Does not decrease + // when unregister is called. + int registration_count_ = 0; + + base::WeakPtrFactory weak_factory_{ + this}; // Must be last. +}; + +FakeGCMProfileService::CustomFakeGCMDriver::CustomFakeGCMDriver( + FakeGCMProfileService* service) + : instance_id::FakeGCMDriverForInstanceID( + base::ThreadTaskRunnerHandle::Get()), + service_(service) {} + +FakeGCMProfileService::CustomFakeGCMDriver::~CustomFakeGCMDriver() {} + +void FakeGCMProfileService::CustomFakeGCMDriver::RegisterImpl( + const std::string& app_id, + const std::vector& sender_ids) { + if (service_->is_offline_) + return; // Drop request. + + // Generate fake registration IDs, encoding the number of sender IDs (used by + // GcmApiTest.RegisterValidation), then an incrementing count (even for the + // same app_id - there's no caching) so tests can distinguish registrations. + std::string registration_id = base::StringPrintf( + "%" PRIuS "-%d", sender_ids.size(), registration_count_); + ++registration_count_; + + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(&CustomFakeGCMDriver::DoRegister, + weak_factory_.GetWeakPtr(), app_id, sender_ids, + registration_id)); +} + +void FakeGCMProfileService::CustomFakeGCMDriver::DoRegister( + const std::string& app_id, + const std::vector& sender_ids, + const std::string& registration_id) { + if (service_->collect_) { + service_->last_registered_app_id_ = app_id; + service_->last_registered_sender_ids_ = sender_ids; + } + RegisterFinished(app_id, registration_id, GCMClient::SUCCESS); +} + +void FakeGCMProfileService::CustomFakeGCMDriver::UnregisterImpl( + const std::string& app_id) { + if (service_->is_offline_) + return; // Drop request. + + GCMClient::Result result = GCMClient::SUCCESS; + if (!service_->unregister_responses_.empty()) { + result = service_->unregister_responses_.front(); + service_->unregister_responses_.pop_front(); + } + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(&CustomFakeGCMDriver::UnregisterFinished, + weak_factory_.GetWeakPtr(), app_id, result)); +} + +void FakeGCMProfileService::CustomFakeGCMDriver::UnregisterWithSenderIdImpl( + const std::string& app_id, + const std::string& sender_id) { + NOTREACHED() << "This Android-specific method is not yet faked."; +} + +void FakeGCMProfileService::CustomFakeGCMDriver::SendImpl( + const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message) { + if (service_->is_offline_) + return; // Drop request. + + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(&CustomFakeGCMDriver::DoSend, weak_factory_.GetWeakPtr(), + app_id, receiver_id, message)); +} + +void FakeGCMProfileService::CustomFakeGCMDriver::EncryptMessage( + const std::string& app_id, + const std::string& authorized_entity, + const std::string& p256dh, + const std::string& auth_secret, + const std::string& message, + EncryptMessageCallback callback) { + // Pretend that message has been encrypted. + std::move(callback).Run(GCMEncryptionResult::ENCRYPTED_DRAFT_08, message); +} + +void FakeGCMProfileService::CustomFakeGCMDriver::DoSend( + const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message) { + if (service_->collect_) { + service_->last_sent_message_ = message; + service_->last_receiver_id_ = receiver_id; + } + SendFinished(app_id, message.id, GCMClient::SUCCESS); +} + +void FakeGCMProfileService::CustomFakeGCMDriver::GetToken( + const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + base::TimeDelta time_to_live, + GetTokenCallback callback) { + if (service_->is_offline_) + return; // Drop request. + + instance_id::FakeGCMDriverForInstanceID::GetToken( + app_id, authorized_entity, scope, time_to_live, std::move(callback)); +} + +void FakeGCMProfileService::CustomFakeGCMDriver::DeleteToken( + const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + DeleteTokenCallback callback) { + if (service_->is_offline_) + return; // Drop request. + + instance_id::FakeGCMDriverForInstanceID::DeleteToken( + app_id, authorized_entity, scope, std::move(callback)); +} + +void FakeGCMProfileService::CustomFakeGCMDriver::OnDispatchMessage( + const std::string& app_id, + const IncomingMessage& message) { + DispatchMessage(app_id, message); +} + +// static +std::unique_ptr FakeGCMProfileService::Build( + content::BrowserContext* context) { + std::unique_ptr service = + std::make_unique(); + service->SetDriverForTesting( + std::make_unique(service.get())); + + return service; +} + +FakeGCMProfileService::FakeGCMProfileService() = default; + +FakeGCMProfileService::~FakeGCMProfileService() = default; + +void FakeGCMProfileService::AddExpectedUnregisterResponse( + GCMClient::Result result) { + unregister_responses_.push_back(result); +} + +void FakeGCMProfileService::DispatchMessage(const std::string& app_id, + const IncomingMessage& message) { + CustomFakeGCMDriver* custom_driver = + static_cast(driver()); + custom_driver->OnDispatchMessage(app_id, message); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/fake_gcm_profile_service.h b/chromium/components/gcm_driver/fake_gcm_profile_service.h new file mode 100644 index 00000000000..14f5ff32240 --- /dev/null +++ b/chromium/components/gcm_driver/fake_gcm_profile_service.h @@ -0,0 +1,79 @@ +// 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 COMPONENTS_GCM_DRIVER_FAKE_GCM_PROFILE_SERVICE_H_ +#define COMPONENTS_GCM_DRIVER_FAKE_GCM_PROFILE_SERVICE_H_ + +#include +#include +#include +#include + +#include "components/gcm_driver/common/gcm_message.h" +#include "components/gcm_driver/gcm_client.h" +#include "components/gcm_driver/gcm_profile_service.h" + +namespace content { +class BrowserContext; +} // namespace content + +namespace gcm { + +// Acts as a bridge between GCM API and GCM Client layer for testing purposes. +class FakeGCMProfileService : public GCMProfileService { + public: + // Helper function to be used with KeyedServiceFactory::SetTestingFactory(). + static std::unique_ptr Build(content::BrowserContext* context); + + FakeGCMProfileService(); + + FakeGCMProfileService(const FakeGCMProfileService&) = delete; + FakeGCMProfileService& operator=(const FakeGCMProfileService&) = delete; + + ~FakeGCMProfileService() override; + + void AddExpectedUnregisterResponse(GCMClient::Result result); + + void DispatchMessage(const std::string& app_id, + const IncomingMessage& message); + + const OutgoingMessage& last_sent_message() const { + return last_sent_message_; + } + + const std::string& last_receiver_id() const { return last_receiver_id_; } + + const std::string& last_registered_app_id() const { + return last_registered_app_id_; + } + + const std::vector& last_registered_sender_ids() const { + return last_registered_sender_ids_; + } + + // Set whether the service will collect parameters of the calls for further + // verification in tests. + void set_collect(bool collect) { collect_ = collect; } + + // Crude offline simulation: requests fail and never run their callbacks (in + // reality, callbacks run within GetGCMBackoffPolicy().maximum_backoff_ms). + void set_offline(bool is_offline) { is_offline_ = is_offline; } + + private: + class CustomFakeGCMDriver; + friend class CustomFakeGCMDriver; + + bool collect_ = false; + bool is_offline_ = false; + + std::string last_registered_app_id_; + std::vector last_registered_sender_ids_; + std::list unregister_responses_; + OutgoingMessage last_sent_message_; + std::string last_receiver_id_; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_FAKE_GCM_PROFILE_SERVICE_H_ diff --git a/chromium/components/gcm_driver/features.cc b/chromium/components/gcm_driver/features.cc new file mode 100644 index 00000000000..9f6507687b1 --- /dev/null +++ b/chromium/components/gcm_driver/features.cc @@ -0,0 +1,41 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/features.h" +#include "base/metrics/field_trial_param_associator.h" +#include "base/metrics/field_trial_params.h" +#include "base/strings/string_number_conversions.h" + +#include +#include + +namespace gcm { + +namespace features { + +const base::Feature kInvalidateTokenFeature{"GCMTokenInvalidAfterDays", + base::FEATURE_ENABLED_BY_DEFAULT}; +const char kParamNameTokenInvalidationPeriodDays[] = + "token_invalidation_period"; +// A token invalidation period of 0 means the feature is disabled, and the +// GCM token never becomes stale. +const int kDefaultTokenInvalidationPeriod = 7; + +base::TimeDelta GetTokenInvalidationInterval() { + if (!base::FeatureList::IsEnabled(kInvalidateTokenFeature)) + return base::TimeDelta(); + std::string override_value = base::GetFieldTrialParamValueByFeature( + kInvalidateTokenFeature, kParamNameTokenInvalidationPeriodDays); + + if (!override_value.empty()) { + int override_value_days; + if (base::StringToInt(override_value, &override_value_days)) + return base::Days(override_value_days); + } + return base::Days(kDefaultTokenInvalidationPeriod); +} + +} // namespace features + +} // namespace gcm diff --git a/chromium/components/gcm_driver/features.h b/chromium/components/gcm_driver/features.h new file mode 100644 index 00000000000..9539efe6e65 --- /dev/null +++ b/chromium/components/gcm_driver/features.h @@ -0,0 +1,24 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_GCM_DRIVER_FEATURES_H_ +#define COMPONENTS_GCM_DRIVER_FEATURES_H_ + +#include "base/feature_list.h" + +namespace gcm { + +namespace features { + +extern const base::Feature kInvalidateTokenFeature; +extern const char kParamNameTokenInvalidationPeriodDays[]; + +// The period after which the GCM token becomes stale. +base::TimeDelta GetTokenInvalidationInterval(); + +} // namespace features + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_FEATURES_H_ diff --git a/chromium/components/gcm_driver/gcm_account_mapper.cc b/chromium/components/gcm_driver/gcm_account_mapper.cc new file mode 100644 index 00000000000..5570f8520b3 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_account_mapper.cc @@ -0,0 +1,386 @@ +// 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 "components/gcm_driver/gcm_account_mapper.h" + +#include + +#include "base/bind.h" +#include "base/guid.h" +#include "base/metrics/histogram_functions.h" +#include "base/time/clock.h" +#include "base/time/default_clock.h" +#include "components/gcm_driver/gcm_driver_desktop.h" +#include "google_apis/gcm/engine/gcm_store.h" + +namespace gcm { + +namespace { + +const char kGCMAccountMapperSenderId[] = "745476177629"; +const char kGCMAccountMapperSendTo[] = "google.com"; +const int kGCMAddMappingMessageTTL = 30 * 60; // 0.5 hours in seconds. +const int kGCMRemoveMappingMessageTTL = 24 * 60 * 60; // 1 day in seconds. +const int kGCMUpdateIntervalHours = 24; +// Because adding an account mapping dependents on a fresh OAuth2 token, we +// allow the update to happen earlier than update due time, if it is within +// the early start time to take advantage of that token. +const int kGCMUpdateEarlyStartHours = 6; +const char kRegistrationIdMessgaeKey[] = "id"; +const char kTokenMessageKey[] = "t"; +const char kAccountMessageKey[] = "a"; +const char kRemoveAccountKey[] = "r"; +const char kRemoveAccountValue[] = "1"; +// Use to handle send to Gaia ID scenario: +const char kGCMSendToGaiaIdAppIdKey[] = "gcmb"; + + +std::string GenerateMessageID() { + return base::GenerateGUID(); +} + +} // namespace + +const char kGCMAccountMapperAppId[] = "com.google.android.gms"; + +GCMAccountMapper::GCMAccountMapper(GCMDriver* gcm_driver) + : gcm_driver_(gcm_driver), + clock_(base::DefaultClock::GetInstance()), + initialized_(false) {} + +GCMAccountMapper::~GCMAccountMapper() = default; + +void GCMAccountMapper::Initialize(const AccountMappings& account_mappings, + const DispatchMessageCallback& callback) { + DCHECK(!initialized_); + initialized_ = true; + accounts_ = account_mappings; + dispatch_message_callback_ = callback; + GetRegistration(); +} + +void GCMAccountMapper::SetAccountTokens( + const std::vector& account_tokens) { + DVLOG(1) << "GCMAccountMapper::SetAccountTokens called with " + << account_tokens.size() << " accounts."; + + // If account mapper is not ready to handle tasks yet, save the latest + // account tokens and return. + if (!IsReady()) { + pending_account_tokens_ = account_tokens; + // If mapper is initialized, but still does not have registration ID, + // maybe the registration gave up. Retrying in case. + if (initialized_ && gcm_driver_->IsStarted()) + GetRegistration(); + return; + } + + // Start from removing the old tokens, from all of the known accounts. + for (auto iter = accounts_.begin(); iter != accounts_.end(); ++iter) { + iter->access_token.clear(); + } + + // Update the internal collection of mappings with the new tokens. + for (auto token_iter = account_tokens.begin(); + token_iter != account_tokens.end(); ++token_iter) { + AccountMapping* account_mapping = + FindMappingByAccountId(token_iter->account_id); + if (!account_mapping) { + AccountMapping new_mapping; + new_mapping.status = AccountMapping::NEW; + new_mapping.account_id = token_iter->account_id; + new_mapping.access_token = token_iter->access_token; + new_mapping.email = token_iter->email; + accounts_.push_back(new_mapping); + } else { + // Since we got a token for an account, drop the remove message and treat + // it as mapped. + if (account_mapping->status == AccountMapping::REMOVING) { + account_mapping->status = AccountMapping::MAPPED; + account_mapping->status_change_timestamp = base::Time(); + account_mapping->last_message_id.clear(); + } + + account_mapping->email = token_iter->email; + account_mapping->access_token = token_iter->access_token; + } + } + + // Decide what to do with each account (either start mapping, or start + // removing). + for (auto mappings_iter = accounts_.begin(); mappings_iter != accounts_.end(); + ++mappings_iter) { + if (mappings_iter->access_token.empty()) { + // Send a remove message if the account was not previously being removed, + // or it doesn't have a pending message, or the pending message is + // already expired, but OnSendError event was lost. + if (mappings_iter->status != AccountMapping::REMOVING || + mappings_iter->last_message_id.empty() || + IsLastStatusChangeOlderThanTTL(*mappings_iter)) { + SendRemoveMappingMessage(*mappings_iter); + } + } else { + // A message is sent for all of the mappings considered NEW, or mappings + // that are ADDING, but have expired message (OnSendError event lost), or + // for those mapped accounts that can be refreshed. + if (mappings_iter->status == AccountMapping::NEW || + (mappings_iter->status == AccountMapping::ADDING && + IsLastStatusChangeOlderThanTTL(*mappings_iter)) || + (mappings_iter->status == AccountMapping::MAPPED && + CanTriggerUpdate(mappings_iter->status_change_timestamp))) { + mappings_iter->last_message_id.clear(); + SendAddMappingMessage(*mappings_iter); + } + } + } +} + +void GCMAccountMapper::ShutdownHandler() { + initialized_ = false; + accounts_.clear(); + registration_id_.clear(); + dispatch_message_callback_.Reset(); +} + +void GCMAccountMapper::OnStoreReset() { + // TODO(crbug.com/661660): Tell server to remove the mapping. But can't use + // upstream GCM send for that since the store got reset. + ShutdownHandler(); +} + +void GCMAccountMapper::OnMessage(const std::string& app_id, + const IncomingMessage& message) { + DCHECK_EQ(app_id, kGCMAccountMapperAppId); + // TODO(fgorski): Report Send to Gaia ID failures using UMA. + + base::UmaHistogramBoolean("GCM.AccountMappingMessageReceived", true); + + if (dispatch_message_callback_.is_null()) { + DVLOG(1) << "dispatch_message_callback_ missing in GCMAccountMapper"; + return; + } + + auto it = message.data.find(kGCMSendToGaiaIdAppIdKey); + if (it == message.data.end()) { + DVLOG(1) << "Send to Gaia ID failure: Embedded app ID missing."; + return; + } + + std::string embedded_app_id = it->second; + if (embedded_app_id.empty()) { + DVLOG(1) << "Send to Gaia ID failure: Embedded app ID is empty."; + return; + } + + // Ensuring the message does not carry the embedded app ID. + IncomingMessage new_message = message; + new_message.data.erase(new_message.data.find(kGCMSendToGaiaIdAppIdKey)); + dispatch_message_callback_.Run(embedded_app_id, new_message); +} + +void GCMAccountMapper::OnMessagesDeleted(const std::string& app_id) { + // Account message does not expect messages right now. +} + +void GCMAccountMapper::OnSendError( + const std::string& app_id, + const GCMClient::SendErrorDetails& send_error_details) { + DCHECK_EQ(app_id, kGCMAccountMapperAppId); + + auto account_mapping_it = + FindMappingByMessageId(send_error_details.message_id); + + if (account_mapping_it == accounts_.end()) + return; + + if (send_error_details.result != GCMClient::TTL_EXCEEDED) { + DVLOG(1) << "Send error result different than TTL EXCEEDED: " + << send_error_details.result << ". " + << "Postponing the retry until a new batch of tokens arrives."; + return; + } + + if (account_mapping_it->status == AccountMapping::REMOVING) { + // Another message to remove mapping can be sent immediately, because TTL + // for those is one day. No need to back off. + SendRemoveMappingMessage(*account_mapping_it); + } else { + if (account_mapping_it->status == AccountMapping::ADDING) { + // There is no mapping established, so we can remove the entry. + // Getting a fresh token will trigger a new attempt. + gcm_driver_->RemoveAccountMapping(account_mapping_it->account_id); + accounts_.erase(account_mapping_it); + } else { + // Account is already MAPPED, we have to wait for another token. + account_mapping_it->last_message_id.clear(); + gcm_driver_->UpdateAccountMapping(*account_mapping_it); + } + } +} + +void GCMAccountMapper::OnSendAcknowledged(const std::string& app_id, + const std::string& message_id) { + DCHECK_EQ(app_id, kGCMAccountMapperAppId); + auto account_mapping_it = FindMappingByMessageId(message_id); + + DVLOG(1) << "OnSendAcknowledged with message ID: " << message_id; + + if (account_mapping_it == accounts_.end()) + return; + + // Here is where we advance a status of a mapping and persist or remove. + if (account_mapping_it->status == AccountMapping::REMOVING) { + // Message removing the account has been confirmed by the GCM, we can remove + // all the information related to the account (from memory and store). + gcm_driver_->RemoveAccountMapping(account_mapping_it->account_id); + accounts_.erase(account_mapping_it); + } else { + // Mapping status is ADDING only when it is a first time mapping. + DCHECK(account_mapping_it->status == AccountMapping::ADDING || + account_mapping_it->status == AccountMapping::MAPPED); + + // Account is marked as mapped with the current time. + account_mapping_it->status = AccountMapping::MAPPED; + account_mapping_it->status_change_timestamp = clock_->Now(); + // There is no pending message for the account. + account_mapping_it->last_message_id.clear(); + + gcm_driver_->UpdateAccountMapping(*account_mapping_it); + } +} + +bool GCMAccountMapper::CanHandle(const std::string& app_id) const { + return app_id.compare(kGCMAccountMapperAppId) == 0; +} + +bool GCMAccountMapper::IsReady() { + return initialized_ && gcm_driver_->IsStarted() && !registration_id_.empty(); +} + +void GCMAccountMapper::SendAddMappingMessage(AccountMapping& account_mapping) { + CreateAndSendMessage(account_mapping); +} + +void GCMAccountMapper::SendRemoveMappingMessage( + AccountMapping& account_mapping) { + // We want to persist an account that is being removed as quickly as possible + // as well as clean up the last message information. + if (account_mapping.status != AccountMapping::REMOVING) { + account_mapping.status = AccountMapping::REMOVING; + account_mapping.status_change_timestamp = clock_->Now(); + } + + account_mapping.last_message_id.clear(); + + gcm_driver_->UpdateAccountMapping(account_mapping); + + CreateAndSendMessage(account_mapping); +} + +void GCMAccountMapper::CreateAndSendMessage( + const AccountMapping& account_mapping) { + OutgoingMessage outgoing_message; + outgoing_message.id = GenerateMessageID(); + outgoing_message.data[kRegistrationIdMessgaeKey] = registration_id_; + outgoing_message.data[kAccountMessageKey] = account_mapping.email; + + if (account_mapping.status == AccountMapping::REMOVING) { + outgoing_message.time_to_live = kGCMRemoveMappingMessageTTL; + outgoing_message.data[kRemoveAccountKey] = kRemoveAccountValue; + } else { + outgoing_message.data[kTokenMessageKey] = account_mapping.access_token; + outgoing_message.time_to_live = kGCMAddMappingMessageTTL; + } + + gcm_driver_->Send(kGCMAccountMapperAppId, kGCMAccountMapperSendTo, + outgoing_message, + base::BindOnce(&GCMAccountMapper::OnSendFinished, + weak_ptr_factory_.GetWeakPtr(), + account_mapping.account_id)); +} + +void GCMAccountMapper::OnSendFinished(const CoreAccountId& account_id, + const std::string& message_id, + GCMClient::Result result) { + // TODO(fgorski): Add another attempt, in case the QUEUE is not full. + if (result != GCMClient::SUCCESS) + return; + + AccountMapping* account_mapping = FindMappingByAccountId(account_id); + DCHECK(account_mapping); + + // If we are dealing with account with status NEW, it is the first time + // mapping, and we should mark it as ADDING. + if (account_mapping->status == AccountMapping::NEW) { + account_mapping->status = AccountMapping::ADDING; + account_mapping->status_change_timestamp = clock_->Now(); + } + + account_mapping->last_message_id = message_id; + + gcm_driver_->UpdateAccountMapping(*account_mapping); +} + +void GCMAccountMapper::GetRegistration() { + DCHECK(registration_id_.empty()); + std::vector sender_ids; + sender_ids.push_back(kGCMAccountMapperSenderId); + gcm_driver_->Register(kGCMAccountMapperAppId, sender_ids, + base::BindOnce(&GCMAccountMapper::OnRegisterFinished, + weak_ptr_factory_.GetWeakPtr())); +} + +void GCMAccountMapper::OnRegisterFinished(const std::string& registration_id, + GCMClient::Result result) { + if (result == GCMClient::SUCCESS) + registration_id_ = registration_id; + + if (IsReady()) { + if (!pending_account_tokens_.empty()) { + SetAccountTokens(pending_account_tokens_); + pending_account_tokens_.clear(); + } + } +} + +bool GCMAccountMapper::CanTriggerUpdate( + const base::Time& last_update_time) const { + return last_update_time + + base::Hours(kGCMUpdateIntervalHours - kGCMUpdateEarlyStartHours) < + clock_->Now(); +} + +bool GCMAccountMapper::IsLastStatusChangeOlderThanTTL( + const AccountMapping& account_mapping) const { + int ttl_seconds = account_mapping.status == AccountMapping::REMOVING ? + kGCMRemoveMappingMessageTTL : kGCMAddMappingMessageTTL; + return account_mapping.status_change_timestamp + base::Seconds(ttl_seconds) < + clock_->Now(); +} + +AccountMapping* GCMAccountMapper::FindMappingByAccountId( + const CoreAccountId& account_id) { + for (auto iter = accounts_.begin(); iter != accounts_.end(); ++iter) { + if (iter->account_id == account_id) + return &*iter; + } + + return nullptr; +} + +GCMAccountMapper::AccountMappings::iterator +GCMAccountMapper::FindMappingByMessageId(const std::string& message_id) { + for (auto iter = accounts_.begin(); iter != accounts_.end(); ++iter) { + if (iter->last_message_id == message_id) + return iter; + } + + return accounts_.end(); +} + +void GCMAccountMapper::SetClockForTesting(base::Clock* clock) { + clock_ = clock; +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_account_mapper.h b/chromium/components/gcm_driver/gcm_account_mapper.h new file mode 100644 index 00000000000..9b4ab1f56f6 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_account_mapper.h @@ -0,0 +1,136 @@ +// 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 COMPONENTS_GCM_DRIVER_GCM_ACCOUNT_MAPPER_H_ +#define COMPONENTS_GCM_DRIVER_GCM_ACCOUNT_MAPPER_H_ + +#include +#include +#include + +#include "base/callback.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "components/gcm_driver/gcm_app_handler.h" +#include "components/gcm_driver/gcm_client.h" +#include "google_apis/gcm/engine/account_mapping.h" + +namespace base { +class Clock; +} + +namespace gcm { + +class GCMDriver; +extern const char kGCMAccountMapperAppId[]; + +// Class for mapping signed-in GAIA accounts to the GCM Device ID. +class GCMAccountMapper : public GCMAppHandler { + public: + // List of account mappings. + using AccountMappings = std::vector; + using DispatchMessageCallback = + base::RepeatingCallback; + + explicit GCMAccountMapper(GCMDriver* gcm_driver); + + GCMAccountMapper(const GCMAccountMapper&) = delete; + GCMAccountMapper& operator=(const GCMAccountMapper&) = delete; + + ~GCMAccountMapper() override; + + void Initialize(const AccountMappings& account_mappings, + const DispatchMessageCallback& callback); + + // Called by AccountTracker, when a new list of account tokens is available. + // This will cause a refresh of account mappings and sending updates to GCM. + void SetAccountTokens( + const std::vector& account_tokens); + + // Implementation of GCMAppHandler: + void ShutdownHandler() override; + void OnStoreReset() override; + void OnMessage(const std::string& app_id, + const IncomingMessage& message) override; + void OnMessagesDeleted(const std::string& app_id) override; + void OnSendError( + const std::string& app_id, + const GCMClient::SendErrorDetails& send_error_details) override; + void OnSendAcknowledged(const std::string& app_id, + const std::string& message_id) override; + bool CanHandle(const std::string& app_id) const override; + + private: + friend class GCMAccountMapperTest; + + typedef std::map OutgoingMessages; + + // Checks whether account mapper is ready to process new account tokens. + bool IsReady(); + + // Informs GCM of an added or refreshed account mapping. + void SendAddMappingMessage(AccountMapping& account_mapping); + + // Informs GCM of a removed account mapping. + void SendRemoveMappingMessage(AccountMapping& account_mapping); + + void CreateAndSendMessage(const AccountMapping& account_mapping); + + // Callback for sending a message. + void OnSendFinished(const CoreAccountId& account_id, + const std::string& message_id, + GCMClient::Result result); + + // Gets a registration for account mapper from GCM. + void GetRegistration(); + + // Callback for registering with GCM. + void OnRegisterFinished(const std::string& registration_id, + GCMClient::Result result); + + // Checks whether the update can be triggered now. If the current time is + // within reasonable time (6 hours) of when the update is due, we want to + // trigger the update immediately to take advantage of a fresh OAuth2 token. + bool CanTriggerUpdate(const base::Time& last_update_time) const; + + // Checks whether last status change is older than a TTL of a message. + bool IsLastStatusChangeOlderThanTTL( + const AccountMapping& account_mapping) const; + + // Finds an account mapping in |accounts_| by |account_id|. + AccountMapping* FindMappingByAccountId(const CoreAccountId& account_id); + // Finds an account mapping in |accounts_| by |message_id|. + // Returns iterator that can be used to delete the account. + AccountMappings::iterator FindMappingByMessageId( + const std::string& message_id); + + // Sets the clock for testing. + void SetClockForTesting(base::Clock* clock); + + // GCMDriver owns GCMAccountMapper. + raw_ptr gcm_driver_; + + // Callback to GCMDriver to dispatch messages sent to Gaia ID. + DispatchMessageCallback dispatch_message_callback_; + + // Clock for timestamping status changes. + raw_ptr clock_; + + // Currnetly tracked account mappings. + AccountMappings accounts_; + + std::vector pending_account_tokens_; + + // GCM Registration ID of the account mapper. + std::string registration_id_; + + bool initialized_; + + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_GCM_ACCOUNT_MAPPER_H_ diff --git a/chromium/components/gcm_driver/gcm_account_mapper_unittest.cc b/chromium/components/gcm_driver/gcm_account_mapper_unittest.cc new file mode 100644 index 00000000000..286d7e41915 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_account_mapper_unittest.cc @@ -0,0 +1,955 @@ +// 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 "components/gcm_driver/gcm_account_mapper.h" + +#include +#include + +#include "base/bind.h" +#include "base/test/simple_test_clock.h" +#include "base/time/time.h" +#include "components/gcm_driver/fake_gcm_driver.h" +#include "google_apis/gcm/engine/account_mapping.h" +#include "google_apis/gcm/engine/gcm_store.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { + +namespace { + +const char kGCMAccountMapperSenderId[] = "745476177629"; +const char kGCMAccountMapperSendTo[] = "google.com"; +const char kRegistrationId[] = "reg_id"; +const char kEmbeddedAppIdKey[] = "gcmb"; +const char kTestAppId[] = "test_app_id"; +const char kTestDataKey[] = "data_key"; +const char kTestDataValue[] = "data_value"; +const char kTestCollapseKey[] = "test_collapse_key"; +const char kTestSenderId[] = "test_sender_id"; + +AccountMapping MakeAccountMapping(const CoreAccountId& account_id, + AccountMapping::MappingStatus status, + const base::Time& status_change_timestamp, + const std::string& last_message_id) { + AccountMapping account_mapping; + account_mapping.account_id = account_id; + account_mapping.email = account_id.ToString() + "@gmail.com"; + // account_mapping.access_token intentionally left empty. + account_mapping.status = status; + account_mapping.status_change_timestamp = status_change_timestamp; + account_mapping.last_message_id = last_message_id; + return account_mapping; +} + +GCMClient::AccountTokenInfo MakeAccountTokenInfo( + const CoreAccountId& account_id) { + GCMClient::AccountTokenInfo account_token; + account_token.account_id = account_id; + account_token.email = account_id.ToString() + "@gmail.com"; + account_token.access_token = account_id.ToString() + "_token"; + return account_token; +} + +void VerifyMappings(const GCMAccountMapper::AccountMappings& expected_mappings, + const GCMAccountMapper::AccountMappings& actual_mappings, + const std::string& verification_info) { + EXPECT_EQ(expected_mappings.size(), actual_mappings.size()) + << "Verification Info: " << verification_info; + auto expected_iter = expected_mappings.begin(); + auto actual_iter = actual_mappings.begin(); + for (; expected_iter != expected_mappings.end() && + actual_iter != actual_mappings.end(); + ++expected_iter, ++actual_iter) { + EXPECT_EQ(expected_iter->email, actual_iter->email) + << "Verification Info: " << verification_info + << "; Account ID of expected: " << expected_iter->account_id; + EXPECT_EQ(expected_iter->account_id, actual_iter->account_id) + << "Verification Info: " << verification_info; + EXPECT_EQ(expected_iter->status, actual_iter->status) + << "Verification Info: " << verification_info + << "; Account ID of expected: " << expected_iter->account_id; + EXPECT_EQ(expected_iter->status_change_timestamp, + actual_iter->status_change_timestamp) + << "Verification Info: " << verification_info + << "; Account ID of expected: " << expected_iter->account_id; + } +} + +class CustomFakeGCMDriver : public FakeGCMDriver { + public: + enum LastMessageAction { + NONE, + SEND_STARTED, + SEND_FINISHED, + SEND_ACKNOWLEDGED + }; + + CustomFakeGCMDriver(); + ~CustomFakeGCMDriver() override; + + // GCMDriver implementation: + void UpdateAccountMapping(const AccountMapping& account_mapping) override; + void RemoveAccountMapping(const CoreAccountId& account_id) override; + void RegisterImpl(const std::string& app_id, + const std::vector& sender_ids) override; + + void CompleteRegister(const std::string& registration_id, + GCMClient::Result result); + void CompleteSend(const std::string& message_id, GCMClient::Result result); + void AcknowledgeSend(const std::string& message_id); + void MessageSendError(const std::string& message_id); + + void CompleteSendAllMessages(); + void AcknowledgeSendAllMessages(); + void SetLastMessageAction(const std::string& message_id, + LastMessageAction action); + void Clear(); + + const AccountMapping& last_account_mapping() const { + return account_mapping_; + } + const std::string& last_message_id() const { return last_message_id_; } + const CoreAccountId& last_removed_account_id() const { + return last_removed_account_id_; + } + LastMessageAction last_action() const { return last_action_; } + bool registration_id_requested() const { return registration_id_requested_; } + + protected: + void SendImpl(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message) override; + + private: + AccountMapping account_mapping_; + std::string last_message_id_; + CoreAccountId last_removed_account_id_; + LastMessageAction last_action_; + std::map all_messages_; + bool registration_id_requested_; +}; + +CustomFakeGCMDriver::CustomFakeGCMDriver() + : last_action_(NONE), registration_id_requested_(false) { +} + +CustomFakeGCMDriver::~CustomFakeGCMDriver() { +} + +void CustomFakeGCMDriver::UpdateAccountMapping( + const AccountMapping& account_mapping) { + account_mapping_.email = account_mapping.email; + account_mapping_.account_id = account_mapping.account_id; + account_mapping_.access_token = account_mapping.access_token; + account_mapping_.status = account_mapping.status; + account_mapping_.status_change_timestamp = + account_mapping.status_change_timestamp; + account_mapping_.last_message_id = account_mapping.last_message_id; +} + +void CustomFakeGCMDriver::RemoveAccountMapping( + const CoreAccountId& account_id) { + last_removed_account_id_ = account_id; +} + +void CustomFakeGCMDriver::RegisterImpl( + const std::string& app_id, + const std::vector& sender_ids) { + DCHECK_EQ(kGCMAccountMapperAppId, app_id); + DCHECK_EQ(1u, sender_ids.size()); + DCHECK_EQ(kGCMAccountMapperSenderId, sender_ids[0]); + registration_id_requested_ = true; +} + +void CustomFakeGCMDriver::CompleteRegister(const std::string& registration_id, + GCMClient::Result result) { + RegisterFinished(kGCMAccountMapperAppId, registration_id, result); +} + +void CustomFakeGCMDriver::CompleteSend(const std::string& message_id, + GCMClient::Result result) { + SendFinished(kGCMAccountMapperAppId, message_id, result); + SetLastMessageAction(message_id, SEND_FINISHED); +} + +void CustomFakeGCMDriver::AcknowledgeSend(const std::string& message_id) { + GCMAppHandler* handler = GetAppHandler(kGCMAccountMapperAppId); + if (handler) + handler->OnSendAcknowledged(kGCMAccountMapperAppId, message_id); + SetLastMessageAction(message_id, SEND_ACKNOWLEDGED); +} + +void CustomFakeGCMDriver::MessageSendError(const std::string& message_id) { + GCMAppHandler* handler = GetAppHandler(kGCMAccountMapperAppId); + if (!handler) + return; + + GCMClient::SendErrorDetails send_error; + send_error.message_id = message_id; + send_error.result = GCMClient::TTL_EXCEEDED; + + handler->OnSendError(kGCMAccountMapperAppId, send_error); +} + +void CustomFakeGCMDriver::SendImpl(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message) { + DCHECK_EQ(kGCMAccountMapperAppId, app_id); + DCHECK_EQ(kGCMAccountMapperSendTo, receiver_id); + + SetLastMessageAction(message.id, SEND_STARTED); +} + +void CustomFakeGCMDriver::CompleteSendAllMessages() { + for (std::map::const_iterator iter = + all_messages_.begin(); + iter != all_messages_.end(); + ++iter) { + if (iter->second == SEND_STARTED) + CompleteSend(iter->first, GCMClient::SUCCESS); + } +} + +void CustomFakeGCMDriver::AcknowledgeSendAllMessages() { + for (std::map::const_iterator iter = + all_messages_.begin(); + iter != all_messages_.end(); + ++iter) { + if (iter->second == SEND_FINISHED) + AcknowledgeSend(iter->first); + } +} + +void CustomFakeGCMDriver::Clear() { + account_mapping_ = AccountMapping(); + last_message_id_.clear(); + last_removed_account_id_ = CoreAccountId(); + last_action_ = NONE; + registration_id_requested_ = false; +} + +void CustomFakeGCMDriver::SetLastMessageAction(const std::string& message_id, + LastMessageAction action) { + last_action_ = action; + last_message_id_ = message_id; + all_messages_[message_id] = action; +} + +} // namespace + +class GCMAccountMapperTest : public testing::Test { + public: + const CoreAccountId kAccountId; + const CoreAccountId kAccountId1; + const CoreAccountId kAccountId2; + const CoreAccountId kAccountId3; + const CoreAccountId kAccountId4; + + GCMAccountMapperTest(); + ~GCMAccountMapperTest() override; + + void Restart(); + + void Initialize(const GCMAccountMapper::AccountMappings mappings); + const GCMAccountMapper::AccountMappings& GetAccounts() const { + return account_mapper_->accounts_; + } + void MessageReceived(const std::string& app_id, + const IncomingMessage& message); + + GCMAccountMapper* mapper() { return account_mapper_.get(); } + + CustomFakeGCMDriver& gcm_driver() { return gcm_driver_; } + + base::SimpleTestClock* clock() { return &clock_; } + const std::string& last_received_app_id() const { + return last_received_app_id_; + } + const IncomingMessage& last_received_message() const { + return last_received_message_; + } + + private: + CustomFakeGCMDriver gcm_driver_; + std::unique_ptr account_mapper_; + base::SimpleTestClock clock_; + std::string last_received_app_id_; + IncomingMessage last_received_message_; +}; + +GCMAccountMapperTest::GCMAccountMapperTest() + : kAccountId("acc_id"), + kAccountId1("acc_id1"), + kAccountId2("acc_id2"), + kAccountId3("acc_id3"), + kAccountId4("acc_id4") { + Restart(); +} + +GCMAccountMapperTest::~GCMAccountMapperTest() { +} + +void GCMAccountMapperTest::Restart() { + if (account_mapper_) + account_mapper_->ShutdownHandler(); + gcm_driver_.RemoveAppHandler(kGCMAccountMapperAppId); + account_mapper_ = std::make_unique(&gcm_driver_); + account_mapper_->SetClockForTesting(&clock_); +} + +void GCMAccountMapperTest::Initialize( + const GCMAccountMapper::AccountMappings mappings) { + mapper()->Initialize( + mappings, base::BindRepeating(&GCMAccountMapperTest::MessageReceived, + base::Unretained(this))); +} + +void GCMAccountMapperTest::MessageReceived(const std::string& app_id, + const IncomingMessage& message) { + last_received_app_id_ = app_id; + last_received_message_ = message; +} + +// Tests the initialization of account mappings (from the store) when empty. +// It also checks that initialization triggers registration ID request. +TEST_F(GCMAccountMapperTest, InitializeAccountMappingsEmpty) { + EXPECT_FALSE(gcm_driver().registration_id_requested()); + Initialize(GCMAccountMapper::AccountMappings()); + EXPECT_TRUE(GetAccounts().empty()); + EXPECT_TRUE(gcm_driver().registration_id_requested()); +} + +// Tests that registration is retried, when new tokens are delivered and in no +// other circumstances. +TEST_F(GCMAccountMapperTest, RegistrationRetryUponFailure) { + Initialize(GCMAccountMapper::AccountMappings()); + EXPECT_TRUE(gcm_driver().registration_id_requested()); + gcm_driver().Clear(); + + gcm_driver().CompleteRegister(kRegistrationId, GCMClient::UNKNOWN_ERROR); + EXPECT_FALSE(gcm_driver().registration_id_requested()); + gcm_driver().Clear(); + + std::vector account_tokens; + account_tokens.push_back(MakeAccountTokenInfo(kAccountId2)); + mapper()->SetAccountTokens(account_tokens); + EXPECT_TRUE(gcm_driver().registration_id_requested()); + gcm_driver().Clear(); + + gcm_driver().CompleteRegister(kRegistrationId, + GCMClient::ASYNC_OPERATION_PENDING); + EXPECT_FALSE(gcm_driver().registration_id_requested()); +} + +// Tests the initialization of account mappings (from the store). +TEST_F(GCMAccountMapperTest, InitializeAccountMappings) { + GCMAccountMapper::AccountMappings account_mappings; + AccountMapping account_mapping1 = MakeAccountMapping( + kAccountId1, AccountMapping::MAPPED, base::Time::Now(), std::string()); + AccountMapping account_mapping2 = MakeAccountMapping( + kAccountId2, AccountMapping::ADDING, base::Time::Now(), "add_message_1"); + account_mappings.push_back(account_mapping1); + account_mappings.push_back(account_mapping2); + + Initialize(account_mappings); + + GCMAccountMapper::AccountMappings mappings = GetAccounts(); + EXPECT_EQ(2UL, mappings.size()); + GCMAccountMapper::AccountMappings::const_iterator iter = mappings.begin(); + + EXPECT_EQ(account_mapping1.account_id, iter->account_id); + EXPECT_EQ(account_mapping1.email, iter->email); + EXPECT_TRUE(account_mapping1.access_token.empty()); + EXPECT_EQ(account_mapping1.status, iter->status); + EXPECT_EQ(account_mapping1.status_change_timestamp, + iter->status_change_timestamp); + EXPECT_TRUE(account_mapping1.last_message_id.empty()); + + ++iter; + EXPECT_EQ(account_mapping2.account_id, iter->account_id); + EXPECT_EQ(account_mapping2.email, iter->email); + EXPECT_TRUE(account_mapping2.access_token.empty()); + EXPECT_EQ(account_mapping2.status, iter->status); + EXPECT_EQ(account_mapping2.status_change_timestamp, + iter->status_change_timestamp); + EXPECT_EQ(account_mapping2.last_message_id, iter->last_message_id); +} + +// Tests that account tokens are not processed until registration ID is +// available. +TEST_F(GCMAccountMapperTest, SetAccountTokensOnlyWorksWithRegisterationId) { + // Start with empty list. + Initialize(GCMAccountMapper::AccountMappings()); + + std::vector account_tokens; + account_tokens.push_back(MakeAccountTokenInfo(kAccountId)); + mapper()->SetAccountTokens(account_tokens); + + EXPECT_TRUE(GetAccounts().empty()); + + account_tokens.clear(); + account_tokens.push_back(MakeAccountTokenInfo(kAccountId1)); + account_tokens.push_back(MakeAccountTokenInfo(kAccountId2)); + mapper()->SetAccountTokens(account_tokens); + + EXPECT_TRUE(GetAccounts().empty()); + + gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS); + + GCMAccountMapper::AccountMappings mappings = GetAccounts(); + EXPECT_EQ(2UL, mappings.size()); + EXPECT_EQ(kAccountId1, mappings[0].account_id); + EXPECT_EQ(kAccountId2, mappings[1].account_id); +} + +// Tests the part where a new account is added with a token, to the point when +// GCM message is sent. +TEST_F(GCMAccountMapperTest, AddMappingToMessageSent) { + Initialize(GCMAccountMapper::AccountMappings()); + gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS); + + std::vector account_tokens; + GCMClient::AccountTokenInfo account_token = MakeAccountTokenInfo(kAccountId); + account_tokens.push_back(account_token); + mapper()->SetAccountTokens(account_tokens); + + GCMAccountMapper::AccountMappings mappings = GetAccounts(); + EXPECT_EQ(1UL, mappings.size()); + GCMAccountMapper::AccountMappings::const_iterator iter = mappings.begin(); + EXPECT_EQ(kAccountId, iter->account_id); + EXPECT_EQ("acc_id@gmail.com", iter->email); + EXPECT_EQ("acc_id_token", iter->access_token); + EXPECT_EQ(AccountMapping::NEW, iter->status); + EXPECT_EQ(base::Time(), iter->status_change_timestamp); + + EXPECT_TRUE(!gcm_driver().last_message_id().empty()); +} + +// Tests the part where GCM message is successfully queued. +TEST_F(GCMAccountMapperTest, AddMappingMessageQueued) { + Initialize(GCMAccountMapper::AccountMappings()); + gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS); + + std::vector account_tokens; + GCMClient::AccountTokenInfo account_token = MakeAccountTokenInfo(kAccountId); + account_tokens.push_back(account_token); + mapper()->SetAccountTokens(account_tokens); + + clock()->SetNow(base::Time::Now()); + gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS); + + EXPECT_EQ(account_token.email, gcm_driver().last_account_mapping().email); + EXPECT_EQ(account_token.account_id, + gcm_driver().last_account_mapping().account_id); + EXPECT_EQ(account_token.access_token, + gcm_driver().last_account_mapping().access_token); + EXPECT_EQ(AccountMapping::ADDING, gcm_driver().last_account_mapping().status); + EXPECT_EQ(clock()->Now(), + gcm_driver().last_account_mapping().status_change_timestamp); + EXPECT_EQ(gcm_driver().last_message_id(), + gcm_driver().last_account_mapping().last_message_id); + + GCMAccountMapper::AccountMappings mappings = GetAccounts(); + GCMAccountMapper::AccountMappings::const_iterator iter = mappings.begin(); + EXPECT_EQ(account_token.email, iter->email); + EXPECT_EQ(account_token.account_id, iter->account_id); + EXPECT_EQ(account_token.access_token, iter->access_token); + EXPECT_EQ(AccountMapping::ADDING, iter->status); + EXPECT_EQ(clock()->Now(), iter->status_change_timestamp); + EXPECT_EQ(gcm_driver().last_message_id(), iter->last_message_id); +} + +// Tests status change from ADDING to MAPPED (Message is acknowledged). +TEST_F(GCMAccountMapperTest, AddMappingMessageAcknowledged) { + Initialize(GCMAccountMapper::AccountMappings()); + gcm_driver().AddAppHandler(kGCMAccountMapperAppId, mapper()); + gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS); + + std::vector account_tokens; + GCMClient::AccountTokenInfo account_token = MakeAccountTokenInfo(kAccountId); + account_tokens.push_back(account_token); + mapper()->SetAccountTokens(account_tokens); + + clock()->SetNow(base::Time::Now()); + gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS); + clock()->SetNow(base::Time::Now()); + gcm_driver().AcknowledgeSend(gcm_driver().last_message_id()); + + EXPECT_EQ(account_token.email, gcm_driver().last_account_mapping().email); + EXPECT_EQ(account_token.account_id, + gcm_driver().last_account_mapping().account_id); + EXPECT_EQ(account_token.access_token, + gcm_driver().last_account_mapping().access_token); + EXPECT_EQ(AccountMapping::MAPPED, gcm_driver().last_account_mapping().status); + EXPECT_EQ(clock()->Now(), + gcm_driver().last_account_mapping().status_change_timestamp); + EXPECT_TRUE(gcm_driver().last_account_mapping().last_message_id.empty()); + + GCMAccountMapper::AccountMappings mappings = GetAccounts(); + GCMAccountMapper::AccountMappings::const_iterator iter = mappings.begin(); + EXPECT_EQ(account_token.email, iter->email); + EXPECT_EQ(account_token.account_id, iter->account_id); + EXPECT_EQ(account_token.access_token, iter->access_token); + EXPECT_EQ(AccountMapping::MAPPED, iter->status); + EXPECT_EQ(clock()->Now(), iter->status_change_timestamp); + EXPECT_TRUE(iter->last_message_id.empty()); +} + +// Tests status change form ADDING to MAPPED (When message was acknowledged, +// after Chrome was restarted). +TEST_F(GCMAccountMapperTest, AddMappingMessageAckedAfterRestart) { + Initialize(GCMAccountMapper::AccountMappings()); + gcm_driver().AddAppHandler(kGCMAccountMapperAppId, mapper()); + gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS); + + std::vector account_tokens; + GCMClient::AccountTokenInfo account_token = MakeAccountTokenInfo(kAccountId); + account_tokens.push_back(account_token); + mapper()->SetAccountTokens(account_tokens); + + clock()->SetNow(base::Time::Now()); + gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS); + + Restart(); + GCMAccountMapper::AccountMappings stored_mappings; + stored_mappings.push_back(gcm_driver().last_account_mapping()); + Initialize(stored_mappings); + gcm_driver().AddAppHandler(kGCMAccountMapperAppId, mapper()); + + clock()->SetNow(base::Time::Now()); + gcm_driver().AcknowledgeSend(gcm_driver().last_message_id()); + + EXPECT_EQ(account_token.email, gcm_driver().last_account_mapping().email); + EXPECT_EQ(account_token.account_id, + gcm_driver().last_account_mapping().account_id); + EXPECT_EQ(account_token.access_token, + gcm_driver().last_account_mapping().access_token); + EXPECT_EQ(AccountMapping::MAPPED, gcm_driver().last_account_mapping().status); + EXPECT_EQ(clock()->Now(), + gcm_driver().last_account_mapping().status_change_timestamp); + EXPECT_TRUE(gcm_driver().last_account_mapping().last_message_id.empty()); + + GCMAccountMapper::AccountMappings mappings = GetAccounts(); + GCMAccountMapper::AccountMappings::const_iterator iter = mappings.begin(); + EXPECT_EQ(account_token.email, iter->email); + EXPECT_EQ(account_token.account_id, iter->account_id); + EXPECT_EQ(account_token.access_token, iter->access_token); + EXPECT_EQ(AccountMapping::MAPPED, iter->status); + EXPECT_EQ(clock()->Now(), iter->status_change_timestamp); + EXPECT_TRUE(iter->last_message_id.empty()); +} + +// Tests a case when ADD message times out for a new account. +TEST_F(GCMAccountMapperTest, AddMappingMessageSendErrorForNewAccount) { + Initialize(GCMAccountMapper::AccountMappings()); + gcm_driver().AddAppHandler(kGCMAccountMapperAppId, mapper()); + gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS); + + std::vector account_tokens; + GCMClient::AccountTokenInfo account_token = MakeAccountTokenInfo(kAccountId); + account_tokens.push_back(account_token); + mapper()->SetAccountTokens(account_tokens); + + clock()->SetNow(base::Time::Now()); + gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS); + + clock()->SetNow(base::Time::Now()); + std::string old_message_id = gcm_driver().last_message_id(); + gcm_driver().MessageSendError(old_message_id); + + // No new message is sent because of the send error, as the token is stale. + // Because the account was new, the entry should be deleted. + EXPECT_EQ(old_message_id, gcm_driver().last_message_id()); + EXPECT_EQ(account_token.account_id, gcm_driver().last_removed_account_id()); + EXPECT_TRUE(GetAccounts().empty()); +} + +/// Tests a case when ADD message times out for a MAPPED account. +TEST_F(GCMAccountMapperTest, AddMappingMessageSendErrorForMappedAccount) { + // Start with one account that is mapped. + base::Time status_change_timestamp = base::Time::Now(); + AccountMapping mapping = + MakeAccountMapping(kAccountId, AccountMapping::MAPPED, + status_change_timestamp, "add_message_id"); + + GCMAccountMapper::AccountMappings stored_mappings; + stored_mappings.push_back(mapping); + Initialize(stored_mappings); + gcm_driver().AddAppHandler(kGCMAccountMapperAppId, mapper()); + gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS); + + clock()->SetNow(base::Time::Now()); + gcm_driver().MessageSendError("add_message_id"); + + // No new message is sent because of the send error, as the token is stale. + // Because the account was new, the entry should be deleted. + EXPECT_TRUE(gcm_driver().last_message_id().empty()); + + GCMAccountMapper::AccountMappings mappings = GetAccounts(); + GCMAccountMapper::AccountMappings::const_iterator iter = mappings.begin(); + EXPECT_EQ(mapping.email, iter->email); + EXPECT_EQ(mapping.account_id, iter->account_id); + EXPECT_EQ(mapping.access_token, iter->access_token); + EXPECT_EQ(AccountMapping::MAPPED, iter->status); + EXPECT_EQ(status_change_timestamp, iter->status_change_timestamp); + EXPECT_TRUE(iter->last_message_id.empty()); +} + +// Tests that a missing token for an account will trigger removing of that +// account. This test goes only until the message is passed to GCM. +TEST_F(GCMAccountMapperTest, RemoveMappingToMessageSent) { + // Start with one account that is mapped. + AccountMapping mapping = MakeAccountMapping( + kAccountId, AccountMapping::MAPPED, base::Time::Now(), std::string()); + + GCMAccountMapper::AccountMappings stored_mappings; + stored_mappings.push_back(mapping); + Initialize(stored_mappings); + gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS); + clock()->SetNow(base::Time::Now()); + + mapper()->SetAccountTokens(std::vector()); + + EXPECT_EQ(mapping.account_id, gcm_driver().last_account_mapping().account_id); + EXPECT_EQ(mapping.email, gcm_driver().last_account_mapping().email); + EXPECT_EQ(AccountMapping::REMOVING, + gcm_driver().last_account_mapping().status); + EXPECT_EQ(clock()->Now(), + gcm_driver().last_account_mapping().status_change_timestamp); + EXPECT_TRUE(gcm_driver().last_account_mapping().last_message_id.empty()); + + GCMAccountMapper::AccountMappings mappings = GetAccounts(); + GCMAccountMapper::AccountMappings::const_iterator iter = mappings.begin(); + EXPECT_EQ(mapping.email, iter->email); + EXPECT_EQ(mapping.account_id, iter->account_id); + EXPECT_EQ(mapping.access_token, iter->access_token); + EXPECT_EQ(AccountMapping::REMOVING, iter->status); + EXPECT_EQ(clock()->Now(), iter->status_change_timestamp); + EXPECT_TRUE(iter->last_message_id.empty()); +} + +// Tests that a missing token for an account will trigger removing of that +// account. This test goes until the message is queued by GCM. +TEST_F(GCMAccountMapperTest, RemoveMappingMessageQueued) { + // Start with one account that is mapped. + AccountMapping mapping = MakeAccountMapping( + kAccountId, AccountMapping::MAPPED, base::Time::Now(), std::string()); + + GCMAccountMapper::AccountMappings stored_mappings; + stored_mappings.push_back(mapping); + Initialize(stored_mappings); + gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS); + clock()->SetNow(base::Time::Now()); + base::Time status_change_timestamp = clock()->Now(); + + mapper()->SetAccountTokens(std::vector()); + clock()->SetNow(base::Time::Now()); + gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS); + + EXPECT_EQ(mapping.account_id, gcm_driver().last_account_mapping().account_id); + EXPECT_EQ(mapping.email, gcm_driver().last_account_mapping().email); + EXPECT_EQ(AccountMapping::REMOVING, + gcm_driver().last_account_mapping().status); + EXPECT_EQ(status_change_timestamp, + gcm_driver().last_account_mapping().status_change_timestamp); + EXPECT_TRUE(!gcm_driver().last_account_mapping().last_message_id.empty()); + + GCMAccountMapper::AccountMappings mappings = GetAccounts(); + GCMAccountMapper::AccountMappings::const_iterator iter = mappings.begin(); + EXPECT_EQ(mapping.email, iter->email); + EXPECT_EQ(mapping.account_id, iter->account_id); + EXPECT_EQ(mapping.access_token, iter->access_token); + EXPECT_EQ(AccountMapping::REMOVING, iter->status); + EXPECT_EQ(status_change_timestamp, iter->status_change_timestamp); + EXPECT_EQ(gcm_driver().last_account_mapping().last_message_id, + iter->last_message_id); +} + +// Tests that a missing token for an account will trigger removing of that +// account. This test goes until the message is acknowledged by GCM. +// This is a complete success scenario for account removal, and it end with +// account mapping being completely gone. +TEST_F(GCMAccountMapperTest, RemoveMappingMessageAcknowledged) { + // Start with one account that is mapped. + AccountMapping mapping = MakeAccountMapping( + kAccountId, AccountMapping::MAPPED, base::Time::Now(), std::string()); + + GCMAccountMapper::AccountMappings stored_mappings; + stored_mappings.push_back(mapping); + Initialize(stored_mappings); + gcm_driver().AddAppHandler(kGCMAccountMapperAppId, mapper()); + gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS); + clock()->SetNow(base::Time::Now()); + + mapper()->SetAccountTokens(std::vector()); + gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS); + gcm_driver().AcknowledgeSend(gcm_driver().last_message_id()); + + EXPECT_EQ(mapping.account_id, gcm_driver().last_removed_account_id()); + + GCMAccountMapper::AccountMappings mappings = GetAccounts(); + EXPECT_TRUE(mappings.empty()); +} + +// Tests that account removing proceeds, when a removing message is acked after +// Chrome was restarted. +TEST_F(GCMAccountMapperTest, RemoveMappingMessageAckedAfterRestart) { + // Start with one account that is mapped. + AccountMapping mapping = + MakeAccountMapping(kAccountId, AccountMapping::REMOVING, + base::Time::Now(), "remove_message_id"); + + GCMAccountMapper::AccountMappings stored_mappings; + stored_mappings.push_back(mapping); + Initialize(stored_mappings); + gcm_driver().AddAppHandler(kGCMAccountMapperAppId, mapper()); + + gcm_driver().AcknowledgeSend("remove_message_id"); + + EXPECT_EQ(mapping.account_id, gcm_driver().last_removed_account_id()); + + GCMAccountMapper::AccountMappings mappings = GetAccounts(); + EXPECT_TRUE(mappings.empty()); +} + +// Tests that account removing proceeds, when a removing message is acked after +// Chrome was restarted. +TEST_F(GCMAccountMapperTest, RemoveMappingMessageSendError) { + // Start with one account that is mapped. + base::Time status_change_timestamp = base::Time::Now(); + AccountMapping mapping = + MakeAccountMapping(kAccountId, AccountMapping::REMOVING, + status_change_timestamp, "remove_message_id"); + + GCMAccountMapper::AccountMappings stored_mappings; + stored_mappings.push_back(mapping); + Initialize(stored_mappings); + gcm_driver().AddAppHandler(kGCMAccountMapperAppId, mapper()); + + clock()->SetNow(base::Time::Now()); + gcm_driver().MessageSendError("remove_message_id"); + + EXPECT_TRUE(gcm_driver().last_removed_account_id().empty()); + + EXPECT_EQ(mapping.account_id, gcm_driver().last_account_mapping().account_id); + EXPECT_EQ(mapping.email, gcm_driver().last_account_mapping().email); + EXPECT_EQ(AccountMapping::REMOVING, + gcm_driver().last_account_mapping().status); + EXPECT_EQ(status_change_timestamp, + gcm_driver().last_account_mapping().status_change_timestamp); + // Message is not persisted, until send is completed. + EXPECT_TRUE(gcm_driver().last_account_mapping().last_message_id.empty()); + + GCMAccountMapper::AccountMappings mappings = GetAccounts(); + GCMAccountMapper::AccountMappings::const_iterator iter = mappings.begin(); + EXPECT_EQ(mapping.email, iter->email); + EXPECT_EQ(mapping.account_id, iter->account_id); + EXPECT_TRUE(iter->access_token.empty()); + EXPECT_EQ(AccountMapping::REMOVING, iter->status); + EXPECT_EQ(status_change_timestamp, iter->status_change_timestamp); + EXPECT_TRUE(iter->last_message_id.empty()); +} + +// Tests that, if a new token arrives when the adding message is in progress +// no new message is sent and account mapper still waits for the first one to +// complete. +TEST_F(GCMAccountMapperTest, TokenIsRefreshedWhenAdding) { + Initialize(GCMAccountMapper::AccountMappings()); + gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS); + + clock()->SetNow(base::Time::Now()); + std::vector account_tokens; + GCMClient::AccountTokenInfo account_token = MakeAccountTokenInfo(kAccountId); + account_tokens.push_back(account_token); + mapper()->SetAccountTokens(account_tokens); + DCHECK_EQ(CustomFakeGCMDriver::SEND_STARTED, gcm_driver().last_action()); + + clock()->SetNow(base::Time::Now()); + gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS); + DCHECK_EQ(CustomFakeGCMDriver::SEND_FINISHED, gcm_driver().last_action()); + + // Providing another token and clearing status. + gcm_driver().Clear(); + mapper()->SetAccountTokens(account_tokens); + DCHECK_EQ(CustomFakeGCMDriver::NONE, gcm_driver().last_action()); +} + +// Tests that, if a new token arrives when a removing message is in progress +// a new adding message is sent and while account mapping status is changed to +// mapped. If the original Removing message arrives it is discarded. +TEST_F(GCMAccountMapperTest, TokenIsRefreshedWhenRemoving) { + // Start with one account that is mapped. + AccountMapping mapping = MakeAccountMapping( + kAccountId, AccountMapping::MAPPED, base::Time::Now(), std::string()); + + GCMAccountMapper::AccountMappings stored_mappings; + stored_mappings.push_back(mapping); + Initialize(stored_mappings); + gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS); + clock()->SetNow(base::Time::Now()); + + // Remove the token to trigger a remove message to be sent + mapper()->SetAccountTokens(std::vector()); + EXPECT_EQ(CustomFakeGCMDriver::SEND_STARTED, gcm_driver().last_action()); + gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS); + EXPECT_EQ(CustomFakeGCMDriver::SEND_FINISHED, gcm_driver().last_action()); + + std::string remove_message_id = gcm_driver().last_message_id(); + gcm_driver().Clear(); + + // The account mapping for acc_id is now in status REMOVING. + // Adding the token for that account. + clock()->SetNow(base::Time::Now()); + std::vector account_tokens; + GCMClient::AccountTokenInfo account_token = MakeAccountTokenInfo(kAccountId); + account_tokens.push_back(account_token); + mapper()->SetAccountTokens(account_tokens); + DCHECK_EQ(CustomFakeGCMDriver::SEND_STARTED, gcm_driver().last_action()); + gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS); + EXPECT_EQ(CustomFakeGCMDriver::SEND_FINISHED, gcm_driver().last_action()); + + std::string add_message_id = gcm_driver().last_message_id(); + + // A remove message confirmation arrives now, but should be ignored. + gcm_driver().AcknowledgeSend(remove_message_id); + + GCMAccountMapper::AccountMappings mappings = GetAccounts(); + GCMAccountMapper::AccountMappings::const_iterator iter = mappings.begin(); + EXPECT_EQ(mapping.email, iter->email); + EXPECT_EQ(mapping.account_id, iter->account_id); + EXPECT_FALSE(iter->access_token.empty()); + EXPECT_EQ(AccountMapping::MAPPED, iter->status); + // Status change timestamp is set to very long time ago, to make sure the next + // round of mapping picks it up. + EXPECT_EQ(base::Time(), iter->status_change_timestamp); + EXPECT_EQ(add_message_id, iter->last_message_id); +} + +// Tests adding/removing works for multiple accounts, after a restart and when +// tokens are periodically delierverd. +TEST_F(GCMAccountMapperTest, MultipleAccountMappings) { + clock()->SetNow(base::Time::Now()); + base::Time half_hour_ago = clock()->Now() - base::Minutes(30); + GCMAccountMapper::AccountMappings stored_mappings; + stored_mappings.push_back(MakeAccountMapping( + kAccountId, AccountMapping::ADDING, half_hour_ago, "acc_id_msg")); + stored_mappings.push_back(MakeAccountMapping( + kAccountId1, AccountMapping::MAPPED, half_hour_ago, "acc_id_1_msg")); + stored_mappings.push_back(MakeAccountMapping( + kAccountId2, AccountMapping::REMOVING, half_hour_ago, "acc_id_2_msg")); + + Initialize(stored_mappings); + gcm_driver().AddAppHandler(kGCMAccountMapperAppId, mapper()); + gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS); + + GCMAccountMapper::AccountMappings expected_mappings(stored_mappings); + + // Finish messages after a restart. + clock()->SetNow(base::Time::Now()); + gcm_driver().AcknowledgeSend(expected_mappings[0].last_message_id); + expected_mappings[0].status_change_timestamp = clock()->Now(); + expected_mappings[0].status = AccountMapping::MAPPED; + expected_mappings[0].last_message_id.clear(); + + clock()->SetNow(base::Time::Now()); + gcm_driver().AcknowledgeSend(expected_mappings[1].last_message_id); + expected_mappings[1].status_change_timestamp = clock()->Now(); + expected_mappings[1].status = AccountMapping::MAPPED; + expected_mappings[1].last_message_id.clear(); + + // Upon success last element is removed. + clock()->SetNow(base::Time::Now()); + gcm_driver().AcknowledgeSend(expected_mappings[2].last_message_id); + expected_mappings.pop_back(); + + VerifyMappings(expected_mappings, GetAccounts(), "Step 1, After restart"); + + // One of accounts gets removed. + std::vector account_tokens; + account_tokens.push_back(MakeAccountTokenInfo(kAccountId)); + + // Advance a day to make sure existing mappings will be reported. + clock()->SetNow(clock()->Now() + base::Days(1)); + mapper()->SetAccountTokens(account_tokens); + + expected_mappings[0].status = AccountMapping::MAPPED; + expected_mappings[1].status = AccountMapping::REMOVING; + expected_mappings[1].status_change_timestamp = clock()->Now(); + + gcm_driver().CompleteSendAllMessages(); + + VerifyMappings( + expected_mappings, GetAccounts(), "Step 2, One account is being removed"); + + clock()->SetNow(clock()->Now() + base::Seconds(5)); + gcm_driver().AcknowledgeSendAllMessages(); + + expected_mappings[0].status_change_timestamp = clock()->Now(); + expected_mappings.pop_back(); + + VerifyMappings( + expected_mappings, GetAccounts(), "Step 3, Removing completed"); + + account_tokens.clear(); + account_tokens.push_back(MakeAccountTokenInfo(kAccountId)); + account_tokens.push_back(MakeAccountTokenInfo(kAccountId3)); + account_tokens.push_back(MakeAccountTokenInfo(kAccountId4)); + + // Advance a day to make sure existing mappings will be reported. + clock()->SetNow(clock()->Now() + base::Days(1)); + mapper()->SetAccountTokens(account_tokens); + + // Mapping from acc_id_0 still in position 0 + expected_mappings.push_back(MakeAccountMapping( + kAccountId3, AccountMapping::NEW, base::Time(), std::string())); + expected_mappings.push_back(MakeAccountMapping( + kAccountId4, AccountMapping::NEW, base::Time(), std::string())); + + VerifyMappings(expected_mappings, GetAccounts(), "Step 4, Two new accounts"); + + clock()->SetNow(clock()->Now() + base::Seconds(1)); + gcm_driver().CompleteSendAllMessages(); + + expected_mappings[1].status = AccountMapping::ADDING; + expected_mappings[1].status_change_timestamp = clock()->Now(); + expected_mappings[2].status = AccountMapping::ADDING; + expected_mappings[2].status_change_timestamp = clock()->Now(); + + VerifyMappings( + expected_mappings, GetAccounts(), "Step 5, Two accounts being added"); + + clock()->SetNow(clock()->Now() + base::Seconds(5)); + gcm_driver().AcknowledgeSendAllMessages(); + + expected_mappings[0].status_change_timestamp = clock()->Now(); + expected_mappings[1].status_change_timestamp = clock()->Now(); + expected_mappings[1].status = AccountMapping::MAPPED; + expected_mappings[2].status_change_timestamp = clock()->Now(); + expected_mappings[2].status = AccountMapping::MAPPED; + + VerifyMappings( + expected_mappings, GetAccounts(), "Step 6, Three mapped accounts"); +} + +TEST_F(GCMAccountMapperTest, DispatchMessageSentToGaiaID) { + Initialize(GCMAccountMapper::AccountMappings()); + gcm_driver().AddAppHandler(kGCMAccountMapperAppId, mapper()); + IncomingMessage message; + message.data[kEmbeddedAppIdKey] = kTestAppId; + message.data[kTestDataKey] = kTestDataValue; + message.collapse_key = kTestCollapseKey; + message.sender_id = kTestSenderId; + mapper()->OnMessage(kGCMAccountMapperAppId, message); + + EXPECT_EQ(kTestAppId, last_received_app_id()); + EXPECT_EQ(1UL, last_received_message().data.size()); + auto it = last_received_message().data.find(kTestDataKey); + EXPECT_TRUE(it != last_received_message().data.end()); + EXPECT_EQ(kTestDataValue, it->second); + EXPECT_EQ(kTestCollapseKey, last_received_message().collapse_key); + EXPECT_EQ(kTestSenderId, last_received_message().sender_id); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_account_tracker.cc b/chromium/components/gcm_driver/gcm_account_tracker.cc new file mode 100644 index 00000000000..1fed7bf7d6e --- /dev/null +++ b/chromium/components/gcm_driver/gcm_account_tracker.cc @@ -0,0 +1,343 @@ +// 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 "components/gcm_driver/gcm_account_tracker.h" + +#include + +#include +#include + +#include "base/bind.h" +#include "base/location.h" +#include "base/task/single_thread_task_runner.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "components/gcm_driver/gcm_driver.h" +#include "components/signin/public/identity_manager/access_token_fetcher.h" +#include "components/signin/public/identity_manager/access_token_info.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/scope_set.h" +#include "google_apis/gaia/gaia_constants.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "net/base/ip_endpoint.h" + +namespace gcm { + +namespace { + +// Name of the GCM account tracker for fetching access tokens. +const char kGCMAccountTrackerName[] = "gcm_account_tracker"; +// Minimum token validity when sending to GCM groups server. +const int64_t kMinimumTokenValidityMs = 500; +// Token reporting interval, when no account changes are detected. +const int64_t kTokenReportingIntervalMs = + 12 * 60 * 60 * 1000; // 12 hours in ms. + +} // namespace + +GCMAccountTracker::AccountInfo::AccountInfo(const std::string& email, + AccountState state) + : email(email), state(state) { +} + +GCMAccountTracker::AccountInfo::~AccountInfo() { +} + +GCMAccountTracker::GCMAccountTracker( + std::unique_ptr account_tracker, + signin::IdentityManager* identity_manager, + GCMDriver* driver) + : account_tracker_(account_tracker.release()), + identity_manager_(identity_manager), + driver_(driver), + shutdown_called_(false) {} + +GCMAccountTracker::~GCMAccountTracker() { + DCHECK(shutdown_called_); +} + +void GCMAccountTracker::Shutdown() { + shutdown_called_ = true; + driver_->RemoveConnectionObserver(this); + account_tracker_->RemoveObserver(this); + account_tracker_->Shutdown(); +} + +void GCMAccountTracker::Start() { + DCHECK(!shutdown_called_); + account_tracker_->AddObserver(this); + driver_->AddConnectionObserver(this); + + std::vector accounts = account_tracker_->GetAccounts(); + for (std::vector::const_iterator iter = accounts.begin(); + iter != accounts.end(); ++iter) { + if (!iter->email.empty()) { + account_infos_.insert(std::make_pair( + iter->account_id, AccountInfo(iter->email, TOKEN_NEEDED))); + } + } + + if (IsTokenReportingRequired()) + ReportTokens(); + else + ScheduleReportTokens(); +} + +void GCMAccountTracker::ScheduleReportTokens() { + // Shortcutting here, in case GCM Driver is not yet connected. In that case + // reporting will be scheduled/started when the connection is made. + if (!driver_->IsConnected()) + return; + + DVLOG(1) << "Deferring the token reporting for: " + << GetTimeToNextTokenReporting().InSeconds() << " seconds."; + + reporting_weak_ptr_factory_.InvalidateWeakPtrs(); + base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, + base::BindOnce(&GCMAccountTracker::ReportTokens, + reporting_weak_ptr_factory_.GetWeakPtr()), + GetTimeToNextTokenReporting()); +} + +void GCMAccountTracker::OnAccountSignInChanged(const CoreAccountInfo& account, + bool is_signed_in) { + if (is_signed_in) + OnAccountSignedIn(account); + else + OnAccountSignedOut(account); +} + +void GCMAccountTracker::OnAccessTokenFetchCompleteForAccount( + CoreAccountId account_id, + GoogleServiceAuthError error, + signin::AccessTokenInfo access_token_info) { + auto iter = account_infos_.find(account_id); + DCHECK(iter != account_infos_.end()); + if (iter != account_infos_.end()) { + DCHECK_EQ(GETTING_TOKEN, iter->second.state); + + if (error.state() == GoogleServiceAuthError::NONE) { + DVLOG(1) << "Get token success: " << account_id; + + iter->second.state = TOKEN_PRESENT; + iter->second.access_token = access_token_info.token; + iter->second.expiration_time = access_token_info.expiration_time; + } else { + DVLOG(1) << "Get token failure: " << account_id; + + // Given the fetcher has a built in retry logic, consider this situation + // to be invalid refresh token, that is only fixed when user signs in. + // Once the users signs in properly the minting will retry. + iter->second.access_token.clear(); + iter->second.state = ACCOUNT_REMOVED; + } + } + + pending_token_requests_.erase(account_id); + ReportTokens(); +} + +void GCMAccountTracker::OnConnected(const net::IPEndPoint& ip_endpoint) { + // We are sure here, that GCM is running and connected. We can start reporting + // tokens if reporting is due now, or schedule reporting for later. + if (IsTokenReportingRequired()) + ReportTokens(); + else + ScheduleReportTokens(); +} + +void GCMAccountTracker::OnDisconnected() { + // We are disconnected, so no point in trying to work with tokens. +} + +void GCMAccountTracker::ReportTokens() { + SanitizeTokens(); + // Make sure all tokens are valid. + if (IsTokenFetchingRequired()) { + GetAllNeededTokens(); + return; + } + + // Wait for all of the pending token requests from GCMAccountTracker to be + // done before you report the results. + if (!pending_token_requests_.empty()) { + return; + } + + bool account_removed = false; + // Stop tracking the accounts, that were removed, as it will be reported to + // the driver. + for (auto iter = account_infos_.begin(); iter != account_infos_.end();) { + if (iter->second.state == ACCOUNT_REMOVED) { + account_removed = true; + account_infos_.erase(iter++); + } else { + ++iter; + } + } + + std::vector account_tokens; + for (auto iter = account_infos_.begin(); iter != account_infos_.end(); + ++iter) { + if (iter->second.state == TOKEN_PRESENT) { + GCMClient::AccountTokenInfo token_info; + token_info.account_id = iter->first; + token_info.email = iter->second.email; + token_info.access_token = iter->second.access_token; + account_tokens.push_back(token_info); + } else { + // This should not happen, as we are making a check that there are no + // pending requests above, stopping tracking of removed accounts, or start + // fetching tokens. + NOTREACHED(); + } + } + + // Make sure that there is something to report, otherwise bail out. + if (!account_tokens.empty() || account_removed) { + DVLOG(1) << "Reporting the tokens to driver: " << account_tokens.size(); + driver_->SetAccountTokens(account_tokens); + driver_->SetLastTokenFetchTime(base::Time::Now()); + ScheduleReportTokens(); + } else { + DVLOG(1) << "No tokens and nothing removed. Skipping callback."; + } +} + +void GCMAccountTracker::SanitizeTokens() { + for (auto iter = account_infos_.begin(); iter != account_infos_.end(); + ++iter) { + if (iter->second.state == TOKEN_PRESENT && + iter->second.expiration_time < + base::Time::Now() + base::Milliseconds(kMinimumTokenValidityMs)) { + iter->second.access_token.clear(); + iter->second.state = TOKEN_NEEDED; + iter->second.expiration_time = base::Time(); + } + } +} + +bool GCMAccountTracker::IsTokenReportingRequired() const { + if (GetTimeToNextTokenReporting().is_zero()) + return true; + + bool reporting_required = false; + for (auto iter = account_infos_.begin(); iter != account_infos_.end(); + ++iter) { + if (iter->second.state == ACCOUNT_REMOVED) + reporting_required = true; + } + + return reporting_required; +} + +bool GCMAccountTracker::IsTokenFetchingRequired() const { + bool token_needed = false; + for (auto iter = account_infos_.begin(); iter != account_infos_.end(); + ++iter) { + if (iter->second.state == TOKEN_NEEDED) + token_needed = true; + } + + return token_needed; +} + +base::TimeDelta GCMAccountTracker::GetTimeToNextTokenReporting() const { + base::TimeDelta time_till_next_reporting = + driver_->GetLastTokenFetchTime() + + base::Milliseconds(kTokenReportingIntervalMs) - base::Time::Now(); + + // Case when token fetching is overdue. + if (time_till_next_reporting.is_negative()) + return base::TimeDelta(); + + // Case when calculated period is larger than expected, including the + // situation when the method is called before GCM driver is completely + // initialized. + if (time_till_next_reporting > + base::Milliseconds(kTokenReportingIntervalMs)) { + return base::Milliseconds(kTokenReportingIntervalMs); + } + + return time_till_next_reporting; +} + +void GCMAccountTracker::GetAllNeededTokens() { + // Only start fetching tokens if driver is running, they have a limited + // validity time and GCM connection is a good indication of network running. + // If the GetAllNeededTokens was called as part of periodic schedule, it may + // not have network. In that case the next network change will trigger token + // fetching. + if (!driver_->IsConnected()) + return; + + // Only start fetching access tokens if the user consented for sync. + if (!identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync)) + return; + + for (auto iter = account_infos_.begin(); iter != account_infos_.end(); + ++iter) { + if (iter->second.state == TOKEN_NEEDED) + GetToken(iter); + } +} + +void GCMAccountTracker::GetToken(AccountInfos::iterator& account_iter) { + DCHECK_EQ(account_iter->second.state, TOKEN_NEEDED); + + signin::ScopeSet scopes; + scopes.insert(GaiaConstants::kGCMGroupServerOAuth2Scope); + scopes.insert(GaiaConstants::kGCMCheckinServerOAuth2Scope); + + // NOTE: It is safe to use base::Unretained() here as |token_fetcher| is owned + // by this object and guarantees that it will not invoke its callback after + // its destruction. + std::unique_ptr token_fetcher = + identity_manager_->CreateAccessTokenFetcherForAccount( + account_iter->first, kGCMAccountTrackerName, scopes, + base::BindOnce( + &GCMAccountTracker::OnAccessTokenFetchCompleteForAccount, + base::Unretained(this), account_iter->first), + signin::AccessTokenFetcher::Mode::kImmediate); + + DCHECK(pending_token_requests_.count(account_iter->first) == 0); + pending_token_requests_.emplace(account_iter->first, + std::move(token_fetcher)); + account_iter->second.state = GETTING_TOKEN; +} + +void GCMAccountTracker::OnAccountSignedIn(const CoreAccountInfo& account) { + DVLOG(1) << "Account signed in: " << account.email; + auto iter = account_infos_.find(account.account_id); + if (iter == account_infos_.end()) { + DCHECK(!account.email.empty()); + account_infos_.insert(std::make_pair( + account.account_id, AccountInfo(account.email, TOKEN_NEEDED))); + } else if (iter->second.state == ACCOUNT_REMOVED) { + iter->second.state = TOKEN_NEEDED; + } + + GetAllNeededTokens(); +} + +void GCMAccountTracker::OnAccountSignedOut(const CoreAccountInfo& account) { + DVLOG(1) << "Account signed out: " << account.email; + auto iter = account_infos_.find(account.account_id); + if (iter == account_infos_.end()) + return; + + iter->second.access_token.clear(); + iter->second.state = ACCOUNT_REMOVED; + + // Delete any ongoing access token request now so that if the account is later + // re-added and a new access token request made, we do not break this class' + // invariant that there is at most one ongoing access token request per + // account. + pending_token_requests_.erase(account.account_id); + ReportTokens(); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_account_tracker.h b/chromium/components/gcm_driver/gcm_account_tracker.h new file mode 100644 index 00000000000..91d2fd3667d --- /dev/null +++ b/chromium/components/gcm_driver/gcm_account_tracker.h @@ -0,0 +1,172 @@ +// 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 COMPONENTS_GCM_DRIVER_GCM_ACCOUNT_TRACKER_H_ +#define COMPONENTS_GCM_DRIVER_GCM_ACCOUNT_TRACKER_H_ + +#include + +#include +#include +#include + +#include "base/memory/raw_ptr.h" +#include "components/gcm_driver/account_tracker.h" +#include "components/gcm_driver/gcm_client.h" +#include "components/gcm_driver/gcm_connection_observer.h" +#include "components/signin/public/identity_manager/access_token_fetcher.h" + +namespace signin { +struct AccessTokenInfo; +class IdentityManager; +} // namespace signin + +namespace base { +class Time; +} + +namespace gcm { + +class GCMDriver; + +// Class for reporting back which accounts are signed into. It is only meant to +// be used when the user is signed into sync. +// +// This class makes a check for tokens periodically, to make sure the user is +// still logged into the profile, so that in the case that the user is not, we +// can immediately report that to the GCM and stop messages addressed to that +// user from ever reaching Chrome. +class GCMAccountTracker : public AccountTracker::Observer, + public GCMConnectionObserver { + public: + // State of the account. + // Allowed transitions: + // TOKEN_NEEDED - account info was created. + // TOKEN_NEEDED -> GETTING_TOKEN - access token was requested. + // GETTING_TOKEN -> TOKEN_NEEDED - access token fetching failed. + // GETTING_TOKEN -> TOKEN_PRESENT - access token fetching succeeded. + // GETTING_TOKEN -> ACCOUNT_REMOVED - account was removed. + // TOKEN_NEEDED -> ACCOUNT_REMOVED - account was removed. + // TOKEN_PRESENT -> ACCOUNT_REMOVED - account was removed. + enum AccountState { + TOKEN_NEEDED, // Needs a token (AccountInfo was recently created or + // token request failed). + GETTING_TOKEN, // There is a pending token request. + TOKEN_PRESENT, // We have a token for the account. + ACCOUNT_REMOVED, // Account was removed, and we didn't report it yet. + }; + + // Stores necessary account information and state of token fetching. + struct AccountInfo { + AccountInfo(const std::string& email, AccountState state); + ~AccountInfo(); + + // Email address of the tracked account. + std::string email; + // OAuth2 access token, when |state| is TOKEN_PRESENT. + std::string access_token; + // Expiration time of the access tokens. + base::Time expiration_time; + // Status of the token fetching. + AccountState state; + }; + + // |account_tracker| is used to deliver information about the accounts present + // in the browser context to |driver|. + GCMAccountTracker(std::unique_ptr account_tracker, + signin::IdentityManager* identity_manager, + GCMDriver* driver); + + GCMAccountTracker(const GCMAccountTracker&) = delete; + GCMAccountTracker& operator=(const GCMAccountTracker&) = delete; + + ~GCMAccountTracker() override; + + // Shuts down the tracker ensuring a proper clean up. After Shutdown() is + // called Start() and Stop() should no longer be used. Must be called before + // destruction. + void Shutdown(); + + // Starts tracking accounts. + void Start(); + + // Gets the number of pending token requests. Only used for testing. + size_t get_pending_token_request_count() const { + return pending_token_requests_.size(); + } + + private: + friend class GCMAccountTrackerTest; + + // Maps account keys to account states. Keyed by account_id as used by + // IdentityManager. + typedef std::map AccountInfos; + + // AccountTracker::Observer overrides. + void OnAccountSignInChanged(const CoreAccountInfo& account, + bool is_signed_in) override; + + void OnAccessTokenFetchCompleteForAccount( + CoreAccountId account_id, + GoogleServiceAuthError error, + signin::AccessTokenInfo access_token_info); + + // GCMConnectionObserver overrides. + void OnConnected(const net::IPEndPoint& ip_endpoint) override; + void OnDisconnected() override; + + // Schedules token reporting. + void ScheduleReportTokens(); + // Report the list of accounts with OAuth2 tokens back using the |callback_| + // function. If there are token requests in progress, do nothing. + void ReportTokens(); + // Verify that all of the tokens are ready to be passed down to the GCM + // Driver, e.g. none of them has expired or is missing. Returns true if not + // all tokens are valid and a fetching yet more tokens is required. + void SanitizeTokens(); + // Indicates whether token reporting is required, either because it is due, or + // some of the accounts were removed. + bool IsTokenReportingRequired() const; + // Indicates whether there are tokens that still need fetching. + bool IsTokenFetchingRequired() const; + // Gets the time until next token reporting. + base::TimeDelta GetTimeToNextTokenReporting() const; + // Checks on all known accounts, and calls GetToken(..) for those with + // |state == TOKEN_NEEDED|. + void GetAllNeededTokens(); + // Starts fetching the OAuth2 token for the GCM group scope. + void GetToken(AccountInfos::iterator& account_iter); + + // Handling of actual sign in and sign out for accounts. + void OnAccountSignedIn(const CoreAccountInfo& account); + void OnAccountSignedOut(const CoreAccountInfo& account); + + // Account tracker. + std::unique_ptr account_tracker_; + + raw_ptr identity_manager_; + + raw_ptr driver_; + + // State of the account. + AccountInfos account_infos_; + + // Indicates whether shutdown has been called. + bool shutdown_called_; + + // Stores the ongoing access token fetchers for deletion either upon + // completion or upon signout of the account for which the request is being + // made. + using AccountIDToTokenFetcherMap = + std::map>; + AccountIDToTokenFetcherMap pending_token_requests_; + + // Creates weak pointers used to postpone reporting tokens. See + // ScheduleReportTokens. + base::WeakPtrFactory reporting_weak_ptr_factory_{this}; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_GCM_ACCOUNT_TRACKER_H_ diff --git a/chromium/components/gcm_driver/gcm_account_tracker_unittest.cc b/chromium/components/gcm_driver/gcm_account_tracker_unittest.cc new file mode 100644 index 00000000000..c2ccf550972 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_account_tracker_unittest.cc @@ -0,0 +1,534 @@ +// 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 "components/gcm_driver/gcm_account_tracker.h" + +#include +#include +#include +#include + +#include "base/memory/raw_ptr.h" +#include "base/test/task_environment.h" +#include "build/chromeos_buildflags.h" +#include "components/gcm_driver/fake_gcm_driver.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "net/base/ip_endpoint.h" +#include "net/http/http_status_code.h" +#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" +#include "services/network/test/test_url_loader_factory.h" +#include "services/network/test/test_utils.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { + +namespace { + +const char kEmail1[] = "account_1@me.com"; +const char kEmail2[] = "account_2@me.com"; + +std::string MakeAccessToken(const CoreAccountId& account_id) { + return "access_token-" + account_id.ToString(); +} + +GCMClient::AccountTokenInfo MakeAccountToken(const CoreAccountInfo& account) { + GCMClient::AccountTokenInfo token_info; + token_info.account_id = account.account_id; + + // TODO(https://crbug.com/856170): This *should* be expected to be the email + // address for the given account, but there is a bug in AccountTracker that + // means that |token_info.email| actually gets populated with the account ID + // by the production code. Hence the test expectation has to match what the + // production code actually does :). If/when that bug gets fixed, this + // function should be changed to take in the email address as well as the + // account ID and populate this field with the email address. + token_info.email = account.email; + token_info.access_token = MakeAccessToken(account.account_id); + return token_info; +} + +void VerifyAccountTokens( + const std::vector& expected_tokens, + const std::vector& actual_tokens) { + EXPECT_EQ(expected_tokens.size(), actual_tokens.size()); + for (auto expected_iter = expected_tokens.begin(), + actual_iter = actual_tokens.begin(); + expected_iter != expected_tokens.end() && + actual_iter != actual_tokens.end(); + ++expected_iter, ++actual_iter) { + EXPECT_EQ(expected_iter->account_id, actual_iter->account_id); + EXPECT_EQ(expected_iter->email, actual_iter->email); + EXPECT_EQ(expected_iter->access_token, actual_iter->access_token); + } +} + +// This version of FakeGCMDriver is customized around handling accounts and +// connection events for testing GCMAccountTracker. +class CustomFakeGCMDriver : public FakeGCMDriver { + public: + CustomFakeGCMDriver(); + + CustomFakeGCMDriver(const CustomFakeGCMDriver&) = delete; + CustomFakeGCMDriver& operator=(const CustomFakeGCMDriver&) = delete; + + ~CustomFakeGCMDriver() override; + + // GCMDriver overrides: + void SetAccountTokens( + const std::vector& account_tokens) override; + void AddConnectionObserver(GCMConnectionObserver* observer) override; + void RemoveConnectionObserver(GCMConnectionObserver* observer) override; + bool IsConnected() const override { return connected_; } + base::Time GetLastTokenFetchTime() override; + void SetLastTokenFetchTime(const base::Time& time) override; + + // Test results and helpers. + void SetConnected(bool connected); + void ResetResults(); + bool update_accounts_called() const { return update_accounts_called_; } + const std::vector& accounts() const { + return accounts_; + } + const GCMConnectionObserver* last_connection_observer() const { + return last_connection_observer_; + } + const GCMConnectionObserver* last_removed_connection_observer() const { + return removed_connection_observer_; + } + + private: + bool connected_; + std::vector accounts_; + bool update_accounts_called_; + raw_ptr last_connection_observer_; + raw_ptr removed_connection_observer_; + net::IPEndPoint ip_endpoint_; + base::Time last_token_fetch_time_; +}; + +CustomFakeGCMDriver::CustomFakeGCMDriver() + : connected_(true), + update_accounts_called_(false), + last_connection_observer_(nullptr), + removed_connection_observer_(nullptr) {} + +CustomFakeGCMDriver::~CustomFakeGCMDriver() { +} + +void CustomFakeGCMDriver::SetAccountTokens( + const std::vector& accounts) { + update_accounts_called_ = true; + accounts_ = accounts; +} + +void CustomFakeGCMDriver::AddConnectionObserver( + GCMConnectionObserver* observer) { + last_connection_observer_ = observer; +} + +void CustomFakeGCMDriver::RemoveConnectionObserver( + GCMConnectionObserver* observer) { + removed_connection_observer_ = observer; +} + +void CustomFakeGCMDriver::SetConnected(bool connected) { + connected_ = connected; + if (connected && last_connection_observer_) + last_connection_observer_->OnConnected(ip_endpoint_); +} + +void CustomFakeGCMDriver::ResetResults() { + accounts_.clear(); + update_accounts_called_ = false; + last_connection_observer_ = nullptr; + removed_connection_observer_ = nullptr; +} + + +base::Time CustomFakeGCMDriver::GetLastTokenFetchTime() { + return last_token_fetch_time_; +} + +void CustomFakeGCMDriver::SetLastTokenFetchTime(const base::Time& time) { + last_token_fetch_time_ = time; +} + +} // namespace + +class GCMAccountTrackerTest : public testing::Test { + public: + GCMAccountTrackerTest(); + ~GCMAccountTrackerTest() override; + + // Helpers to pass fake info to the tracker. + CoreAccountInfo AddAccount(const std::string& email); + CoreAccountInfo SetPrimaryAccount(const std::string& email); + void ClearPrimaryAccount(); + void RemoveAccount(const CoreAccountId& account_id); + + // Helpers for dealing with OAuth2 access token requests. + void IssueAccessToken(const CoreAccountId& account_id); + void IssueExpiredAccessToken(const CoreAccountId& account_id); + void IssueError(const CoreAccountId& account_id); + + // Accessors to account tracker and gcm driver. + GCMAccountTracker* tracker() { return tracker_.get(); } + CustomFakeGCMDriver* driver() { return &driver_; } + + // Accessors to private methods of account tracker. + bool IsFetchingRequired() const; + bool IsTokenReportingRequired() const; + base::TimeDelta GetTimeToNextTokenReporting() const; + + network::TestURLLoaderFactory* test_url_loader_factory() { + return &test_url_loader_factory_; + } + + private: + CustomFakeGCMDriver driver_; + + base::test::SingleThreadTaskEnvironment task_environment_; + network::TestURLLoaderFactory test_url_loader_factory_; + signin::IdentityTestEnvironment identity_test_env_; + + std::unique_ptr tracker_; +}; + +GCMAccountTrackerTest::GCMAccountTrackerTest() { + std::unique_ptr gaia_account_tracker( + new AccountTracker(identity_test_env_.identity_manager())); + + tracker_ = std::make_unique( + std::move(gaia_account_tracker), identity_test_env_.identity_manager(), + &driver_); +} + +GCMAccountTrackerTest::~GCMAccountTrackerTest() { + if (tracker_) + tracker_->Shutdown(); +} + +CoreAccountInfo GCMAccountTrackerTest::AddAccount(const std::string& email) { + return identity_test_env_.MakeAccountAvailable(email); +} + +CoreAccountInfo GCMAccountTrackerTest::SetPrimaryAccount( + const std::string& email) { + // NOTE: Setting of the primary account info must be done first on ChromeOS + // to ensure that AccountTracker and GCMAccountTracker respond as expected + // when the token is added to the token service. + // TODO(blundell): On non-ChromeOS, it would be good to add tests wherein + // setting of the primary account is done afterward to check that the flow + // that ensues from the GoogleSigninSucceeded callback firing works as + // expected. + return identity_test_env_.MakePrimaryAccountAvailable( + email, signin::ConsentLevel::kSync); +} + +void GCMAccountTrackerTest::ClearPrimaryAccount() { + identity_test_env_.ClearPrimaryAccount(); +} + +void GCMAccountTrackerTest::RemoveAccount(const CoreAccountId& account_id) { + identity_test_env_.RemoveRefreshTokenForAccount(account_id); +} + +void GCMAccountTrackerTest::IssueAccessToken(const CoreAccountId& account_id) { + identity_test_env_.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken( + account_id, MakeAccessToken(account_id), base::Time::Max()); +} + +void GCMAccountTrackerTest::IssueExpiredAccessToken( + const CoreAccountId& account_id) { + identity_test_env_.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken( + account_id, MakeAccessToken(account_id), base::Time::Now()); +} + +void GCMAccountTrackerTest::IssueError(const CoreAccountId& account_id) { + identity_test_env_.WaitForAccessTokenRequestIfNecessaryAndRespondWithError( + account_id, + GoogleServiceAuthError(GoogleServiceAuthError::SERVICE_UNAVAILABLE)); +} + +bool GCMAccountTrackerTest::IsFetchingRequired() const { + return tracker_->IsTokenFetchingRequired(); +} + +bool GCMAccountTrackerTest::IsTokenReportingRequired() const { + return tracker_->IsTokenReportingRequired(); +} + +base::TimeDelta GCMAccountTrackerTest::GetTimeToNextTokenReporting() const { + return tracker_->GetTimeToNextTokenReporting(); +} + +TEST_F(GCMAccountTrackerTest, NoAccounts) { + EXPECT_FALSE(driver()->update_accounts_called()); + tracker()->Start(); + // Callback should not be called if there where no accounts provided. + EXPECT_FALSE(driver()->update_accounts_called()); + EXPECT_TRUE(driver()->accounts().empty()); +} + +// Verifies that callback is called after a token is issued for a single account +// with a specific scope. In this scenario, the underlying account tracker is +// still working when the CompleteCollectingTokens is called for the first time. +TEST_F(GCMAccountTrackerTest, SingleAccount) { + CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); + + tracker()->Start(); + EXPECT_FALSE(driver()->update_accounts_called()); + + IssueAccessToken(account1.account_id); + EXPECT_TRUE(driver()->update_accounts_called()); + + std::vector expected_accounts; + expected_accounts.push_back(MakeAccountToken(account1)); + VerifyAccountTokens(expected_accounts, driver()->accounts()); +} + +TEST_F(GCMAccountTrackerTest, MultipleAccounts) { + CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); + CoreAccountInfo account2 = AddAccount(kEmail2); + + tracker()->Start(); + EXPECT_FALSE(driver()->update_accounts_called()); + + IssueAccessToken(account1.account_id); + EXPECT_FALSE(driver()->update_accounts_called()); + + IssueAccessToken(account2.account_id); + EXPECT_TRUE(driver()->update_accounts_called()); + + std::vector expected_accounts; + expected_accounts.push_back(MakeAccountToken(account1)); + expected_accounts.push_back(MakeAccountToken(account2)); + VerifyAccountTokens(expected_accounts, driver()->accounts()); +} + +TEST_F(GCMAccountTrackerTest, AccountAdded) { + tracker()->Start(); + driver()->ResetResults(); + + CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); + EXPECT_FALSE(driver()->update_accounts_called()); + + IssueAccessToken(account1.account_id); + EXPECT_TRUE(driver()->update_accounts_called()); + + std::vector expected_accounts; + expected_accounts.push_back(MakeAccountToken(account1)); + VerifyAccountTokens(expected_accounts, driver()->accounts()); +} + +TEST_F(GCMAccountTrackerTest, AccountRemoved) { + CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); + CoreAccountInfo account2 = AddAccount(kEmail2); + + tracker()->Start(); + IssueAccessToken(account1.account_id); + IssueAccessToken(account2.account_id); + EXPECT_TRUE(driver()->update_accounts_called()); + + driver()->ResetResults(); + EXPECT_FALSE(driver()->update_accounts_called()); + + RemoveAccount(account2.account_id); + EXPECT_TRUE(driver()->update_accounts_called()); + + std::vector expected_accounts; + expected_accounts.push_back(MakeAccountToken(account1)); + VerifyAccountTokens(expected_accounts, driver()->accounts()); +} + +#if !BUILDFLAG(IS_CHROMEOS_ASH) +// Tests that clearing the primary account when having multiple accounts +// does not crash the application. +// Regression test for crbug.com/1234406 +TEST_F(GCMAccountTrackerTest, AccountRemovedWithoutSyncConsentNoCrash) { + CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); + CoreAccountInfo account2 = AddAccount(kEmail2); + + // Set last fetch time to now so that access token fetch is not required + // but not started. + driver()->SetLastTokenFetchTime(base::Time::Now()); + tracker()->Start(); + EXPECT_FALSE(driver()->update_accounts_called()); + + // Reset the last fetch time to verify that clearing the primary account + // will not trigger a token fetch. + driver()->SetLastTokenFetchTime(base::Time()); + EXPECT_EQ(base::TimeDelta(), GetTimeToNextTokenReporting()); + ClearPrimaryAccount(); + EXPECT_TRUE(driver()->update_accounts_called()); +} +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) + +TEST_F(GCMAccountTrackerTest, GetTokenFailed) { + CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); + CoreAccountInfo account2 = AddAccount(kEmail2); + + tracker()->Start(); + IssueAccessToken(account1.account_id); + EXPECT_FALSE(driver()->update_accounts_called()); + + IssueError(account2.account_id); + + // Failed token is not retried any more. Account marked as removed. + EXPECT_EQ(0UL, tracker()->get_pending_token_request_count()); + EXPECT_TRUE(driver()->update_accounts_called()); + + std::vector expected_accounts; + expected_accounts.push_back(MakeAccountToken(account1)); + VerifyAccountTokens(expected_accounts, driver()->accounts()); +} + +TEST_F(GCMAccountTrackerTest, GetTokenFailedAccountRemoved) { + CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); + CoreAccountInfo account2 = AddAccount(kEmail2); + + tracker()->Start(); + IssueAccessToken(account1.account_id); + + driver()->ResetResults(); + RemoveAccount(account2.account_id); + IssueError(account2.account_id); + + EXPECT_TRUE(driver()->update_accounts_called()); + + std::vector expected_accounts; + expected_accounts.push_back(MakeAccountToken(account1)); + VerifyAccountTokens(expected_accounts, driver()->accounts()); +} + +TEST_F(GCMAccountTrackerTest, AccountRemovedWhileRequestsPending) { + CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); + CoreAccountInfo account2 = AddAccount(kEmail2); + + tracker()->Start(); + IssueAccessToken(account1.account_id); + EXPECT_FALSE(driver()->update_accounts_called()); + + RemoveAccount(account2.account_id); + IssueAccessToken(account2.account_id); + EXPECT_TRUE(driver()->update_accounts_called()); + + std::vector expected_accounts; + expected_accounts.push_back(MakeAccountToken(account1)); + VerifyAccountTokens(expected_accounts, driver()->accounts()); +} + +// Makes sure that tracker observes GCM connection when running. +TEST_F(GCMAccountTrackerTest, TrackerObservesConnection) { + EXPECT_EQ(nullptr, driver()->last_connection_observer()); + tracker()->Start(); + EXPECT_EQ(tracker(), driver()->last_connection_observer()); + tracker()->Shutdown(); + EXPECT_EQ(tracker(), driver()->last_removed_connection_observer()); +} + +// Makes sure that token fetching happens only after connection is established. +TEST_F(GCMAccountTrackerTest, PostponeTokenFetchingUntilConnected) { + driver()->SetConnected(false); + CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); + tracker()->Start(); + + EXPECT_EQ(0UL, tracker()->get_pending_token_request_count()); + driver()->SetConnected(true); + + EXPECT_EQ(1UL, tracker()->get_pending_token_request_count()); +} + +TEST_F(GCMAccountTrackerTest, InvalidateExpiredTokens) { + CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); + CoreAccountInfo account2 = AddAccount(kEmail2); + tracker()->Start(); + + EXPECT_EQ(2UL, tracker()->get_pending_token_request_count()); + + IssueExpiredAccessToken(account1.account_id); + IssueAccessToken(account2.account_id); + // Because the first token is expired, we expect the sanitize to kick in and + // clean it up before the SetAccessToken is called. This also means a new + // token request will be issued + EXPECT_FALSE(driver()->update_accounts_called()); + EXPECT_EQ(1UL, tracker()->get_pending_token_request_count()); +} + +// Testing for whether there are still more tokens to be fetched. Typically the +// need for token fetching triggers immediate request, unless there is no +// connection, that is why connection is set on and off in this test. +TEST_F(GCMAccountTrackerTest, IsTokenFetchingRequired) { + tracker()->Start(); + driver()->SetConnected(false); + EXPECT_FALSE(IsFetchingRequired()); + CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); + EXPECT_TRUE(IsFetchingRequired()); + + driver()->SetConnected(true); + EXPECT_FALSE(IsFetchingRequired()); // Indicates that fetching has started. + IssueAccessToken(account1.account_id); + EXPECT_FALSE(IsFetchingRequired()); + + CoreAccountInfo account2 = AddAccount(kEmail2); + EXPECT_FALSE(IsFetchingRequired()); // Indicates that fetching has started. + + // Disconnect the driver again so that the access token request being + // fulfilled doesn't immediately cause another access token request (which + // then would cause IsFetchingRequired() to be false, preventing us from + // distinguishing this case from the case where IsFetchingRequired() is false + // because GCMAccountTracker didn't detect that a new access token needs to be + // fetched). + driver()->SetConnected(false); + IssueExpiredAccessToken(account2.account_id); + + // Make sure that if the token was expired it is marked as being needed again. + EXPECT_TRUE(IsFetchingRequired()); +} + +// Tests what is the expected time to the next token fetching. +TEST_F(GCMAccountTrackerTest, GetTimeToNextTokenReporting) { + tracker()->Start(); + // At this point the last token fetch time is never. + EXPECT_EQ(base::TimeDelta(), GetTimeToNextTokenReporting()); + + // Regular case. The tokens have been just reported. + driver()->SetLastTokenFetchTime(base::Time::Now()); + EXPECT_TRUE(GetTimeToNextTokenReporting() <= base::Seconds(12 * 60 * 60)); + + // A case when gcm driver is not yet initialized. + driver()->SetLastTokenFetchTime(base::Time::Max()); + EXPECT_EQ(base::Seconds(12 * 60 * 60), GetTimeToNextTokenReporting()); + + // A case when token reporting calculation is expected to result in more than + // 12 hours, in which case we expect exactly 12 hours. + driver()->SetLastTokenFetchTime(base::Time::Now() + base::Days(2)); + EXPECT_EQ(base::Seconds(12 * 60 * 60), GetTimeToNextTokenReporting()); +} + +// Tests conditions when token reporting is required. +TEST_F(GCMAccountTrackerTest, IsTokenReportingRequired) { + tracker()->Start(); + // Required because it is overdue. + EXPECT_TRUE(IsTokenReportingRequired()); + + // Not required because it just happened. + driver()->SetLastTokenFetchTime(base::Time::Now()); + EXPECT_FALSE(IsTokenReportingRequired()); + + CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); + IssueAccessToken(account1.account_id); + driver()->ResetResults(); + // Reporting was triggered, which means testing for required will give false, + // but we have the update call. + RemoveAccount(account1.account_id); + EXPECT_TRUE(driver()->update_accounts_called()); + EXPECT_FALSE(IsTokenReportingRequired()); +} + +// TODO(fgorski): Add test for adding account after removal >> make sure it does +// not mark removal. + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_activity.cc b/chromium/components/gcm_driver/gcm_activity.cc new file mode 100644 index 00000000000..bee57d4b988 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_activity.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 "components/gcm_driver/gcm_activity.h" + +namespace gcm { + +Activity::Activity() + : time(base::Time::Now()) { +} + +Activity::~Activity() { +} + +CheckinActivity::CheckinActivity() { +} + +CheckinActivity::~CheckinActivity() { +} + +ConnectionActivity::ConnectionActivity() { +} + +ConnectionActivity::~ConnectionActivity() { +} + +RegistrationActivity::RegistrationActivity() { +} + +RegistrationActivity::~RegistrationActivity() { +} + +ReceivingActivity::ReceivingActivity() + : message_byte_size(0) { +} + +ReceivingActivity::~ReceivingActivity() { +} + +SendingActivity::SendingActivity() { +} + +SendingActivity::~SendingActivity() { +} + +DecryptionFailureActivity::DecryptionFailureActivity() { +} + +DecryptionFailureActivity::~DecryptionFailureActivity() { +} + +RecordedActivities::RecordedActivities() { +} + +RecordedActivities::RecordedActivities(const RecordedActivities& other) = + default; + +RecordedActivities::~RecordedActivities() { +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_activity.h b/chromium/components/gcm_driver/gcm_activity.h new file mode 100644 index 00000000000..80c9d0316f9 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_activity.h @@ -0,0 +1,90 @@ +// 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 COMPONENTS_GCM_DRIVER_GCM_ACTIVITY_H_ +#define COMPONENTS_GCM_DRIVER_GCM_ACTIVITY_H_ + +#include +#include + +#include "base/time/time.h" + +namespace gcm { + +// Contains data that are common to all activity kinds below. +struct Activity { + Activity(); + virtual ~Activity(); + + base::Time time; + std::string event; // A short description of the event. + std::string details; // Any additional detail about the event. +}; + +// Contains relevant data of a connection activity. +struct ConnectionActivity : Activity { + ConnectionActivity(); + ~ConnectionActivity() override; +}; + +// Contains relevant data of a check-in activity. +struct CheckinActivity : Activity { + CheckinActivity(); + ~CheckinActivity() override; +}; + +// Contains relevant data of a registration/unregistration step. +struct RegistrationActivity : Activity { + RegistrationActivity(); + ~RegistrationActivity() override; + + std::string app_id; + // For GCM, comma separated sender ids. For Instance ID, authorized entity. + std::string source; +}; + +// Contains relevant data of a message receiving event. +struct ReceivingActivity : Activity { + ReceivingActivity(); + ~ReceivingActivity() override; + + std::string app_id; + std::string from; + int message_byte_size; +}; + +// Contains relevant data of a send-message step. +struct SendingActivity : Activity { + SendingActivity(); + ~SendingActivity() override; + + std::string app_id; + std::string receiver_id; + std::string message_id; +}; + +// Contains relevant data of a message decryption failure. +struct DecryptionFailureActivity : Activity { + DecryptionFailureActivity(); + ~DecryptionFailureActivity() override; + + std::string app_id; +}; + +struct RecordedActivities { + RecordedActivities(); + RecordedActivities(const RecordedActivities& other); + virtual ~RecordedActivities(); + + std::vector checkin_activities; + std::vector connection_activities; + std::vector registration_activities; + std::vector receiving_activities; + std::vector sending_activities; + std::vector decryption_failure_activities; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_GCM_ACTIVITY_H_ diff --git a/chromium/components/gcm_driver/gcm_app_handler.cc b/chromium/components/gcm_driver/gcm_app_handler.cc new file mode 100644 index 00000000000..c8827995d44 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_app_handler.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 "components/gcm_driver/gcm_app_handler.h" + +namespace gcm { + +GCMAppHandler::GCMAppHandler() = default; +GCMAppHandler::~GCMAppHandler() = default; + +void GCMAppHandler::OnMessageDecryptionFailed( + const std::string& app_id, + const std::string& message_id, + const std::string& error_message) {} + +bool GCMAppHandler::CanHandle(const std::string& app_id) const { + return false; +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_app_handler.h b/chromium/components/gcm_driver/gcm_app_handler.h new file mode 100644 index 00000000000..676eeea8960 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_app_handler.h @@ -0,0 +1,67 @@ +// 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 COMPONENTS_GCM_DRIVER_GCM_APP_HANDLER_H_ +#define COMPONENTS_GCM_DRIVER_GCM_APP_HANDLER_H_ + +#include + +#include "components/gcm_driver/gcm_client.h" + +namespace gcm { + +// Defines the interface to provide handling and event routing logic for a given +// app. +class GCMAppHandler { + public: + GCMAppHandler(); + virtual ~GCMAppHandler(); + + // Called to do all the cleanup when GCM is shutting down. + // In the case that multiple apps share the same app handler, it should be + // make safe for ShutdownHandler to be called multiple times. + virtual void ShutdownHandler() = 0; + + // Called when the GCM store is reset (e.g. due to corruption), which changes + // the device ID, invalidating all prior registrations. Any stored state + // related to GCM registrations or InstanceIDs should be deleted. This should + // only be considered a defense in depth, as this method will not be called if + // the store is reset before this app handler is registered; hence it is + // recommended to regularly revalidate any stored registrations/InstanceIDs. + // TODO(johnme): GCMDriver doesn't yet provide an API for revalidating them. + virtual void OnStoreReset() = 0; + + // Called when a GCM message has been received. + virtual void OnMessage(const std::string& app_id, + const IncomingMessage& message) = 0; + + // Called when some GCM messages have been deleted from the server. + virtual void OnMessagesDeleted(const std::string& app_id) = 0; + + // Called when a GCM message failed to be delivered. + virtual void OnSendError( + const std::string& app_id, + const GCMClient::SendErrorDetails& send_error_details) = 0; + + // Called when a GCM message was received by GCM server. + virtual void OnSendAcknowledged(const std::string& app_id, + const std::string& message_id) = 0; + + // Called when a GCM message has been received but decryption failed. + // |message_id| is a message identifier sent by the GCM server. + // |error_message| is human-readable description of the error, for reporting + // purposes. By default this handler does nothing. + virtual void OnMessageDecryptionFailed(const std::string& app_id, + const std::string& message_id, + const std::string& error_message); + + // If no app handler has been added with the exact app_id of an incoming + // event, all handlers will be asked (in arbitrary order) whether they can + // handle the app_id, and the first to return true will receive the event. + virtual bool CanHandle(const std::string& app_id) const; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_GCM_APP_HANDLER_H_ diff --git a/chromium/components/gcm_driver/gcm_backoff_policy.cc b/chromium/components/gcm_driver/gcm_backoff_policy.cc new file mode 100644 index 00000000000..45ccd8b5b00 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_backoff_policy.cc @@ -0,0 +1,46 @@ +// 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 "components/gcm_driver/gcm_backoff_policy.h" + +namespace gcm { + +namespace { + +// Backoff policy. Shared across GCM requests. +// Note: In order to ensure a minimum of 20 seconds between server errors (for +// server reasons), we have a 30s +- 10s (33%) jitter initial backoff. +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. + 30 * 1000, // 30 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.33, // 33%. + + // Maximum amount of time we are willing to delay our request in ms. + 10 * 60 * 1000, // 10 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 + +const net::BackoffEntry::Policy& GetGCMBackoffPolicy() { + return kDefaultBackoffPolicy; +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_backoff_policy.h b/chromium/components/gcm_driver/gcm_backoff_policy.h new file mode 100644 index 00000000000..d49accbadf7 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_backoff_policy.h @@ -0,0 +1,17 @@ +// 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 COMPONENTS_GCM_DRIVER_GCM_BACKOFF_POLICY_H_ +#define COMPONENTS_GCM_DRIVER_GCM_BACKOFF_POLICY_H_ + +#include "net/base/backoff_entry.h" + +namespace gcm { + +// Returns the backoff policy that applies to all GCM requests. +const net::BackoffEntry::Policy& GetGCMBackoffPolicy(); + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_GCM_BACKOFF_POLICY_H_ diff --git a/chromium/components/gcm_driver/gcm_client.cc b/chromium/components/gcm_driver/gcm_client.cc new file mode 100644 index 00000000000..cc9ccc1f764 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_client.cc @@ -0,0 +1,38 @@ +// 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 "components/gcm_driver/gcm_client.h" + +namespace gcm { + +GCMClient::ChromeBuildInfo::ChromeBuildInfo() + : platform(PLATFORM_UNSPECIFIED), channel(CHANNEL_UNKNOWN) {} + +GCMClient::ChromeBuildInfo::~ChromeBuildInfo() = default; + +GCMClient::SendErrorDetails::SendErrorDetails() : result(UNKNOWN_ERROR) {} + +GCMClient::SendErrorDetails::SendErrorDetails(const SendErrorDetails& other) = + default; + +GCMClient::SendErrorDetails::~SendErrorDetails() = default; + +GCMClient::GCMStatistics::GCMStatistics() + : is_recording(false), + gcm_client_created(false), + connection_client_created(false), + android_id(0u), + android_secret(0u), + send_queue_size(0), + resend_queue_size(0) {} + +GCMClient::GCMStatistics::GCMStatistics(const GCMStatistics& other) = default; + +GCMClient::GCMStatistics::~GCMStatistics() = default; + +GCMClient::GCMClient() = default; + +GCMClient::~GCMClient() = default; + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_client.h b/chromium/components/gcm_driver/gcm_client.h new file mode 100644 index 00000000000..4ee187af8fd --- /dev/null +++ b/chromium/components/gcm_driver/gcm_client.h @@ -0,0 +1,366 @@ +// 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 COMPONENTS_GCM_DRIVER_GCM_CLIENT_H_ +#define COMPONENTS_GCM_DRIVER_GCM_CLIENT_H_ + +#include + +#include +#include +#include +#include + +#include "base/memory/scoped_refptr.h" +#include "components/gcm_driver/common/gcm_message.h" +#include "components/gcm_driver/gcm_activity.h" +#include "components/gcm_driver/registration_info.h" +#include "google_apis/gaia/core_account_id.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "services/network/public/mojom/proxy_resolving_socket.mojom-forward.h" + +namespace base { +class FilePath; +class RetainingOneShotTimer; +class SequencedTaskRunner; +} // namespace base + +namespace net { +class IPEndPoint; +} // namespace net + +namespace network { +class NetworkConnectionTracker; +class SharedURLLoaderFactory; +} // namespace network + +namespace gcm { + +struct AccountMapping; +class Encryptor; +enum class GCMDecryptionResult; + +// Interface that encapsulates the network communications with the Google Cloud +// Messaging server. This interface is not supposed to be thread-safe. +class GCMClient { + public: + // Controls how GCM is being started. At first, GCMClient will be initialized + // and GCM store will be loaded. Then GCM connection may or may not be + // initiated depending on this enum value. + enum StartMode { + // GCM should be started only when it is being actually used. If no + // registration record is found, GCM will not kick off. + DELAYED_START, + // GCM should be started immediately. + IMMEDIATE_START + }; + + // Used for UMA. Can add enum values, but never renumber or delete and reuse. + enum Result { + // Successful operation. + SUCCESS, + // Invalid parameter. + INVALID_PARAMETER, + // GCM is disabled. + GCM_DISABLED, + // 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, + + // Used for UMA. Keep kMaxValue up to date and sync with histograms.xml. + kMaxValue = UNKNOWN_ERROR + }; + + enum ChromePlatform { + PLATFORM_WIN, + PLATFORM_MAC, + PLATFORM_LINUX, + PLATFORM_CROS, + PLATFORM_IOS, + PLATFORM_ANDROID, + PLATFORM_UNSPECIFIED + }; + + enum ChromeChannel { + CHANNEL_STABLE, + CHANNEL_BETA, + CHANNEL_DEV, + CHANNEL_CANARY, + CHANNEL_UNKNOWN + }; + + struct ChromeBuildInfo { + ChromeBuildInfo(); + ~ChromeBuildInfo(); + + ChromePlatform platform; + ChromeChannel channel; + std::string version; + std::string product_category_for_subtypes; + }; + + // Detailed information of the Send Error event. + struct SendErrorDetails { + SendErrorDetails(); + SendErrorDetails(const SendErrorDetails& other); + ~SendErrorDetails(); + + std::string message_id; + MessageData additional_data; + Result result; + }; + + // Internal states and activity statistics of a GCM client. + struct GCMStatistics { + public: + GCMStatistics(); + GCMStatistics(const GCMStatistics& other); + ~GCMStatistics(); + + bool is_recording; + bool gcm_client_created; + std::string gcm_client_state; + bool connection_client_created; + std::string connection_state; + base::Time last_checkin; + base::Time next_checkin; + uint64_t android_id; + uint64_t android_secret; + std::vector registered_app_ids; + int send_queue_size; + int resend_queue_size; + + RecordedActivities recorded_activities; + }; + + // Information about account. + struct AccountTokenInfo { + CoreAccountId account_id; + std::string email; + std::string access_token; + }; + + // A delegate interface that allows the GCMClient instance to interact with + // its caller, i.e. notifying asynchronous event. + class Delegate { + public: + // Called when the registration completed successfully or an error occurs. + // |registration_info|: the specific information required for the + // registration. + // |registration_id|: non-empty if the registration completed successfully. + // |result|: the type of the error if an error occured, success otherwise. + virtual void OnRegisterFinished( + scoped_refptr registration_info, + const std::string& registration_id, + Result result) = 0; + + // Called when the unregistration completed. + // |registration_info|: the specific information required for the + // registration. + // |result|: result of the unregistration. + virtual void OnUnregisterFinished( + scoped_refptr registration_info, + GCMClient::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. + // |send_error_detials|: Details of the send error event, like mesasge ID. + virtual void OnMessageSendError( + const std::string& app_id, + const SendErrorDetails& send_error_details) = 0; + + // Called when a message was acknowledged by the GCM server. + // |app_id|: application ID. + // |message_id|: ID of the acknowledged message. + virtual void OnSendAcknowledged(const std::string& app_id, + const std::string& message_id) = 0; + + // Called when the GCM becomes ready. To get to this state, GCMClient + // finished loading from the GCM store and retrieved the device check-in + // from the server if it hadn't yet. + // |account_mappings|: a persisted list of accounts mapped to this GCM + // client. + // |last_token_fetch_time|: time of a last successful token fetch. + virtual void OnGCMReady(const std::vector& account_mappings, + const base::Time& last_token_fetch_time) = 0; + + // Called when activities are being recorded and a new activity has just + // been recorded. + virtual void OnActivityRecorded() = 0; + + // Called when a new connection is established and a successful handshake + // has been performed. + virtual void OnConnected(const net::IPEndPoint& ip_endpoint) = 0; + + // Called when the connection is interrupted. + virtual void OnDisconnected() = 0; + + // Called when the GCM store is reset (e.g. due to corruption), which + // changes the device ID, invalidating all prior registrations. + virtual void OnStoreReset() = 0; + }; + + GCMClient(); + virtual ~GCMClient(); + + // Begins initialization of the GCM Client. This will not trigger a + // connection. Must be called on |io_task_runner|. + // |chrome_build_info|: chrome info, i.e., version, channel and etc. + // |store_path|: path to the GCM store. + // |remove_account_mappings_with_email_key|: Whether account mappings having + // email as account key should be removed while loading. Required + // during the migration of account identifier from email to Gaia ID. + // |blocking_task_runner|: for running blocking file tasks. + // |io_task_runner|: for running IO tasks. When provided, it could be a + // wrapper on top of base::ThreadTaskRunnerHandle::Get() to provide power + // management featueres so that a delayed task posted to it can wake the + // system up from sleep to perform the task. + // |get_socket_factory_callback|: a callback that can accept a receiver for a + // network::mojom::ProxyResolvingSocketFactory. It needs to be safe to + // run on any thread. + // |delegate|: the delegate whose methods will be called asynchronously in + // response to events and messages. + virtual void Initialize( + const ChromeBuildInfo& chrome_build_info, + const base::FilePath& store_path, + bool remove_account_mappings_with_email_key, + const scoped_refptr& blocking_task_runner, + scoped_refptr io_task_runner, + base::RepeatingCallback)> + get_socket_factory_callback, + const scoped_refptr& url_loader_factory, + network::NetworkConnectionTracker* network_connection_tracker_, + std::unique_ptr encryptor, + Delegate* delegate) = 0; + + // This will initiate the GCM connection only if |start_mode| means to start + // the GCM immediately or the GCM registration records are found in the store. + // Note that it is OK to call Start multiple times and the implementation + // should handle it gracefully. + virtual void Start(StartMode start_mode) = 0; + + // Stops using the GCM service. This will not erase the persisted data. + virtual void Stop() = 0; + + // Registers with the server to access the provided service. + // Delegate::OnRegisterFinished will be called asynchronously upon completion. + // |registration_info|: the specific information required for the + // registration. For GCM, it will contain app id and + // sender IDs. For InstanceID, it will contain app_id, + // authorized entity and scope. + virtual void Register(scoped_refptr registration_info) = 0; + + // Checks that the provided |registration_id| (aka token for Instance ID + // registrations) matches the stored registration info. Also checks sender IDs + // match for GCM registrations. + virtual bool ValidateRegistration( + scoped_refptr registration_info, + const std::string& registration_id) = 0; + + // Unregisters from the server to stop accessing the provided service. + // Delegate::OnUnregisterFinished will be called asynchronously upon + // completion. + // |registration_info|: the specific information required for the + // registration. For GCM, it will contain app id (sender + // IDs can be ingored). For InstanceID, it will contain + // app id, authorized entity and scope. + virtual void Unregister( + scoped_refptr registration_info) = 0; + + // Sends a message to a given receiver. Delegate::OnSendFinished will be + // called asynchronously upon completion. + // |app_id|: application ID. + // |receiver_id|: registration ID of the receiver party. + // |message|: message to be sent. + virtual void Send(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message) = 0; + + // Records a decryption failure due to |result| for the |app_id|. + virtual void RecordDecryptionFailure(const std::string& app_id, + GCMDecryptionResult result) = 0; + + // Enables or disables internal activity recording. + virtual void SetRecording(bool recording) = 0; + + // Clear all recorded GCM activity logs. + virtual void ClearActivityLogs() = 0; + + // Gets internal states and statistics. + virtual GCMStatistics GetStatistics() const = 0; + + // Sets a list of accounts with OAuth2 tokens for the next checkin. + // |account_tokens|: list of email addresses, account IDs and OAuth2 access + // tokens. + virtual void SetAccountTokens( + const std::vector& account_tokens) = 0; + + // Persists the |account_mapping| in the store. + virtual void UpdateAccountMapping(const AccountMapping& account_mapping) = 0; + + // Removes the account mapping related to |account_id| from the persistent + // store. + virtual void RemoveAccountMapping(const CoreAccountId& account_id) = 0; + + // Sets last token fetch time in persistent store. + virtual void SetLastTokenFetchTime(const base::Time& time) = 0; + + // Updates the timer used by the HeartbeatManager for sending heartbeats. + virtual void UpdateHeartbeatTimer( + std::unique_ptr timer) = 0; + + // Adds the Instance ID data for a specific app to the persistent store. + virtual void AddInstanceIDData(const std::string& app_id, + const std::string& instance_id, + const std::string& extra_data) = 0; + + // Removes the Instance ID data for a specific app from the persistent store. + virtual void RemoveInstanceIDData(const std::string& app_id) = 0; + + // Retrieves the Instance ID data for a specific app from the persistent + // store. + virtual void GetInstanceIDData(const std::string& app_id, + std::string* instance_id, + std::string* extra_data) = 0; + + // Gets and sets custom heartbeat interval for the MCS connection. + // |scope| is used to identify the component that requests a custom interval + // to be set, and allows that component to later revoke the setting. It should + // be unique. + virtual void AddHeartbeatInterval(const std::string& scope, + int interval_ms) = 0; + virtual void RemoveHeartbeatInterval(const std::string& scope) = 0; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_GCM_CLIENT_H_ diff --git a/chromium/components/gcm_driver/gcm_client_factory.cc b/chromium/components/gcm_driver/gcm_client_factory.cc new file mode 100644 index 00000000000..9ef8ad4e054 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_client_factory.cc @@ -0,0 +1,23 @@ +// 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 "components/gcm_driver/gcm_client_factory.h" + +#include "base/memory/ptr_util.h" +#include "components/gcm_driver/gcm_client_impl.h" + +namespace gcm { + +std::unique_ptr GCMClientFactory::BuildInstance() { + return std::unique_ptr(new GCMClientImpl( + base::WrapUnique(new GCMInternalsBuilder()))); +} + +GCMClientFactory::GCMClientFactory() { +} + +GCMClientFactory::~GCMClientFactory() { +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_client_factory.h b/chromium/components/gcm_driver/gcm_client_factory.h new file mode 100644 index 00000000000..a2121cdce62 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_client_factory.h @@ -0,0 +1,30 @@ +// 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 COMPONENTS_GCM_DRIVER_GCM_CLIENT_FACTORY_H_ +#define COMPONENTS_GCM_DRIVER_GCM_CLIENT_FACTORY_H_ + +#include + +namespace gcm { + +class GCMClient; + +class GCMClientFactory { + public: + GCMClientFactory(); + + GCMClientFactory(const GCMClientFactory&) = delete; + GCMClientFactory& operator=(const GCMClientFactory&) = delete; + + virtual ~GCMClientFactory(); + + // Creates a new instance of GCMClient. The testing code could override this + // to provide a mocked instance. + virtual std::unique_ptr BuildInstance(); +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_GCM_CLIENT_FACTORY_H_ diff --git a/chromium/components/gcm_driver/gcm_client_impl.cc b/chromium/components/gcm_driver/gcm_client_impl.cc new file mode 100644 index 00000000000..016228a0326 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_client_impl.cc @@ -0,0 +1,1481 @@ +// 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 "components/gcm_driver/gcm_client_impl.h" + +#include + +#include +#include + +#include "base/bind.h" +#include "base/files/file_path.h" +#include "base/location.h" +#include "base/logging.h" +#include "base/memory/ptr_util.h" +#include "base/metrics/histogram_functions.h" +#include "base/metrics/histogram_macros.h" +#include "base/strings/string_number_conversions.h" +#include "base/task/sequenced_task_runner.h" +#include "base/task/single_thread_task_runner.h" +#include "base/time/default_clock.h" +#include "base/timer/timer.h" +#include "components/crx_file/id_util.h" +#include "components/gcm_driver/crypto/gcm_decryption_result.h" +#include "components/gcm_driver/features.h" +#include "components/gcm_driver/gcm_account_mapper.h" +#include "components/gcm_driver/gcm_backoff_policy.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/engine/checkin_request.h" +#include "google_apis/gcm/engine/connection_factory_impl.h" +#include "google_apis/gcm/engine/gcm_registration_request_handler.h" +#include "google_apis/gcm/engine/gcm_store_impl.h" +#include "google_apis/gcm/engine/gcm_unregistration_request_handler.h" +#include "google_apis/gcm/engine/instance_id_delete_token_request_handler.h" +#include "google_apis/gcm/engine/instance_id_get_token_request_handler.h" +#include "google_apis/gcm/monitoring/gcm_stats_recorder.h" +#include "google_apis/gcm/protocol/checkin.pb.h" +#include "google_apis/gcm/protocol/mcs.pb.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#include "url/gurl.h" + +namespace gcm { + +// It is okay to append to the enum if these states grow. DO NOT reorder, +// renumber or otherwise reuse existing values. +// Do not assign an explicit value to REGISTRATION_CACHE_STATUS_COUNT, as +// this lets the compiler keep it up to date. +enum class RegistrationCacheStatus { + REGISTRATION_NOT_FOUND = 0, + REGISTRATION_FOUND_AND_FRESH = 1, + REGISTRATION_FOUND_BUT_STALE = 2, + REGISTRATION_FOUND_BUT_SENDERS_DONT_MATCH = 3, + // NOTE: always keep this entry at the end. Add new value only immediately + // above this line. Make sure to update the corresponding histogram enum + // accordingly. + REGISTRATION_CACHE_STATUS_COUNT +}; + +namespace { + +// Indicates a message type of the received message. +enum MessageType { + UNKNOWN, // Undetermined type. + DATA_MESSAGE, // Regular data message. + DELETED_MESSAGES, // Messages were deleted on the server. + SEND_ERROR, // Error sending a message. +}; + +enum ResetStoreError { + DESTROYING_STORE_FAILED, + INFINITE_STORE_RESET, + // NOTE: always keep this entry at the end. Add new value only immediately + // above this line. Make sure to update the corresponding histogram enum + // accordingly. + RESET_STORE_ERROR_COUNT +}; + +const int kMaxRegistrationRetries = 5; +const int kMaxUnregistrationRetries = 5; +const char kDeletedCountKey[] = "total_deleted"; +const char kMessageTypeDataMessage[] = "gcm"; +const char kMessageTypeDeletedMessagesKey[] = "deleted_messages"; +const char kMessageTypeKey[] = "message_type"; +const char kMessageTypeSendErrorKey[] = "send_error"; +const char kSubtypeKey[] = "subtype"; +const char kSendMessageFromValue[] = "gcm@chrome.com"; +const int64_t kDefaultUserSerialNumber = 0LL; +const int kDestroyGCMStoreDelayMS = 5 * 60 * 1000; // 5 minutes. + +GCMClient::Result ToGCMClientResult(MCSClient::MessageSendStatus status) { + switch (status) { + case MCSClient::QUEUED: + return GCMClient::SUCCESS; + case MCSClient::MESSAGE_TOO_LARGE: + return GCMClient::INVALID_PARAMETER; + case MCSClient::QUEUE_SIZE_LIMIT_REACHED: + case MCSClient::APP_QUEUE_SIZE_LIMIT_REACHED: + case MCSClient::NO_CONNECTION_ON_ZERO_TTL: + case MCSClient::TTL_EXCEEDED: + return GCMClient::NETWORK_ERROR; + case MCSClient::SENT: + case MCSClient::SEND_STATUS_COUNT: + NOTREACHED(); + break; + } + return GCMClientImpl::UNKNOWN_ERROR; +} + +void ToCheckinProtoVersion( + const GCMClient::ChromeBuildInfo& chrome_build_info, + checkin_proto::ChromeBuildProto* android_build_info) { + checkin_proto::ChromeBuildProto_Platform platform = + checkin_proto::ChromeBuildProto_Platform_PLATFORM_LINUX; + switch (chrome_build_info.platform) { + case GCMClient::PLATFORM_WIN: + platform = checkin_proto::ChromeBuildProto_Platform_PLATFORM_WIN; + break; + case GCMClient::PLATFORM_MAC: + platform = checkin_proto::ChromeBuildProto_Platform_PLATFORM_MAC; + break; + case GCMClient::PLATFORM_LINUX: + platform = checkin_proto::ChromeBuildProto_Platform_PLATFORM_LINUX; + break; + case GCMClient::PLATFORM_IOS: + platform = checkin_proto::ChromeBuildProto_Platform_PLATFORM_IOS; + break; + case GCMClient::PLATFORM_ANDROID: + platform = checkin_proto::ChromeBuildProto_Platform_PLATFORM_ANDROID; + break; + case GCMClient::PLATFORM_CROS: + platform = checkin_proto::ChromeBuildProto_Platform_PLATFORM_CROS; + break; + case GCMClient::PLATFORM_UNSPECIFIED: + // For unknown platform, return as LINUX. + platform = checkin_proto::ChromeBuildProto_Platform_PLATFORM_LINUX; + break; + } + android_build_info->set_platform(platform); + + checkin_proto::ChromeBuildProto_Channel channel = + checkin_proto::ChromeBuildProto_Channel_CHANNEL_UNKNOWN; + switch (chrome_build_info.channel) { + case GCMClient::CHANNEL_STABLE: + channel = checkin_proto::ChromeBuildProto_Channel_CHANNEL_STABLE; + break; + case GCMClient::CHANNEL_BETA: + channel = checkin_proto::ChromeBuildProto_Channel_CHANNEL_BETA; + break; + case GCMClient::CHANNEL_DEV: + channel = checkin_proto::ChromeBuildProto_Channel_CHANNEL_DEV; + break; + case GCMClient::CHANNEL_CANARY: + channel = checkin_proto::ChromeBuildProto_Channel_CHANNEL_CANARY; + break; + case GCMClient::CHANNEL_UNKNOWN: + channel = checkin_proto::ChromeBuildProto_Channel_CHANNEL_UNKNOWN; + break; + } + android_build_info->set_channel(channel); + + android_build_info->set_chrome_version(chrome_build_info.version); +} + +MessageType DecodeMessageType(const std::string& value) { + if (kMessageTypeDeletedMessagesKey == value) + return DELETED_MESSAGES; + if (kMessageTypeSendErrorKey == value) + return SEND_ERROR; + if (kMessageTypeDataMessage == value) + return DATA_MESSAGE; + return UNKNOWN; +} + +int ConstructGCMVersion(const std::string& chrome_version) { + // Major Chrome version is passed as GCM version. + size_t pos = chrome_version.find('.'); + if (pos == std::string::npos) { + NOTREACHED(); + return 0; + } + + int gcm_version = 0; + base::StringToInt( + base::StringPiece(chrome_version.c_str(), pos), &gcm_version); + return gcm_version; +} + +std::string SerializeInstanceIDData(const std::string& instance_id, + const std::string& extra_data) { + DCHECK(!instance_id.empty() && !extra_data.empty()); + DCHECK(instance_id.find(',') == std::string::npos); + return instance_id + "," + extra_data; +} + +bool DeserializeInstanceIDData(const std::string& serialized_data, + std::string* instance_id, + std::string* extra_data) { + DCHECK(instance_id && extra_data); + std::size_t pos = serialized_data.find(','); + if (pos == std::string::npos) + return false; + *instance_id = serialized_data.substr(0, pos); + *extra_data = serialized_data.substr(pos + 1); + return !instance_id->empty() && !extra_data->empty(); +} + +bool InstanceIDUsesSubtypeForAppId(const std::string& app_id) { + // Always use subtypes with Instance ID, except for Chrome Apps/Extensions. + return !crx_file::id_util::IdIsValid(app_id); +} + +void RecordResetStoreErrorToUMA(ResetStoreError error) { + UMA_HISTOGRAM_ENUMERATION("GCM.ResetStore", error, RESET_STORE_ERROR_COUNT); +} + +} // namespace + +void RecordRegistrationRequestToUMA(gcm::RegistrationCacheStatus status) { + UMA_HISTOGRAM_ENUMERATION( + "GCM.RegistrationCacheStatus", status, + RegistrationCacheStatus::REGISTRATION_CACHE_STATUS_COUNT); +} +GCMInternalsBuilder::GCMInternalsBuilder() {} +GCMInternalsBuilder::~GCMInternalsBuilder() {} + +base::Clock* GCMInternalsBuilder::GetClock() { + return base::DefaultClock::GetInstance(); +} + +std::unique_ptr GCMInternalsBuilder::BuildMCSClient( + const std::string& version, + base::Clock* clock, + ConnectionFactory* connection_factory, + GCMStore* gcm_store, + scoped_refptr io_task_runner, + GCMStatsRecorder* recorder) { + return std::make_unique(version, clock, connection_factory, + gcm_store, std::move(io_task_runner), + recorder); +} + +std::unique_ptr GCMInternalsBuilder::BuildConnectionFactory( + const std::vector& endpoints, + const net::BackoffEntry::Policy& backoff_policy, + base::RepeatingCallback)> + get_socket_factory_callback, + scoped_refptr io_task_runner, + GCMStatsRecorder* recorder, + network::NetworkConnectionTracker* network_connection_tracker) { + return std::make_unique( + endpoints, backoff_policy, std::move(get_socket_factory_callback), + std::move(io_task_runner), recorder, network_connection_tracker); +} + +GCMClientImpl::CheckinInfo::CheckinInfo() + : android_id(0), secret(0), accounts_set(false) { +} + +GCMClientImpl::CheckinInfo::~CheckinInfo() { +} + +void GCMClientImpl::CheckinInfo::SnapshotCheckinAccounts() { + last_checkin_accounts.clear(); + for (auto iter = account_tokens.begin(); iter != account_tokens.end(); + ++iter) { + last_checkin_accounts.insert(iter->first); + } +} + +void GCMClientImpl::CheckinInfo::Reset() { + android_id = 0; + secret = 0; + accounts_set = false; + account_tokens.clear(); + last_checkin_accounts.clear(); +} + +GCMClientImpl::GCMClientImpl( + std::unique_ptr internals_builder) + : internals_builder_(std::move(internals_builder)), + state_(UNINITIALIZED), + delegate_(nullptr), + start_mode_(DELAYED_START), + clock_(internals_builder_->GetClock()), + gcm_store_reset_(false), + network_connection_tracker_(nullptr) {} + +GCMClientImpl::~GCMClientImpl() { +} + +void GCMClientImpl::Initialize( + const ChromeBuildInfo& chrome_build_info, + const base::FilePath& path, + bool remove_account_mappings_with_email_key, + const scoped_refptr& blocking_task_runner, + scoped_refptr io_task_runner, + base::RepeatingCallback)> + get_socket_factory_callback, + const scoped_refptr& url_loader_factory, + network::NetworkConnectionTracker* network_connection_tracker, + std::unique_ptr encryptor, + GCMClient::Delegate* delegate) { + DCHECK_EQ(UNINITIALIZED, state_); + DCHECK(delegate); + DCHECK(io_task_runner); + DCHECK(io_task_runner->RunsTasksInCurrentSequence()); + + get_socket_factory_callback_ = std::move(get_socket_factory_callback); + url_loader_factory_ = url_loader_factory; + network_connection_tracker_ = network_connection_tracker; + chrome_build_info_ = chrome_build_info; + gcm_store_ = std::make_unique( + path, remove_account_mappings_with_email_key, blocking_task_runner, + std::move(encryptor)); + delegate_ = delegate; + io_task_runner_ = std::move(io_task_runner); + recorder_.SetDelegate(this); + state_ = INITIALIZED; +} + +void GCMClientImpl::Start(StartMode start_mode) { + DCHECK_NE(UNINITIALIZED, state_); + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + if (state_ == LOADED) { + // Start the GCM if not yet. + if (start_mode == IMMEDIATE_START) { + // Give up the scheduling to wipe out the store since now some one starts + // to use GCM. + destroying_gcm_store_ptr_factory_.InvalidateWeakPtrs(); + + StartGCM(); + } + return; + } + + // The delay start behavior will be abandoned when Start has been called + // once with IMMEDIATE_START behavior. + if (start_mode == IMMEDIATE_START) + start_mode_ = IMMEDIATE_START; + + // Bail out if the loading is not started or completed. + if (state_ != INITIALIZED) + return; + + // Once the loading is completed, the check-in will be initiated. + // If we're in lazy start mode, don't create a new store since none is really + // using GCM functionality yet. + gcm_store_->Load((start_mode == IMMEDIATE_START) ? GCMStore::CREATE_IF_MISSING + : GCMStore::DO_NOT_CREATE, + base::BindOnce(&GCMClientImpl::OnLoadCompleted, + weak_ptr_factory_.GetWeakPtr())); + state_ = LOADING; +} + +void GCMClientImpl::OnLoadCompleted( + std::unique_ptr result) { + DCHECK_EQ(LOADING, state_); + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + if (!result->success) { + if (result->store_does_not_exist) { + if (start_mode_ == IMMEDIATE_START) { + // An immediate start was requested during the delayed start that just + // completed. Perform it now. + gcm_store_->Load(GCMStore::CREATE_IF_MISSING, + base::BindOnce(&GCMClientImpl::OnLoadCompleted, + weak_ptr_factory_.GetWeakPtr())); + } else { + // In the case that the store does not exist, set |state_| back to + // INITIALIZED such that store loading could be triggered again when + // Start() is called with IMMEDIATE_START. + state_ = INITIALIZED; + } + } else { + // Otherwise, destroy the store to try again. + ResetStore(); + } + return; + } + gcm_store_reset_ = false; + + device_checkin_info_.android_id = result->device_android_id; + device_checkin_info_.secret = result->device_security_token; + device_checkin_info_.last_checkin_accounts = result->last_checkin_accounts; + // A case where there were previously no accounts reported with checkin is + // considered to be the same as when the list of accounts is empty. It enables + // scheduling a periodic checkin for devices with no signed in users + // immediately after restart, while keeping |accounts_set == false| delays the + // checkin until the list of accounts is set explicitly. + if (result->last_checkin_accounts.size() == 0) + device_checkin_info_.accounts_set = true; + last_checkin_time_ = result->last_checkin_time; + gservices_settings_.UpdateFromLoadResult(*result); + + for (auto iter = result->registrations.begin(); + iter != result->registrations.end(); + ++iter) { + std::string registration_id; + scoped_refptr registration = + RegistrationInfo::BuildFromString(iter->first, iter->second, + ®istration_id); + // TODO(jianli): Add UMA to track the error case. + if (registration) + registrations_.emplace(std::move(registration), registration_id); + } + + for (auto iter = result->instance_id_data.begin(); + iter != result->instance_id_data.end(); + ++iter) { + std::string instance_id; + std::string extra_data; + if (DeserializeInstanceIDData(iter->second, &instance_id, &extra_data)) + instance_id_data_[iter->first] = std::make_pair(instance_id, extra_data); + } + + load_result_ = std::move(result); + state_ = LOADED; + + // Don't initiate the GCM connection when GCM is in delayed start mode and + // not any standalone app has registered GCM yet. + if (start_mode_ == DELAYED_START && !HasStandaloneRegisteredApp()) { + // If no standalone app is using GCM and the device ID is present, schedule + // to have the store wiped out. + if (device_checkin_info_.android_id) { + io_task_runner_->PostDelayedTask( + FROM_HERE, + base::BindOnce(&GCMClientImpl::DestroyStoreWhenNotNeeded, + destroying_gcm_store_ptr_factory_.GetWeakPtr()), + base::Milliseconds(kDestroyGCMStoreDelayMS)); + } + + return; + } + + StartGCM(); +} + +void GCMClientImpl::StartGCM() { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + // Taking over the value of account_mappings before passing the ownership of + // load result to InitializeMCSClient. + std::vector account_mappings; + account_mappings.swap(load_result_->account_mappings); + base::Time last_token_fetch_time = load_result_->last_token_fetch_time; + + InitializeMCSClient(); + + if (device_checkin_info_.IsValid()) { + SchedulePeriodicCheckin(); + OnReady(account_mappings, last_token_fetch_time); + return; + } + + state_ = INITIAL_DEVICE_CHECKIN; + device_checkin_info_.Reset(); + StartCheckin(); +} + +void GCMClientImpl::InitializeMCSClient() { + DCHECK(network_connection_tracker_); + std::vector endpoints; + endpoints.push_back(gservices_settings_.GetMCSMainEndpoint()); + GURL fallback_endpoint = gservices_settings_.GetMCSFallbackEndpoint(); + if (fallback_endpoint.is_valid()) + endpoints.push_back(fallback_endpoint); + connection_factory_ = internals_builder_->BuildConnectionFactory( + endpoints, GetGCMBackoffPolicy(), get_socket_factory_callback_, + io_task_runner_, &recorder_, network_connection_tracker_); + connection_factory_->SetConnectionListener(this); + mcs_client_ = internals_builder_->BuildMCSClient( + chrome_build_info_.version, clock_, connection_factory_.get(), + gcm_store_.get(), io_task_runner_, &recorder_); + + mcs_client_->Initialize( + base::BindRepeating(&GCMClientImpl::OnMCSError, + weak_ptr_factory_.GetWeakPtr()), + base::BindRepeating(&GCMClientImpl::OnMessageReceivedFromMCS, + weak_ptr_factory_.GetWeakPtr()), + base::BindRepeating(&GCMClientImpl::OnMessageSentToMCS, + weak_ptr_factory_.GetWeakPtr()), + std::move(load_result_)); +} + +void GCMClientImpl::OnFirstTimeDeviceCheckinCompleted( + const CheckinInfo& checkin_info) { + DCHECK(!device_checkin_info_.IsValid()); + + device_checkin_info_.android_id = checkin_info.android_id; + device_checkin_info_.secret = checkin_info.secret; + // If accounts were not set by now, we can consider them set (to empty list) + // to make sure periodic checkins get scheduled after initial checkin. + device_checkin_info_.accounts_set = true; + gcm_store_->SetDeviceCredentials( + checkin_info.android_id, checkin_info.secret, + base::BindOnce(&GCMClientImpl::SetDeviceCredentialsCallback, + weak_ptr_factory_.GetWeakPtr())); + + OnReady(std::vector(), base::Time()); +} + +void GCMClientImpl::OnReady(const std::vector& account_mappings, + const base::Time& last_token_fetch_time) { + state_ = READY; + StartMCSLogin(); + + delegate_->OnGCMReady(account_mappings, last_token_fetch_time); +} + +void GCMClientImpl::StartMCSLogin() { + DCHECK_EQ(READY, state_); + DCHECK(device_checkin_info_.IsValid()); + mcs_client_->Login(device_checkin_info_.android_id, + device_checkin_info_.secret); +} + +void GCMClientImpl::DestroyStoreWhenNotNeeded() { + if (state_ != LOADED || start_mode_ != DELAYED_START) + return; + + gcm_store_->Destroy(base::BindOnce(&GCMClientImpl::DestroyStoreCallback, + weak_ptr_factory_.GetWeakPtr())); +} + +void GCMClientImpl::ResetStore() { + // If already being reset, don't do it again. We want to prevent from + // resetting and loading from the store again and again. + if (gcm_store_reset_) { + RecordResetStoreErrorToUMA(INFINITE_STORE_RESET); + state_ = UNINITIALIZED; + return; + } + gcm_store_reset_ = true; + + // Destroy the GCM store to start over. + gcm_store_->Destroy(base::BindOnce(&GCMClientImpl::ResetStoreCallback, + weak_ptr_factory_.GetWeakPtr())); +} + +void GCMClientImpl::SetAccountTokens( + const std::vector& account_tokens) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + device_checkin_info_.account_tokens.clear(); + for (auto iter = account_tokens.begin(); iter != account_tokens.end(); + ++iter) { + device_checkin_info_.account_tokens[iter->email] = iter->access_token; + } + + bool accounts_set_before = device_checkin_info_.accounts_set; + device_checkin_info_.accounts_set = true; + + DVLOG(1) << "Set account called with: " << account_tokens.size() + << " accounts."; + + if (state_ != READY && state_ != INITIAL_DEVICE_CHECKIN) + return; + + bool account_removed = false; + for (auto iter = device_checkin_info_.last_checkin_accounts.begin(); + iter != device_checkin_info_.last_checkin_accounts.end(); ++iter) { + if (device_checkin_info_.account_tokens.find(*iter) == + device_checkin_info_.account_tokens.end()) { + account_removed = true; + } + } + + // Checkin will be forced when any of the accounts was removed during the + // current Chrome session or if there has been an account removed between the + // restarts of Chrome. If there is a checkin in progress, it will be canceled. + // We only force checkin when user signs out. When there is a new account + // signed in, the periodic checkin will take care of adding the association in + // reasonable time. + if (account_removed) { + DVLOG(1) << "Detected that account has been removed. Forcing checkin."; + checkin_request_.reset(); + StartCheckin(); + } else if (!accounts_set_before) { + SchedulePeriodicCheckin(); + DVLOG(1) << "Accounts set for the first time. Scheduled periodic checkin."; + } +} + +void GCMClientImpl::UpdateAccountMapping( + const AccountMapping& account_mapping) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + gcm_store_->AddAccountMapping( + account_mapping, base::BindOnce(&GCMClientImpl::DefaultStoreCallback, + weak_ptr_factory_.GetWeakPtr())); +} + +void GCMClientImpl::RemoveAccountMapping(const CoreAccountId& account_id) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + gcm_store_->RemoveAccountMapping( + account_id, base::BindOnce(&GCMClientImpl::DefaultStoreCallback, + weak_ptr_factory_.GetWeakPtr())); +} + +void GCMClientImpl::SetLastTokenFetchTime(const base::Time& time) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + gcm_store_->SetLastTokenFetchTime( + time, + base::BindOnce(&GCMClientImpl::IgnoreWriteResultCallback, + weak_ptr_factory_.GetWeakPtr(), + /*operation_suffix_for_uma=*/"SetLastTokenFetchTime")); +} + +void GCMClientImpl::UpdateHeartbeatTimer( + std::unique_ptr timer) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + DCHECK(mcs_client_); + mcs_client_->UpdateHeartbeatTimer(std::move(timer)); +} + +void GCMClientImpl::AddInstanceIDData(const std::string& app_id, + const std::string& instance_id, + const std::string& extra_data) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + instance_id_data_[app_id] = std::make_pair(instance_id, extra_data); + // TODO(crbug/1028761): If this call fails, we likely leak a registration + // (the one stored in instance_id_data_ would be used for a registration but + // not persisted). + gcm_store_->AddInstanceIDData( + app_id, SerializeInstanceIDData(instance_id, extra_data), + base::BindOnce(&GCMClientImpl::IgnoreWriteResultCallback, + weak_ptr_factory_.GetWeakPtr(), + /*operation_suffix_for_uma=*/"AddInstanceIDData")); +} + +void GCMClientImpl::RemoveInstanceIDData(const std::string& app_id) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + instance_id_data_.erase(app_id); + gcm_store_->RemoveInstanceIDData( + app_id, + base::BindOnce(&GCMClientImpl::IgnoreWriteResultCallback, + weak_ptr_factory_.GetWeakPtr(), + /*operation_suffix_for_uma=*/"RemoveInstanceIDData")); +} + +void GCMClientImpl::GetInstanceIDData(const std::string& app_id, + std::string* instance_id, + std::string* extra_data) { + DCHECK(instance_id && extra_data); + + auto iter = instance_id_data_.find(app_id); + if (iter == instance_id_data_.end()) + return; + *instance_id = iter->second.first; + *extra_data = iter->second.second; +} + +void GCMClientImpl::AddHeartbeatInterval(const std::string& scope, + int interval_ms) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + DCHECK(mcs_client_); + mcs_client_->AddHeartbeatInterval(scope, interval_ms); +} + +void GCMClientImpl::RemoveHeartbeatInterval(const std::string& scope) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + DCHECK(mcs_client_); + mcs_client_->RemoveHeartbeatInterval(scope); +} + +void GCMClientImpl::StartCheckin() { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + // Make sure no checkin is in progress. + if (checkin_request_) + return; + + checkin_proto::ChromeBuildProto chrome_build_proto; + ToCheckinProtoVersion(chrome_build_info_, &chrome_build_proto); + CheckinRequest::RequestInfo request_info(device_checkin_info_.android_id, + device_checkin_info_.secret, + device_checkin_info_.account_tokens, + gservices_settings_.digest(), + chrome_build_proto); + checkin_request_ = std::make_unique( + gservices_settings_.GetCheckinURL(), request_info, GetGCMBackoffPolicy(), + base::BindOnce(&GCMClientImpl::OnCheckinCompleted, + weak_ptr_factory_.GetWeakPtr()), + url_loader_factory_, io_task_runner_, &recorder_); + // Taking a snapshot of the accounts count here, as there might be an asynch + // update of the account tokens while checkin is in progress. + device_checkin_info_.SnapshotCheckinAccounts(); + checkin_request_->Start(); +} + +void GCMClientImpl::OnCheckinCompleted( + net::HttpStatusCode response_code, + const checkin_proto::AndroidCheckinResponse& checkin_response) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + checkin_request_.reset(); + + if (response_code == net::HTTP_UNAUTHORIZED || + response_code == net::HTTP_BAD_REQUEST) { + LOG(ERROR) << "Checkin rejected. Resetting GCM Store."; + ResetStore(); + return; + } + + DCHECK(checkin_response.has_android_id()); + DCHECK(checkin_response.has_security_token()); + CheckinInfo checkin_info; + checkin_info.android_id = checkin_response.android_id(); + checkin_info.secret = checkin_response.security_token(); + + if (state_ == INITIAL_DEVICE_CHECKIN) { + OnFirstTimeDeviceCheckinCompleted(checkin_info); + } else { + // checkin_info is not expected to change after a periodic checkin as it + // would invalidate the registration IDs. + DCHECK_EQ(READY, state_); + DCHECK_EQ(device_checkin_info_.android_id, checkin_info.android_id); + DCHECK_EQ(device_checkin_info_.secret, checkin_info.secret); + } + + if (device_checkin_info_.IsValid()) { + // First update G-services settings, as something might have changed. + if (gservices_settings_.UpdateFromCheckinResponse(checkin_response)) { + gcm_store_->SetGServicesSettings( + gservices_settings_.settings_map(), gservices_settings_.digest(), + base::BindOnce(&GCMClientImpl::SetGServicesSettingsCallback, + weak_ptr_factory_.GetWeakPtr())); + } + + last_checkin_time_ = clock_->Now(); + gcm_store_->SetLastCheckinInfo( + last_checkin_time_, device_checkin_info_.last_checkin_accounts, + base::BindOnce(&GCMClientImpl::SetLastCheckinInfoCallback, + weak_ptr_factory_.GetWeakPtr())); + SchedulePeriodicCheckin(); + } +} + +void GCMClientImpl::SetGServicesSettingsCallback(bool success) { + DCHECK(success); +} + +void GCMClientImpl::SchedulePeriodicCheckin() { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + // Make sure no checkin is in progress. + if (checkin_request_.get() || !device_checkin_info_.accounts_set) + return; + + // There should be only one periodic checkin pending at a time. Removing + // pending periodic checkin to schedule a new one. + periodic_checkin_ptr_factory_.InvalidateWeakPtrs(); + + base::TimeDelta time_to_next_checkin = GetTimeToNextCheckin(); + if (time_to_next_checkin.is_negative()) + time_to_next_checkin = base::TimeDelta(); + + io_task_runner_->PostDelayedTask( + FROM_HERE, + base::BindOnce(&GCMClientImpl::StartCheckin, + periodic_checkin_ptr_factory_.GetWeakPtr()), + time_to_next_checkin); +} + +base::TimeDelta GCMClientImpl::GetTimeToNextCheckin() const { + return last_checkin_time_ + gservices_settings_.GetCheckinInterval() - + clock_->Now(); +} + +void GCMClientImpl::SetLastCheckinInfoCallback(bool success) { + // TODO(fgorski): This is one of the signals that store needs a rebuild. + DCHECK(success); +} + +void GCMClientImpl::SetDeviceCredentialsCallback(bool success) { + // TODO(fgorski): This is one of the signals that store needs a rebuild. + DCHECK(success); +} + +void GCMClientImpl::UpdateRegistrationCallback(bool success) { + // TODO(fgorski): This is one of the signals that store needs a rebuild. + DCHECK(success); +} + +void GCMClientImpl::DefaultStoreCallback(bool success) { + DCHECK(success); +} + +void GCMClientImpl::IgnoreWriteResultCallback( + const std::string& operation_suffix_for_uma, + bool success) { + // TODO(crbug.com/1081149): Implement proper error handling. + // TODO(fgorski): Ignoring the write result for now to make sure + // sync_intergration_tests are not broken. +} + +void GCMClientImpl::DestroyStoreCallback(bool success) { + ResetCache(); + + if (!success) { + LOG(ERROR) << "Failed to destroy GCM store"; + RecordResetStoreErrorToUMA(DESTROYING_STORE_FAILED); + state_ = UNINITIALIZED; + return; + } + + state_ = INITIALIZED; +} + +void GCMClientImpl::ResetStoreCallback(bool success) { + // Even an incomplete reset may invalidate registrations, and this might be + // the only opportunity to notify the delegate. For example a partial reset + // that deletes the "CURRENT" file will cause GCMStoreImpl to consider the DB + // to no longer exist, in which case the next load will simply create a new + // store rather than resetting it. + delegate_->OnStoreReset(); + + if (!success) { + LOG(ERROR) << "Failed to reset GCM store"; + RecordResetStoreErrorToUMA(DESTROYING_STORE_FAILED); + state_ = UNINITIALIZED; + return; + } + + state_ = INITIALIZED; + Start(start_mode_); +} + +void GCMClientImpl::Stop() { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + // TODO(fgorski): Perhaps we should make a distinction between a Stop and a + // Shutdown. + DVLOG(1) << "Stopping the GCM Client"; + ResetCache(); + state_ = INITIALIZED; + gcm_store_->Close(); +} + +void GCMClientImpl::ResetCache() { + weak_ptr_factory_.InvalidateWeakPtrs(); + periodic_checkin_ptr_factory_.InvalidateWeakPtrs(); + device_checkin_info_.Reset(); + connection_factory_.reset(); + delegate_->OnDisconnected(); + mcs_client_.reset(); + checkin_request_.reset(); + // Delete all of the pending registration and unregistration requests. + pending_registration_requests_.clear(); + pending_unregistration_requests_.clear(); +} + +void GCMClientImpl::Register( + scoped_refptr registration_info) { + DCHECK_EQ(state_, READY); + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + // Registrations should never pass as an app_id the special category used + // internally when registering with a subtype. See security note in + // GCMClientImpl::HandleIncomingMessage. + CHECK_NE(registration_info->app_id, + chrome_build_info_.product_category_for_subtypes); + + // Find and use the cached registration ID. + RegistrationInfoMap::const_iterator registrations_iter = + registrations_.find(registration_info); + if (registrations_iter != registrations_.end()) { + bool matched = true; + + // For GCM registration, we also match the sender IDs since multiple + // registrations are not supported. + const GCMRegistrationInfo* gcm_registration_info = + GCMRegistrationInfo::FromRegistrationInfo(registration_info.get()); + if (gcm_registration_info) { + const GCMRegistrationInfo* cached_gcm_registration_info = + GCMRegistrationInfo::FromRegistrationInfo( + registrations_iter->first.get()); + DCHECK(cached_gcm_registration_info); + if (gcm_registration_info->sender_ids != + cached_gcm_registration_info->sender_ids) { + matched = false; + // Senders IDs don't match existing registration. + RecordRegistrationRequestToUMA( + RegistrationCacheStatus::REGISTRATION_FOUND_BUT_SENDERS_DONT_MATCH); + } + } + + if (matched) { + // Skip registration if token is fresh. + base::TimeDelta token_invalidation_period = + features::GetTokenInvalidationInterval(); + base::TimeDelta time_since_last_validation = + clock_->Now() - registrations_iter->first->last_validated; + if (token_invalidation_period.is_zero() || + time_since_last_validation < token_invalidation_period) { + // Token is fresh, or token invalidation is disabled. + // Use cached registration. + delegate_->OnRegisterFinished(registration_info, + registrations_iter->second, SUCCESS); + RecordRegistrationRequestToUMA( + RegistrationCacheStatus::REGISTRATION_FOUND_AND_FRESH); + return; + } else { + // Token is stale. + RecordRegistrationRequestToUMA( + RegistrationCacheStatus::REGISTRATION_FOUND_BUT_STALE); + } + } + } else { + // New Registration request (no existing registration) + RecordRegistrationRequestToUMA( + RegistrationCacheStatus::REGISTRATION_NOT_FOUND); + } + + std::unique_ptr request_handler; + std::string source_to_record; + + const GCMRegistrationInfo* gcm_registration_info = + GCMRegistrationInfo::FromRegistrationInfo(registration_info.get()); + if (gcm_registration_info) { + std::string senders; + for (auto iter = gcm_registration_info->sender_ids.begin(); + iter != gcm_registration_info->sender_ids.end(); + ++iter) { + if (!senders.empty()) + senders.append(","); + senders.append(*iter); + } + request_handler = std::make_unique(senders); + source_to_record = senders; + } + + const InstanceIDTokenInfo* instance_id_token_info = + InstanceIDTokenInfo::FromRegistrationInfo(registration_info.get()); + if (instance_id_token_info) { + auto instance_id_iter = instance_id_data_.find(registration_info->app_id); + DCHECK(instance_id_iter != instance_id_data_.end()); + + request_handler = std::make_unique( + instance_id_iter->second.first, + instance_id_token_info->authorized_entity, + instance_id_token_info->scope, + ConstructGCMVersion(chrome_build_info_.version), + instance_id_token_info->time_to_live); + source_to_record = instance_id_token_info->authorized_entity + "/" + + instance_id_token_info->scope; + } + + bool use_subtype = instance_id_token_info && + InstanceIDUsesSubtypeForAppId(registration_info->app_id); + std::string category = use_subtype + ? chrome_build_info_.product_category_for_subtypes + : registration_info->app_id; + std::string subtype = use_subtype ? registration_info->app_id : std::string(); + RegistrationRequest::RequestInfo request_info(device_checkin_info_.android_id, + device_checkin_info_.secret, + category, subtype); + + std::unique_ptr registration_request( + new RegistrationRequest( + gservices_settings_.GetRegistrationURL(), request_info, + std::move(request_handler), GetGCMBackoffPolicy(), + base::BindOnce(&GCMClientImpl::OnRegisterCompleted, + weak_ptr_factory_.GetWeakPtr(), registration_info), + kMaxRegistrationRetries, url_loader_factory_, io_task_runner_, + &recorder_, source_to_record)); + registration_request->Start(); + pending_registration_requests_.insert( + std::make_pair(registration_info, std::move(registration_request))); +} + +void GCMClientImpl::OnRegisterCompleted( + scoped_refptr registration_info, + RegistrationRequest::Status status, + const std::string& registration_id) { + DCHECK(delegate_); + + Result result; + PendingRegistrationRequests::const_iterator iter = + pending_registration_requests_.find(registration_info); + if (iter == pending_registration_requests_.end()) { + result = UNKNOWN_ERROR; + } else if (status == RegistrationRequest::INVALID_SENDER) { + result = INVALID_PARAMETER; + } else if (registration_id.empty()) { + // All other errors are currently treated as SERVER_ERROR (including + // REACHED_MAX_RETRIES due to the device being offline!). + result = SERVER_ERROR; + } else { + result = SUCCESS; + } + + if (result == SUCCESS) { + // Cache it. + // Note that the existing cached record has to be removed first because + // otherwise the key value in registrations_ will not be updated. For GCM + // registrations, the key consists of pair of app_id and sender_ids though + // only app_id is used in the key comparison. + registrations_.erase(registration_info); + registration_info->last_validated = clock_->Now(); + registrations_[registration_info] = registration_id; + + // Save it in the persistent store. + gcm_store_->AddRegistration( + registration_info->GetSerializedKey(), + registration_info->GetSerializedValue(registration_id), + base::BindOnce(&GCMClientImpl::UpdateRegistrationCallback, + weak_ptr_factory_.GetWeakPtr())); + } + + delegate_->OnRegisterFinished( + registration_info, + result == SUCCESS ? registration_id : std::string(), + result); + + if (iter != pending_registration_requests_.end()) + pending_registration_requests_.erase(iter); +} + +bool GCMClientImpl::ValidateRegistration( + scoped_refptr registration_info, + const std::string& registration_id) { + DCHECK_EQ(state_, READY); + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + // Must have a cached registration. + RegistrationInfoMap::const_iterator registrations_iter = + registrations_.find(registration_info); + if (registrations_iter == registrations_.end()) + return false; + + // Cached registration ID must match. + const std::string& cached_registration_id = registrations_iter->second; + if (registration_id != cached_registration_id) + return false; + + // For GCM registration, we also match the sender IDs since multiple + // registrations are not supported. + const GCMRegistrationInfo* gcm_registration_info = + GCMRegistrationInfo::FromRegistrationInfo(registration_info.get()); + if (gcm_registration_info) { + const GCMRegistrationInfo* cached_gcm_registration_info = + GCMRegistrationInfo::FromRegistrationInfo( + registrations_iter->first.get()); + DCHECK(cached_gcm_registration_info); + if (gcm_registration_info->sender_ids != + cached_gcm_registration_info->sender_ids) { + return false; + } + } + + return true; +} + +void GCMClientImpl::Unregister( + scoped_refptr registration_info) { + DCHECK_EQ(state_, READY); + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + std::unique_ptr request_handler; + std::string source_to_record; + + const GCMRegistrationInfo* gcm_registration_info = + GCMRegistrationInfo::FromRegistrationInfo(registration_info.get()); + if (gcm_registration_info) { + request_handler = std::make_unique( + registration_info->app_id); + } + + const InstanceIDTokenInfo* instance_id_token_info = + InstanceIDTokenInfo::FromRegistrationInfo(registration_info.get()); + if (instance_id_token_info) { + auto instance_id_iter = instance_id_data_.find(registration_info->app_id); + if (instance_id_iter == instance_id_data_.end()) { + // This should not be reached since we should not delete tokens when + // an InstanceID has not been created yet. + NOTREACHED(); + return; + } + + request_handler = std::make_unique( + instance_id_iter->second.first, + instance_id_token_info->authorized_entity, + instance_id_token_info->scope, + ConstructGCMVersion(chrome_build_info_.version)); + source_to_record = instance_id_token_info->authorized_entity + "/" + + instance_id_token_info->scope; + } + + // Remove the registration/token(s) from the cache and the store. + // TODO(jianli): Remove it only when the request is successful. + if (instance_id_token_info && + instance_id_token_info->authorized_entity == "*" && + instance_id_token_info->scope == "*") { + // If authorized_entity and scope are '*', find and remove all associated + // tokens. + bool token_found = false; + for (auto iter = registrations_.begin(); + iter != registrations_.end();) { + InstanceIDTokenInfo* cached_instance_id_token_info = + InstanceIDTokenInfo::FromRegistrationInfo(iter->first.get()); + if (cached_instance_id_token_info && + cached_instance_id_token_info->app_id == registration_info->app_id) { + token_found = true; + gcm_store_->RemoveRegistration( + cached_instance_id_token_info->GetSerializedKey(), + base::BindOnce(&GCMClientImpl::UpdateRegistrationCallback, + weak_ptr_factory_.GetWeakPtr())); + registrations_.erase(iter++); + } else { + ++iter; + } + } + + // If no token is found for the Instance ID, don't need to unregister + // since the Instance ID is not sent to the server yet. + if (!token_found) { + OnUnregisterCompleted(registration_info, + UnregistrationRequest::SUCCESS); + return; + } + } else { + auto iter = registrations_.find(registration_info); + if (iter == registrations_.end()) { + delegate_->OnUnregisterFinished(registration_info, INVALID_PARAMETER); + return; + } + registrations_.erase(iter); + + gcm_store_->RemoveRegistration( + registration_info->GetSerializedKey(), + base::BindOnce(&GCMClientImpl::UpdateRegistrationCallback, + weak_ptr_factory_.GetWeakPtr())); + } + + bool use_subtype = instance_id_token_info && + InstanceIDUsesSubtypeForAppId(registration_info->app_id); + std::string category = use_subtype + ? chrome_build_info_.product_category_for_subtypes + : registration_info->app_id; + std::string subtype = use_subtype ? registration_info->app_id : std::string(); + UnregistrationRequest::RequestInfo request_info( + device_checkin_info_.android_id, device_checkin_info_.secret, category, + subtype); + + std::unique_ptr unregistration_request( + new UnregistrationRequest( + gservices_settings_.GetRegistrationURL(), request_info, + std::move(request_handler), GetGCMBackoffPolicy(), + base::BindOnce(&GCMClientImpl::OnUnregisterCompleted, + weak_ptr_factory_.GetWeakPtr(), registration_info), + kMaxUnregistrationRetries, url_loader_factory_, io_task_runner_, + &recorder_, source_to_record)); + unregistration_request->Start(); + pending_unregistration_requests_.insert( + std::make_pair(registration_info, std::move(unregistration_request))); +} + +void GCMClientImpl::OnUnregisterCompleted( + scoped_refptr registration_info, + UnregistrationRequest::Status status) { + DVLOG(1) << "Unregister completed for app: " << registration_info->app_id + << " with " << (status ? "success." : "failure."); + + Result result; + switch (status) { + case UnregistrationRequest::SUCCESS: + result = SUCCESS; + break; + case UnregistrationRequest::INVALID_PARAMETERS: + result = INVALID_PARAMETER; + break; + default: + // All other errors are currently treated as SERVER_ERROR (including + // REACHED_MAX_RETRIES due to the device being offline!). + result = SERVER_ERROR; + break; + } + delegate_->OnUnregisterFinished(registration_info, result); + + pending_unregistration_requests_.erase(registration_info); +} + +void GCMClientImpl::OnGCMStoreDestroyed(bool success) { + DLOG_IF(ERROR, !success) << "GCM store failed to be destroyed!"; + UMA_HISTOGRAM_BOOLEAN("GCM.StoreDestroySucceeded", success); +} + +void GCMClientImpl::Send(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message) { + DCHECK_EQ(state_, READY); + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + mcs_proto::DataMessageStanza stanza; + stanza.set_ttl(message.time_to_live); + stanza.set_sent(clock_->Now().ToInternalValue() / + base::Time::kMicrosecondsPerSecond); + stanza.set_id(message.id); + stanza.set_from(kSendMessageFromValue); + stanza.set_to(receiver_id); + stanza.set_category(app_id); + + for (auto iter = message.data.begin(); iter != message.data.end(); ++iter) { + mcs_proto::AppData* app_data = stanza.add_app_data(); + app_data->set_key(iter->first); + app_data->set_value(iter->second); + } + + MCSMessage mcs_message(stanza); + DVLOG(1) << "MCS message size: " << mcs_message.size(); + mcs_client_->SendMessage(mcs_message); +} + +std::string GCMClientImpl::GetStateString() const { + switch(state_) { + case GCMClientImpl::UNINITIALIZED: + return "UNINITIALIZED"; + case GCMClientImpl::INITIALIZED: + return "INITIALIZED"; + case GCMClientImpl::LOADING: + return "LOADING"; + case GCMClientImpl::LOADED: + return "LOADED"; + case GCMClientImpl::INITIAL_DEVICE_CHECKIN: + return "INITIAL_DEVICE_CHECKIN"; + case GCMClientImpl::READY: + return "READY"; + } + NOTREACHED(); + return std::string(); +} + +void GCMClientImpl::RecordDecryptionFailure(const std::string& app_id, + GCMDecryptionResult result) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + recorder_.RecordDecryptionFailure(app_id, result); +} + +void GCMClientImpl::SetRecording(bool recording) { + recorder_.set_is_recording(recording); +} + +void GCMClientImpl::ClearActivityLogs() { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + recorder_.Clear(); +} + +GCMClient::GCMStatistics GCMClientImpl::GetStatistics() const { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + GCMClient::GCMStatistics stats; + stats.gcm_client_created = true; + stats.is_recording = recorder_.is_recording(); + stats.gcm_client_state = GetStateString(); + stats.connection_client_created = mcs_client_ != nullptr; + stats.last_checkin = last_checkin_time_; + stats.next_checkin = + last_checkin_time_ + gservices_settings_.GetCheckinInterval(); + if (connection_factory_) + stats.connection_state = connection_factory_->GetConnectionStateString(); + if (mcs_client_) { + stats.send_queue_size = mcs_client_->GetSendQueueSize(); + stats.resend_queue_size = mcs_client_->GetResendQueueSize(); + } + if (device_checkin_info_.android_id > 0) + stats.android_id = device_checkin_info_.android_id; + if (device_checkin_info_.secret > 0) + stats.android_secret = device_checkin_info_.secret; + + recorder_.CollectActivities(&stats.recorded_activities); + + for (auto it = registrations_.begin(); it != registrations_.end(); ++it) { + stats.registered_app_ids.push_back(it->first->app_id); + } + return stats; +} + +void GCMClientImpl::OnActivityRecorded() { + delegate_->OnActivityRecorded(); +} + +void GCMClientImpl::OnConnected(const GURL& current_server, + const net::IPEndPoint& ip_endpoint) { + // TODO(gcm): expose current server in debug page. + delegate_->OnActivityRecorded(); + delegate_->OnConnected(ip_endpoint); +} + +void GCMClientImpl::OnDisconnected() { + delegate_->OnActivityRecorded(); + delegate_->OnDisconnected(); +} + +void GCMClientImpl::OnMessageReceivedFromMCS(const gcm::MCSMessage& message) { + switch (message.tag()) { + case kLoginResponseTag: + DVLOG(1) << "Login response received by GCM Client. Ignoring."; + return; + case kDataMessageStanzaTag: + DVLOG(1) << "A downstream message received. Processing..."; + HandleIncomingMessage(message); + return; + default: + NOTREACHED() << "Message with unexpected tag received by GCMClient"; + return; + } +} + +void GCMClientImpl::OnMessageSentToMCS(int64_t user_serial_number, + const std::string& app_id, + const std::string& message_id, + MCSClient::MessageSendStatus status) { + DCHECK_EQ(user_serial_number, kDefaultUserSerialNumber); + DCHECK(delegate_); + + // TTL_EXCEEDED is singled out here, because it can happen long time after the + // message was sent. That is why it comes as |OnMessageSendError| event rather + // than |OnSendFinished|. SendErrorDetails.additional_data is left empty. + // All other errors will be raised immediately, through asynchronous callback. + // It is expected that TTL_EXCEEDED will be issued for a message that was + // previously issued |OnSendFinished| with status SUCCESS. + // TODO(jianli): Consider adding UMA for this status. + if (status == MCSClient::TTL_EXCEEDED) { + SendErrorDetails send_error_details; + send_error_details.message_id = message_id; + send_error_details.result = GCMClient::TTL_EXCEEDED; + delegate_->OnMessageSendError(app_id, send_error_details); + } else if (status == MCSClient::SENT) { + delegate_->OnSendAcknowledged(app_id, message_id); + } else { + delegate_->OnSendFinished(app_id, message_id, ToGCMClientResult(status)); + } +} + +void GCMClientImpl::OnMCSError() { + // TODO(fgorski): For now it replaces the initialization method. Long term it + // should have an error or status passed in. +} + +void GCMClientImpl::HandleIncomingMessage(const gcm::MCSMessage& message) { + DCHECK(delegate_); + + const mcs_proto::DataMessageStanza& data_message_stanza = + reinterpret_cast( + message.GetProtobuf()); + DCHECK_EQ(data_message_stanza.device_user_id(), kDefaultUserSerialNumber); + + // Copy all the data from the stanza to a MessageData object. When present, + // keys like kSubtypeKey or kMessageTypeKey will be filtered out later. + MessageData message_data; + for (int i = 0; i < data_message_stanza.app_data_size(); ++i) { + std::string key = data_message_stanza.app_data(i).key(); + message_data[key] = data_message_stanza.app_data(i).value(); + } + + std::string subtype; + auto subtype_iter = message_data.find(kSubtypeKey); + if (subtype_iter != message_data.end()) { + subtype = subtype_iter->second; + message_data.erase(subtype_iter); + } + + // SECURITY NOTE: Subtypes received from GCM *cannot* be trusted for + // registrations without a subtype (as the sender can pass any subtype they + // want). They can however be trusted for registrations that are known to have + // a subtype (as GCM overwrites anything passed by the sender). + // + // So a given Chrome profile always passes a fixed string called + // |product_category_for_subtypes| (of the form "com.chrome.macosx") as the + // category when registering with a subtype, and incoming subtypes are only + // trusted for that category. + // + // TODO(johnme): Remove this check if GCM starts sending the subtype in a + // field that's guaranteed to be trusted (b/18198485). + // + // (On Android, all registrations made by Chrome on behalf of third-party + // apps/extensions/websites have always had a subtype, so such a check is not + // necessary - or possible, since category is fixed to the true package name). + bool subtype_is_trusted = data_message_stanza.category() == + chrome_build_info_.product_category_for_subtypes; + bool use_subtype = subtype_is_trusted && !subtype.empty(); + std::string app_id = use_subtype ? subtype : data_message_stanza.category(); + + MessageType message_type = DATA_MESSAGE; + auto type_iter = message_data.find(kMessageTypeKey); + if (type_iter != message_data.end()) { + message_type = DecodeMessageType(type_iter->second); + message_data.erase(type_iter); + } + + switch (message_type) { + case DATA_MESSAGE: + HandleIncomingDataMessage(app_id, use_subtype, data_message_stanza, + message_data); + break; + case DELETED_MESSAGES: + HandleIncomingDeletedMessages(app_id, data_message_stanza, message_data); + break; + case SEND_ERROR: + HandleIncomingSendError(app_id, data_message_stanza, message_data); + break; + case UNKNOWN: + DVLOG(1) << "Unknown message_type received. Message ignored. " + << "App ID: " << app_id << "."; + break; + } +} + +void GCMClientImpl::HandleIncomingDataMessage( + const std::string& app_id, + bool was_subtype, + const mcs_proto::DataMessageStanza& data_message_stanza, + MessageData& message_data) { + UMA_HISTOGRAM_BOOLEAN("GCM.DataMessageReceived", true); + + bool has_collapse_key = + data_message_stanza.has_token() && !data_message_stanza.token().empty(); + UMA_HISTOGRAM_BOOLEAN("GCM.DataMessageReceivedHasCollapseKey", + has_collapse_key); + + recorder_.RecordDataMessageReceived(app_id, data_message_stanza.from(), + data_message_stanza.ByteSize(), + GCMStatsRecorder::DATA_MESSAGE); + + IncomingMessage incoming_message; + incoming_message.sender_id = data_message_stanza.from(); + incoming_message.message_id = data_message_stanza.persistent_id(); + if (data_message_stanza.has_token()) + incoming_message.collapse_key = data_message_stanza.token(); + incoming_message.data = message_data; + incoming_message.raw_data = data_message_stanza.raw_data(); + + delegate_->OnMessageReceived(app_id, incoming_message); +} + +void GCMClientImpl::HandleIncomingDeletedMessages( + const std::string& app_id, + const mcs_proto::DataMessageStanza& data_message_stanza, + MessageData& message_data) { + int deleted_count = 0; + auto count_iter = message_data.find(kDeletedCountKey); + if (count_iter != message_data.end()) { + if (!base::StringToInt(count_iter->second, &deleted_count)) + deleted_count = 0; + } + UMA_HISTOGRAM_COUNTS_1000("GCM.DeletedMessagesReceived", deleted_count); + + recorder_.RecordDataMessageReceived(app_id, data_message_stanza.from(), + data_message_stanza.ByteSize(), + GCMStatsRecorder::DELETED_MESSAGES); + delegate_->OnMessagesDeleted(app_id); +} + +void GCMClientImpl::HandleIncomingSendError( + const std::string& app_id, + const mcs_proto::DataMessageStanza& data_message_stanza, + MessageData& message_data) { + SendErrorDetails send_error_details; + send_error_details.additional_data = message_data; + send_error_details.result = SERVER_ERROR; + send_error_details.message_id = data_message_stanza.persistent_id(); + + recorder_.RecordIncomingSendError(app_id, data_message_stanza.to(), + data_message_stanza.id()); + delegate_->OnMessageSendError(app_id, send_error_details); +} + +bool GCMClientImpl::HasStandaloneRegisteredApp() const { + if (registrations_.empty()) + return false; + // Note that account mapper is not counted as a standalone app since it is + // automatically started when other app uses GCM. + return registrations_.size() > 1 || + !ExistsGCMRegistrationInMap(registrations_, kGCMAccountMapperAppId); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_client_impl.h b/chromium/components/gcm_driver/gcm_client_impl.h new file mode 100644 index 00000000000..c393383a537 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_client_impl.h @@ -0,0 +1,430 @@ +// 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 COMPONENTS_GCM_DRIVER_GCM_CLIENT_IMPL_H_ +#define COMPONENTS_GCM_DRIVER_GCM_CLIENT_IMPL_H_ + +#include + +#include +#include +#include +#include +#include +#include + +#include "base/compiler_specific.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/ref_counted.h" +#include "base/memory/weak_ptr.h" +#include "base/timer/timer.h" +#include "components/gcm_driver/gcm_client.h" +#include "components/gcm_driver/gcm_stats_recorder_impl.h" +#include "google_apis/gcm/base/mcs_message.h" +#include "google_apis/gcm/engine/gcm_store.h" +#include "google_apis/gcm/engine/gservices_settings.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" +#include "google_apis/gcm/protocol/android_checkin.pb.h" +#include "google_apis/gcm/protocol/checkin.pb.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "net/http/http_status_code.h" +#include "services/network/public/mojom/proxy_resolving_socket.mojom.h" + +class GURL; + +namespace base { +class Clock; +class Time; +} // namespace base + +namespace mcs_proto { +class DataMessageStanza; +} // namespace mcs_proto + +namespace network { +class NetworkConnectionTracker; +class SharedURLLoaderFactory; +} // namespace network + +namespace gcm { + +class CheckinRequest; +class ConnectionFactory; +class GCMClientImplTest; + +// Helper class for building GCM internals. Allows tests to inject fake versions +// as necessary. +class GCMInternalsBuilder { + public: + GCMInternalsBuilder(); + virtual ~GCMInternalsBuilder(); + + virtual base::Clock* GetClock(); + virtual std::unique_ptr BuildMCSClient( + const std::string& version, + base::Clock* clock, + ConnectionFactory* connection_factory, + GCMStore* gcm_store, + scoped_refptr io_task_runner, + GCMStatsRecorder* recorder); + virtual std::unique_ptr BuildConnectionFactory( + const std::vector& endpoints, + const net::BackoffEntry::Policy& backoff_policy, + base::RepeatingCallback)> + get_socket_factory_callback, + scoped_refptr io_task_runner, + GCMStatsRecorder* recorder, + network::NetworkConnectionTracker* network_connection_tracker); +}; + +// Implements the GCM Client. It is used to coordinate MCS Client (communication +// with MCS) and other pieces of GCM infrastructure like Registration and +// Checkins. It also allows for registering user delegates that host +// applications that send and receive messages. +class GCMClientImpl + : public GCMClient, public GCMStatsRecorder::Delegate, + public ConnectionFactory::ConnectionListener { + public: + // State representation of the GCMClient. + // Any change made to this enum should have corresponding change in the + // GetStateString(...) function. + enum State { + // Uninitialized. + UNINITIALIZED, + // Initialized, + INITIALIZED, + // GCM store loading is in progress. + LOADING, + // GCM store is loaded. + LOADED, + // Initial device checkin is in progress. + INITIAL_DEVICE_CHECKIN, + // Ready to accept requests. + READY, + }; + + explicit GCMClientImpl( + std::unique_ptr internals_builder); + + GCMClientImpl(const GCMClientImpl&) = delete; + GCMClientImpl& operator=(const GCMClientImpl&) = delete; + + ~GCMClientImpl() override; + + // GCMClient implementation. + void Initialize( + const ChromeBuildInfo& chrome_build_info, + const base::FilePath& store_path, + bool remove_account_mappings_with_email_key, + const scoped_refptr& blocking_task_runner, + scoped_refptr io_task_runner, + base::RepeatingCallback)> + get_socket_factory_callback, + const scoped_refptr& url_loader_factory, + network::NetworkConnectionTracker* network_connection_tracker, + std::unique_ptr encryptor, + GCMClient::Delegate* delegate) override; + void Start(StartMode start_mode) override; + void Stop() override; + void Register(scoped_refptr registration_info) override; + bool ValidateRegistration(scoped_refptr registration_info, + const std::string& registration_id) override; + void Unregister(scoped_refptr registration_info) override; + void Send(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message) override; + void RecordDecryptionFailure(const std::string& app_id, + GCMDecryptionResult result) override; + void SetRecording(bool recording) override; + void ClearActivityLogs() override; + GCMStatistics GetStatistics() const override; + void SetAccountTokens( + const std::vector& account_tokens) override; + void UpdateAccountMapping(const AccountMapping& account_mapping) override; + void RemoveAccountMapping(const CoreAccountId& account_id) override; + void SetLastTokenFetchTime(const base::Time& time) override; + void UpdateHeartbeatTimer( + std::unique_ptr timer) override; + void AddInstanceIDData(const std::string& app_id, + const std::string& instance_id, + const std::string& extra_data) override; + void RemoveInstanceIDData(const std::string& app_id) override; + void GetInstanceIDData(const std::string& app_id, + std::string* instance_id, + std::string* extra_data) override; + void AddHeartbeatInterval(const std::string& scope, int interval_ms) override; + void RemoveHeartbeatInterval(const std::string& scope) override; + + // GCMStatsRecorder::Delegate implemenation. + void OnActivityRecorded() override; + + // ConnectionFactory::ConnectionListener implementation. + void OnConnected(const GURL& current_server, + const net::IPEndPoint& ip_endpoint) override; + void OnDisconnected() override; + + private: + // The check-in info for the device. + // TODO(fgorski): Convert to a class with explicit getters/setters. + struct CheckinInfo { + CheckinInfo(); + ~CheckinInfo(); + bool IsValid() const { return android_id != 0 && secret != 0; } + void SnapshotCheckinAccounts(); + void Reset(); + + // Android ID of the device as assigned by the server. + uint64_t android_id; + // Security token of the device as assigned by the server. + uint64_t secret; + // True if accounts were already provided through SetAccountsForCheckin(), + // or when |last_checkin_accounts| was loaded as empty. + bool accounts_set; + // Map of account email addresses and OAuth2 tokens that will be sent to the + // checkin server on a next checkin. + std::map account_tokens; + // As set of accounts last checkin was completed with. + std::set last_checkin_accounts; + }; + + // Reasons for resetting the GCM Store. + // Note: this enum is recorded into a histogram. Do not change enum value + // or order. + enum ResetReason { + LOAD_FAILURE, // GCM store failed to load, but the store exists. + CHECKIN_REJECTED, // Checkin was rejected by server. + + RESET_REASON_COUNT, + }; + + // Collection of pending registration requests. Keys are RegistrationInfo + // instance, while values are pending registration requests to obtain a + // registration ID for requesting application. + using PendingRegistrationRequests = + std::map, + std::unique_ptr, + RegistrationInfoComparer>; + + // Collection of pending unregistration requests. Keys are RegistrationInfo + // instance, while values are pending unregistration requests to disable the + // registration ID currently assigned to the application. + using PendingUnregistrationRequests = + std::map, + std::unique_ptr, + RegistrationInfoComparer>; + + friend class GCMClientImplTest; + friend class GCMClientInstanceIDTest; + + // Returns text representation of the enum State. + std::string GetStateString() const; + + // Callbacks for the MCSClient. + // Receives messages and dispatches them to relevant user delegates. + void OnMessageReceivedFromMCS(const gcm::MCSMessage& message); + // Receives confirmation of sent messages or information about errors. + void OnMessageSentToMCS(int64_t user_serial_number, + const std::string& app_id, + const std::string& message_id, + MCSClient::MessageSendStatus status); + // Receives information about mcs_client_ errors. + void OnMCSError(); + + // Runs after GCM Store load is done to trigger continuation of the + // initialization. + void OnLoadCompleted(std::unique_ptr result); + // Starts the GCM. + void StartGCM(); + // Initializes mcs_client_, which handles the connection to MCS. + void InitializeMCSClient(); + // Complets the first time device checkin. + void OnFirstTimeDeviceCheckinCompleted(const CheckinInfo& checkin_info); + // Starts a login on mcs_client_. + void StartMCSLogin(); + // Resets the GCM store when it is corrupted. + void ResetStore(); + // Sets state to ready. This will initiate the MCS login and notify the + // delegates. + void OnReady(const std::vector& account_mappings, + const base::Time& last_token_fetch_time); + + // Starts a first time device checkin. + void StartCheckin(); + // Completes the device checkin request by parsing the |checkin_response|. + // Function also cleans up the pending checkin. + void OnCheckinCompleted( + net::HttpStatusCode response_code, + const checkin_proto::AndroidCheckinResponse& checkin_response); + + // Callback passed to GCMStore::SetGServicesSettings. + void SetGServicesSettingsCallback(bool success); + + // Schedules next periodic device checkin and makes sure there is at most one + // pending checkin at a time. This function is meant to be called after a + // successful checkin. + void SchedulePeriodicCheckin(); + // Gets the time until next checkin. + base::TimeDelta GetTimeToNextCheckin() const; + // Callback for setting last checkin information in the |gcm_store_|. + void SetLastCheckinInfoCallback(bool success); + + // Callback for persisting device credentials in the |gcm_store_|. + void SetDeviceCredentialsCallback(bool success); + + // Callback for persisting registration info in the |gcm_store_|. + void UpdateRegistrationCallback(bool success); + + // Callback for all store operations that do not try to recover, if write in + // |gcm_store_| fails. + void DefaultStoreCallback(bool success); + + // Callback for store operation where result does not matter. + void IgnoreWriteResultCallback(const std::string& operation_suffix_for_uma, + bool success); + + // Callback for destroying the GCM store. + void DestroyStoreCallback(bool success); + + // Callback for resetting the GCM store. The store will be reloaded. + void ResetStoreCallback(bool success); + + // Completes the registration request. + void OnRegisterCompleted(scoped_refptr registration_info, + RegistrationRequest::Status status, + const std::string& registration_id); + + // Completes the unregistration request. + void OnUnregisterCompleted(scoped_refptr registration_info, + UnregistrationRequest::Status status); + + // Completes the GCM store destroy request. + void OnGCMStoreDestroyed(bool success); + + // Handles incoming data message and dispatches it the delegate of this class. + void HandleIncomingMessage(const gcm::MCSMessage& message); + + // Fires OnMessageReceived event on the delegate of this class, based on the + // details in |data_message_stanza| and |message_data|. + void HandleIncomingDataMessage( + const std::string& app_id, + bool was_subtype, + const mcs_proto::DataMessageStanza& data_message_stanza, + MessageData& message_data); + + // Fires OnMessagesDeleted event on the delegate of this class, based on the + // details in |data_message_stanza| and |message_data|. + void HandleIncomingDeletedMessages( + const std::string& app_id, + const mcs_proto::DataMessageStanza& data_message_stanza, + MessageData& message_data); + + // Fires OnMessageSendError event on the delegate of this class, based on the + // details in |data_message_stanza| and |message_data|. + void HandleIncomingSendError( + const std::string& app_id, + const mcs_proto::DataMessageStanza& data_message_stanza, + MessageData& message_data); + + // Is there any standalone app being registered for GCM? + bool HasStandaloneRegisteredApp() const; + + // Destroys the store when it is not needed. + void DestroyStoreWhenNotNeeded(); + + // Reset all cahced values. + void ResetCache(); + + // Builder for the GCM internals (mcs client, etc.). + std::unique_ptr internals_builder_; + + // Recorder that logs GCM activities. + GCMStatsRecorderImpl recorder_; + + // State of the GCM Client Implementation. + State state_; + + raw_ptr delegate_; + + // Flag to indicate if the GCM should be delay started until it is actually + // used in either of the following cases: + // 1) The GCM store contains the registration records. + // 2) GCM functionailities are explicitly called. + StartMode start_mode_; + + // Device checkin info (android ID and security token used by device). + CheckinInfo device_checkin_info_; + + // Clock used for timing of retry logic. Passed in for testing. + raw_ptr clock_; + + // Information about the chrome build. + // TODO(fgorski): Check if it can be passed in constructor and made const. + ChromeBuildInfo chrome_build_info_; + + // Persistent data store for keeping device credentials, messages and user to + // serial number mappings. + std::unique_ptr gcm_store_; + + // Data loaded from the GCM store. + std::unique_ptr load_result_; + + // Tracks if the GCM store has been reset. This is used to prevent from + // resetting and loading from the store again and again. + bool gcm_store_reset_; + + std::unique_ptr connection_factory_; + base::RepeatingCallback)> + get_socket_factory_callback_; + + scoped_refptr url_loader_factory_; + + raw_ptr network_connection_tracker_; + + scoped_refptr io_task_runner_; + + // Controls receiving and sending of packets and reliable message queueing. + // Must be destroyed before |network_session_|. + std::unique_ptr mcs_client_; + + std::unique_ptr checkin_request_; + + // Cached registration info. + RegistrationInfoMap registrations_; + + // Currently pending registration requests. GCMClientImpl owns the + // RegistrationRequests. + PendingRegistrationRequests pending_registration_requests_; + + // Currently pending unregistration requests. GCMClientImpl owns the + // UnregistrationRequests. + PendingUnregistrationRequests pending_unregistration_requests_; + + // G-services settings that were provided by MCS. + GServicesSettings gservices_settings_; + + // Time of the last successful checkin. + base::Time last_checkin_time_; + + // Cached instance ID data, key is app ID and value is pair of instance ID + // and extra data. + std::map> instance_id_data_; + + // Factory for creating references when scheduling periodic checkin. + base::WeakPtrFactory periodic_checkin_ptr_factory_{this}; + + // Factory for wiping out GCM store. + base::WeakPtrFactory destroying_gcm_store_ptr_factory_{this}; + + // Factory for creating references in callbacks. + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_GCM_CLIENT_IMPL_H_ diff --git a/chromium/components/gcm_driver/gcm_client_impl_unittest.cc b/chromium/components/gcm_driver/gcm_client_impl_unittest.cc new file mode 100644 index 00000000000..9492e9105e6 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_client_impl_unittest.cc @@ -0,0 +1,1966 @@ +// 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 "components/gcm_driver/gcm_client_impl.h" + +#include + +#include +#include + +#include "base/callback_helpers.h" +#include "base/command_line.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/memory/ptr_util.h" +#include "base/strings/string_number_conversions.h" +#include "base/test/metrics/histogram_tester.h" +#include "base/test/scoped_feature_list.h" +#include "base/test/task_environment.h" +#include "base/time/clock.h" +#include "base/timer/timer.h" +#include "components/gcm_driver/features.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/fake_connection_factory.h" +#include "google_apis/gcm/engine/fake_connection_handler.h" +#include "google_apis/gcm/engine/gservices_settings.h" +#include "google_apis/gcm/monitoring/gcm_stats_recorder.h" +#include "google_apis/gcm/protocol/android_checkin.pb.h" +#include "google_apis/gcm/protocol/checkin.pb.h" +#include "google_apis/gcm/protocol/mcs.pb.h" +#include "net/test/gtest_util.h" +#include "net/test/scoped_disable_exit_on_dfatal.h" +#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" +#include "services/network/public/mojom/url_response_head.mojom.h" +#include "services/network/test/test_network_connection_tracker.h" +#include "services/network/test/test_url_loader_factory.h" +#include "services/network/test/test_utils.h" +#include "testing/gtest/include/gtest/gtest-spi.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/leveldatabase/leveldb_chrome.h" + +namespace gcm { +namespace { + +enum LastEvent { + NONE, + LOADING_COMPLETED, + REGISTRATION_COMPLETED, + UNREGISTRATION_COMPLETED, + MESSAGE_SEND_ERROR, + MESSAGE_SEND_ACK, + MESSAGE_RECEIVED, + MESSAGES_DELETED, +}; + +const char kChromeVersion[] = "45.0.0.1"; +const uint64_t kDeviceAndroidId = 54321; +const uint64_t kDeviceSecurityToken = 12345; +const uint64_t kDeviceAndroidId2 = 11111; +const uint64_t kDeviceSecurityToken2 = 2222; +const int64_t kSettingsCheckinInterval = 16 * 60 * 60; +const char kProductCategoryForSubtypes[] = "com.chrome.macosx"; +const char kExtensionAppId[] = "abcdefghijklmnopabcdefghijklmnop"; +const char kRegistrationId[] = "reg_id"; +const char kSubtypeAppId[] = "app_id"; +const char kSender[] = "project_id"; +const char kSender2[] = "project_id2"; +const char kRegistrationResponsePrefix[] = "token="; +const char kUnregistrationResponsePrefix[] = "deleted="; +const char kRawData[] = "example raw data"; + +const char kInstanceID[] = "iid_1"; +const char kScope[] = "GCM"; +const char kDeleteTokenResponse[] = "token=foo"; +const int kTestTokenInvalidationPeriod = 5; +const char kMessageId[] = "0:12345%5678"; + +const char kRegisterUrl[] = "https://android.clients.google.com/c2dm/register3"; + +// Helper for building arbitrary data messages. +MCSMessage BuildDownstreamMessage( + const std::string& project_id, + const std::string& category, + const std::string& subtype, + const std::map& data, + const std::string& raw_data) { + mcs_proto::DataMessageStanza data_message; + data_message.set_from(project_id); + data_message.set_category(category); + for (auto iter = data.begin(); iter != data.end(); ++iter) { + mcs_proto::AppData* app_data = data_message.add_app_data(); + app_data->set_key(iter->first); + app_data->set_value(iter->second); + } + if (!subtype.empty()) { + mcs_proto::AppData* app_data = data_message.add_app_data(); + app_data->set_key("subtype"); + app_data->set_value(subtype); + } + data_message.set_raw_data(raw_data); + data_message.set_persistent_id(kMessageId); + return MCSMessage(kDataMessageStanzaTag, data_message); +} + +GCMClient::AccountTokenInfo MakeAccountToken(const std::string& email, + const std::string& token) { + GCMClient::AccountTokenInfo account_token; + account_token.email = email; + account_token.access_token = token; + return account_token; +} + +std::map MakeEmailToTokenMap( + const std::vector& account_tokens) { + std::map email_token_map; + for (auto iter = account_tokens.begin(); iter != account_tokens.end(); + ++iter) { + email_token_map[iter->email] = iter->access_token; + } + return email_token_map; +} + +class FakeMCSClient : public MCSClient { + public: + FakeMCSClient(base::Clock* clock, + ConnectionFactory* connection_factory, + GCMStore* gcm_store, + scoped_refptr io_task_runner, + GCMStatsRecorder* recorder); + ~FakeMCSClient() override; + void Login(uint64_t android_id, uint64_t security_token) override; + void SendMessage(const MCSMessage& message) override; + + uint64_t last_android_id() const { return last_android_id_; } + uint64_t last_security_token() const { return last_security_token_; } + uint8_t last_message_tag() const { return last_message_tag_; } + const mcs_proto::DataMessageStanza& last_data_message_stanza() const { + return last_data_message_stanza_; + } + + private: + uint64_t last_android_id_; + uint64_t last_security_token_; + uint8_t last_message_tag_; + mcs_proto::DataMessageStanza last_data_message_stanza_; +}; + +FakeMCSClient::FakeMCSClient( + base::Clock* clock, + ConnectionFactory* connection_factory, + GCMStore* gcm_store, + scoped_refptr io_task_runner, + GCMStatsRecorder* recorder) + : MCSClient("", + clock, + connection_factory, + gcm_store, + io_task_runner, + recorder), + last_android_id_(0u), + last_security_token_(0u), + last_message_tag_(kNumProtoTypes) {} + +FakeMCSClient::~FakeMCSClient() { +} + +void FakeMCSClient::Login(uint64_t android_id, uint64_t security_token) { + last_android_id_ = android_id; + last_security_token_ = security_token; +} + +void FakeMCSClient::SendMessage(const MCSMessage& message) { + last_message_tag_ = message.tag(); + if (last_message_tag_ == kDataMessageStanzaTag) { + last_data_message_stanza_.CopyFrom( + reinterpret_cast( + message.GetProtobuf())); + } +} + +class AutoAdvancingTestClock : public base::Clock { + public: + explicit AutoAdvancingTestClock(base::TimeDelta auto_increment_time_delta); + + AutoAdvancingTestClock(const AutoAdvancingTestClock&) = delete; + AutoAdvancingTestClock& operator=(const AutoAdvancingTestClock&) = delete; + + ~AutoAdvancingTestClock() override; + + base::Time Now() const override; + void Advance(base::TimeDelta delta); + int call_count() const { return call_count_; } + + private: + mutable int call_count_; + base::TimeDelta auto_increment_time_delta_; + mutable base::Time now_; +}; + +AutoAdvancingTestClock::AutoAdvancingTestClock( + base::TimeDelta auto_increment_time_delta) + : call_count_(0), auto_increment_time_delta_(auto_increment_time_delta) { +} + +AutoAdvancingTestClock::~AutoAdvancingTestClock() { +} + +base::Time AutoAdvancingTestClock::Now() const { + call_count_++; + now_ += auto_increment_time_delta_; + return now_; +} + +void AutoAdvancingTestClock::Advance(base::TimeDelta delta) { + now_ += delta; +} + +class FakeGCMInternalsBuilder : public GCMInternalsBuilder { + public: + explicit FakeGCMInternalsBuilder(base::TimeDelta clock_step); + ~FakeGCMInternalsBuilder() override; + + base::Clock* GetClock() override; + std::unique_ptr BuildMCSClient( + const std::string& version, + base::Clock* clock, + ConnectionFactory* connection_factory, + GCMStore* gcm_store, + scoped_refptr io_task_runner, + GCMStatsRecorder* recorder) override; + std::unique_ptr BuildConnectionFactory( + const std::vector& endpoints, + const net::BackoffEntry::Policy& backoff_policy, + base::RepeatingCallback)> + get_socket_factory_callback, + scoped_refptr io_task_runner, + GCMStatsRecorder* recorder, + network::NetworkConnectionTracker* network_connection_tracker) override; + + private: + AutoAdvancingTestClock clock_; +}; + +FakeGCMInternalsBuilder::FakeGCMInternalsBuilder(base::TimeDelta clock_step) + : clock_(clock_step) {} + +FakeGCMInternalsBuilder::~FakeGCMInternalsBuilder() {} + +base::Clock* FakeGCMInternalsBuilder::GetClock() { + return &clock_; +} + +std::unique_ptr FakeGCMInternalsBuilder::BuildMCSClient( + const std::string& version, + base::Clock* clock, + ConnectionFactory* connection_factory, + GCMStore* gcm_store, + scoped_refptr io_task_runner, + GCMStatsRecorder* recorder) { + return base::WrapUnique(new FakeMCSClient( + clock, connection_factory, gcm_store, io_task_runner, recorder)); +} + +std::unique_ptr +FakeGCMInternalsBuilder::BuildConnectionFactory( + const std::vector& endpoints, + const net::BackoffEntry::Policy& backoff_policy, + base::RepeatingCallback)> + get_socket_factory_callback, + scoped_refptr io_task_runner, + GCMStatsRecorder* recorder, + network::NetworkConnectionTracker* network_connection_tracker) { + return base::WrapUnique(new FakeConnectionFactory()); +} + +} // namespace + +class GCMClientImplTest : public testing::Test, + public GCMClient::Delegate { + public: + GCMClientImplTest(); + ~GCMClientImplTest() override; + + void SetUp() override; + void TearDown() override; + + void SetFeatureParams(const base::Feature& feature, + const base::FieldTrialParams& params); + + void InitializeInvalidationFieldTrial(); + + void BuildGCMClient(base::TimeDelta clock_step); + void InitializeGCMClient(); + void StartGCMClient(); + void Register(const std::string& app_id, + const std::vector& senders); + void Unregister(const std::string& app_id); + void ReceiveMessageFromMCS(const MCSMessage& message); + void ReceiveOnMessageSentToMCS( + const std::string& app_id, + const std::string& message_id, + const MCSClient::MessageSendStatus status); + void FailCheckin(net::HttpStatusCode response_code); + void CompleteCheckin(uint64_t android_id, + uint64_t security_token, + const std::string& digest, + const std::map& settings); + void CompleteCheckinImpl(uint64_t android_id, + uint64_t security_token, + const std::string& digest, + const std::map& settings, + net::HttpStatusCode response_code); + void CompleteRegistration(const std::string& registration_id); + void CompleteUnregistration(const std::string& app_id); + + bool ExistsRegistration(const std::string& app_id) const; + void AddRegistration(const std::string& app_id, + const std::vector& sender_ids, + const std::string& registration_id); + + // GCMClient::Delegate overrides (for verification). + void OnRegisterFinished(scoped_refptr registration_info, + const std::string& registration_id, + GCMClient::Result result) override; + void OnUnregisterFinished(scoped_refptr registration_info, + GCMClient::Result result) override; + void OnSendFinished(const std::string& app_id, + const std::string& message_id, + GCMClient::Result result) override {} + void OnMessageReceived(const std::string& registration_id, + const IncomingMessage& message) override; + void OnMessagesDeleted(const std::string& app_id) override; + void OnMessageSendError( + const std::string& app_id, + const gcm::GCMClient::SendErrorDetails& send_error_details) override; + void OnSendAcknowledged(const std::string& app_id, + const std::string& message_id) override; + void OnGCMReady(const std::vector& account_mappings, + const base::Time& last_token_fetch_time) override; + void OnActivityRecorded() override {} + void OnConnected(const net::IPEndPoint& ip_endpoint) override {} + void OnDisconnected() override {} + void OnStoreReset() override {} + + GCMClientImpl* gcm_client() const { return gcm_client_.get(); } + GCMClientImpl::State gcm_client_state() const { + return gcm_client_->state_; + } + FakeMCSClient* mcs_client() const { + return static_cast(gcm_client_->mcs_client_.get()); + } + ConnectionFactory* connection_factory() const { + return gcm_client_->connection_factory_.get(); + } + + const GCMClientImpl::CheckinInfo& device_checkin_info() const { + return gcm_client_->device_checkin_info_; + } + + void reset_last_event() { + last_event_ = NONE; + last_app_id_.clear(); + last_registration_id_.clear(); + last_message_id_.clear(); + last_result_ = GCMClient::UNKNOWN_ERROR; + last_account_mappings_.clear(); + last_token_fetch_time_ = base::Time(); + } + + LastEvent last_event() const { return last_event_; } + const std::string& last_app_id() const { return last_app_id_; } + const std::string& last_registration_id() const { + return last_registration_id_; + } + const std::string& last_message_id() const { return last_message_id_; } + GCMClient::Result last_result() const { return last_result_; } + const IncomingMessage& last_message() const { return last_message_; } + const GCMClient::SendErrorDetails& last_error_details() const { + return last_error_details_; + } + const base::Time& last_token_fetch_time() const { + return last_token_fetch_time_; + } + const std::vector& last_account_mappings() { + return last_account_mappings_; + } + + const GServicesSettings& gservices_settings() const { + return gcm_client_->gservices_settings_; + } + + const base::FilePath& temp_directory_path() const { + return temp_directory_.GetPath(); + } + + base::FilePath gcm_store_path() const { + // Pass an non-existent directory as store path to match the exact + // behavior in the production code. Currently GCMStoreImpl checks if + // the directory exist or not to determine the store existence. + return temp_directory_.GetPath().Append(FILE_PATH_LITERAL("GCM Store")); + } + + int64_t CurrentTime(); + + // Tooling. + void PumpLoopUntilIdle(); + bool CreateUniqueTempDir(); + AutoAdvancingTestClock* clock() const { + return static_cast(gcm_client_->clock_); + } + network::TestURLLoaderFactory* url_loader_factory() { + return &test_url_loader_factory_; + } + + void FastForwardBy(const base::TimeDelta& duration) { + task_environment_.FastForwardBy(duration); + } + + private: + base::test::TaskEnvironment task_environment_{ + base::test::TaskEnvironment::TimeSource::MOCK_TIME}; + + // Must be declared first so that it is destroyed last. Injected to + // GCM client. + base::ScopedTempDir temp_directory_; + + // Variables used for verification. + LastEvent last_event_; + std::string last_app_id_; + std::string last_registration_id_; + std::string last_message_id_; + GCMClient::Result last_result_; + IncomingMessage last_message_; + GCMClient::SendErrorDetails last_error_details_; + base::Time last_token_fetch_time_; + std::vector last_account_mappings_; + + std::unique_ptr gcm_client_; + + // Injected to GCM client. + network::TestURLLoaderFactory test_url_loader_factory_; + base::test::ScopedFeatureList scoped_feature_list_; +}; + +GCMClientImplTest::GCMClientImplTest() + : last_event_(NONE), last_result_(GCMClient::UNKNOWN_ERROR) {} + +GCMClientImplTest::~GCMClientImplTest() {} + +void GCMClientImplTest::SetUp() { + testing::Test::SetUp(); + ASSERT_TRUE(CreateUniqueTempDir()); + BuildGCMClient(base::TimeDelta()); + InitializeGCMClient(); + StartGCMClient(); + InitializeInvalidationFieldTrial(); + ASSERT_NO_FATAL_FAILURE( + CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken, std::string(), + std::map())); +} + +void GCMClientImplTest::TearDown() { + gcm_client_.reset(); + PumpLoopUntilIdle(); + testing::Test::TearDown(); +} + +void GCMClientImplTest::SetFeatureParams(const base::Feature& feature, + const base::FieldTrialParams& params) { + scoped_feature_list_.InitAndEnableFeatureWithParameters(feature, params); + + base::FieldTrialParams actual_params; + EXPECT_TRUE(base::GetFieldTrialParamsByFeature( + features::kInvalidateTokenFeature, &actual_params)); + EXPECT_EQ(params, actual_params); +} + +void GCMClientImplTest::InitializeInvalidationFieldTrial() { + std::map params; + params[features::kParamNameTokenInvalidationPeriodDays] = + std::to_string(kTestTokenInvalidationPeriod); + ASSERT_NO_FATAL_FAILURE( + SetFeatureParams(features::kInvalidateTokenFeature, std::move(params))); +} + +void GCMClientImplTest::PumpLoopUntilIdle() { + task_environment_.RunUntilIdle(); +} + +bool GCMClientImplTest::CreateUniqueTempDir() { + return temp_directory_.CreateUniqueTempDir(); +} + +void GCMClientImplTest::BuildGCMClient(base::TimeDelta clock_step) { + gcm_client_ = + std::make_unique(base::WrapUnique( + new FakeGCMInternalsBuilder(clock_step))); +} + +void GCMClientImplTest::FailCheckin(net::HttpStatusCode response_code) { + std::map settings; + CompleteCheckinImpl(0, 0, GServicesSettings::CalculateDigest(settings), + settings, response_code); +} + +void GCMClientImplTest::CompleteCheckin( + uint64_t android_id, + uint64_t security_token, + const std::string& digest, + const std::map& settings) { + CompleteCheckinImpl(android_id, security_token, digest, settings, + net::HTTP_OK); +} + +void GCMClientImplTest::CompleteCheckinImpl( + uint64_t android_id, + uint64_t security_token, + const std::string& digest, + const std::map& settings, + net::HttpStatusCode response_code) { + checkin_proto::AndroidCheckinResponse response; + response.set_stats_ok(true); + response.set_android_id(android_id); + response.set_security_token(security_token); + + // For testing G-services settings. + if (!digest.empty()) { + response.set_digest(digest); + for (auto it = settings.begin(); it != settings.end(); ++it) { + checkin_proto::GservicesSetting* setting = response.add_setting(); + setting->set_name(it->first); + setting->set_value(it->second); + } + response.set_settings_diff(false); + } + + std::string response_string; + response.SerializeToString(&response_string); + + EXPECT_TRUE(url_loader_factory()->SimulateResponseForPendingRequest( + gservices_settings().GetCheckinURL(), + network::URLLoaderCompletionStatus(net::OK), + network::CreateURLResponseHead(response_code), response_string)); + // Give a chance for GCMStoreImpl::Backend to finish persisting data. + PumpLoopUntilIdle(); +} + +void GCMClientImplTest::CompleteRegistration( + const std::string& registration_id) { + std::string response(kRegistrationResponsePrefix); + response.append(registration_id); + + EXPECT_TRUE(url_loader_factory()->SimulateResponseForPendingRequest( + GURL(kRegisterUrl), network::URLLoaderCompletionStatus(net::OK), + network::CreateURLResponseHead(net::HTTP_OK), response)); + + // Give a chance for GCMStoreImpl::Backend to finish persisting data. + PumpLoopUntilIdle(); +} + +void GCMClientImplTest::CompleteUnregistration( + const std::string& app_id) { + std::string response(kUnregistrationResponsePrefix); + response.append(app_id); + + EXPECT_TRUE(url_loader_factory()->SimulateResponseForPendingRequest( + GURL(kRegisterUrl), network::URLLoaderCompletionStatus(net::OK), + network::CreateURLResponseHead(net::HTTP_OK), response)); + + // Give a chance for GCMStoreImpl::Backend to finish persisting data. + PumpLoopUntilIdle(); +} + +bool GCMClientImplTest::ExistsRegistration(const std::string& app_id) const { + return ExistsGCMRegistrationInMap(gcm_client_->registrations_, app_id); +} + +void GCMClientImplTest::AddRegistration( + const std::string& app_id, + const std::vector& sender_ids, + const std::string& registration_id) { + auto registration = base::MakeRefCounted(); + registration->app_id = app_id; + registration->sender_ids = sender_ids; + gcm_client_->registrations_.emplace(std::move(registration), registration_id); +} + +void GCMClientImplTest::InitializeGCMClient() { + clock()->Advance(base::Milliseconds(1)); + + // Actual initialization. + GCMClient::ChromeBuildInfo chrome_build_info; + chrome_build_info.version = kChromeVersion; + chrome_build_info.product_category_for_subtypes = kProductCategoryForSubtypes; + gcm_client_->Initialize( + chrome_build_info, gcm_store_path(), + /*remove_account_mappings_with_email_key=*/true, + task_environment_.GetMainThreadTaskRunner(), + base::ThreadTaskRunnerHandle::Get(), base::DoNothing(), + base::MakeRefCounted( + &test_url_loader_factory_), + network::TestNetworkConnectionTracker::GetInstance(), + base::WrapUnique(new FakeEncryptor), this); +} + +void GCMClientImplTest::StartGCMClient() { + // Start loading and check-in. + gcm_client_->Start(GCMClient::IMMEDIATE_START); + + PumpLoopUntilIdle(); +} + +void GCMClientImplTest::Register(const std::string& app_id, + const std::vector& senders) { + auto gcm_info = base::MakeRefCounted(); + gcm_info->app_id = app_id; + gcm_info->sender_ids = senders; + gcm_client()->Register(std::move(gcm_info)); +} + +void GCMClientImplTest::Unregister(const std::string& app_id) { + auto gcm_info = base::MakeRefCounted(); + gcm_info->app_id = app_id; + gcm_client()->Unregister(std::move(gcm_info)); +} + +void GCMClientImplTest::ReceiveMessageFromMCS(const MCSMessage& message) { + gcm_client_->recorder_.RecordConnectionInitiated(std::string()); + gcm_client_->recorder_.RecordConnectionSuccess(); + gcm_client_->OnMessageReceivedFromMCS(message); +} + +void GCMClientImplTest::ReceiveOnMessageSentToMCS( + const std::string& app_id, + const std::string& message_id, + const MCSClient::MessageSendStatus status) { + gcm_client_->OnMessageSentToMCS(0LL, app_id, message_id, status); +} + +void GCMClientImplTest::OnGCMReady( + const std::vector& account_mappings, + const base::Time& last_token_fetch_time) { + last_event_ = LOADING_COMPLETED; + last_account_mappings_ = account_mappings; + last_token_fetch_time_ = last_token_fetch_time; +} + +void GCMClientImplTest::OnMessageReceived(const std::string& registration_id, + const IncomingMessage& message) { + last_event_ = MESSAGE_RECEIVED; + last_app_id_ = registration_id; + last_message_ = message; +} + +void GCMClientImplTest::OnRegisterFinished( + scoped_refptr registration_info, + const std::string& registration_id, + GCMClient::Result result) { + last_event_ = REGISTRATION_COMPLETED; + last_app_id_ = registration_info->app_id; + last_registration_id_ = registration_id; + last_result_ = result; +} + +void GCMClientImplTest::OnUnregisterFinished( + scoped_refptr registration_info, + GCMClient::Result result) { + last_event_ = UNREGISTRATION_COMPLETED; + last_app_id_ = registration_info->app_id; + last_result_ = result; +} + +void GCMClientImplTest::OnMessagesDeleted(const std::string& app_id) { + last_event_ = MESSAGES_DELETED; + last_app_id_ = app_id; +} + +void GCMClientImplTest::OnMessageSendError( + const std::string& app_id, + const gcm::GCMClient::SendErrorDetails& send_error_details) { + last_event_ = MESSAGE_SEND_ERROR; + last_app_id_ = app_id; + last_error_details_ = send_error_details; +} + +void GCMClientImplTest::OnSendAcknowledged(const std::string& app_id, + const std::string& message_id) { + last_event_ = MESSAGE_SEND_ACK; + last_app_id_ = app_id; + last_message_id_ = message_id; +} + +int64_t GCMClientImplTest::CurrentTime() { + return clock()->Now().ToInternalValue() / base::Time::kMicrosecondsPerSecond; +} + +TEST_F(GCMClientImplTest, LoadingCompleted) { + EXPECT_EQ(LOADING_COMPLETED, last_event()); + EXPECT_EQ(kDeviceAndroidId, mcs_client()->last_android_id()); + EXPECT_EQ(kDeviceSecurityToken, mcs_client()->last_security_token()); + + // Checking freshly loaded CheckinInfo. + EXPECT_EQ(kDeviceAndroidId, device_checkin_info().android_id); + EXPECT_EQ(kDeviceSecurityToken, device_checkin_info().secret); + EXPECT_TRUE(device_checkin_info().last_checkin_accounts.empty()); + EXPECT_TRUE(device_checkin_info().accounts_set); + EXPECT_TRUE(device_checkin_info().account_tokens.empty()); +} + +TEST_F(GCMClientImplTest, LoadingBusted) { + // Close the GCM store. + gcm_client()->Stop(); + PumpLoopUntilIdle(); + + // Mess up the store. + EXPECT_TRUE(leveldb_chrome::CorruptClosedDBForTesting(gcm_store_path())); + + // Restart GCM client. The store should be reset and the loading should + // complete successfully. + reset_last_event(); + BuildGCMClient(base::TimeDelta()); + InitializeGCMClient(); + StartGCMClient(); + ASSERT_NO_FATAL_FAILURE( + CompleteCheckin(kDeviceAndroidId2, kDeviceSecurityToken2, std::string(), + std::map())); + + EXPECT_EQ(LOADING_COMPLETED, last_event()); + EXPECT_EQ(kDeviceAndroidId2, mcs_client()->last_android_id()); + EXPECT_EQ(kDeviceSecurityToken2, mcs_client()->last_security_token()); +} + +TEST_F(GCMClientImplTest, LoadingWithEmptyDirectory) { + // Close the GCM store. + gcm_client()->Stop(); + PumpLoopUntilIdle(); + + // Make the store directory empty, to simulate a previous destroy store + // operation failing to delete the store directory. + ASSERT_TRUE(base::DeletePathRecursively(gcm_store_path())); + ASSERT_TRUE(base::CreateDirectory(gcm_store_path())); + + base::HistogramTester histogram_tester; + + // Restart GCM client. The store should be considered to not exist. + BuildGCMClient(base::TimeDelta()); + InitializeGCMClient(); + gcm_client()->Start(GCMClient::DELAYED_START); + PumpLoopUntilIdle(); + histogram_tester.ExpectUniqueSample("GCM.LoadStatus", + 13 /* STORE_DOES_NOT_EXIST */, 1); + // Since the store does not exist, the database should not have been opened. + histogram_tester.ExpectTotalCount("GCM.Database.Open", 0); + // Without a store, DELAYED_START loading should only reach INITIALIZED state. + EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state()); + + // The store directory should still exist (and be empty). If not, then the + // DELAYED_START load has probably reset the store, rather than leaving that + // to the next IMMEDIATE_START load as expected. + ASSERT_TRUE(base::DirectoryExists(gcm_store_path())); + ASSERT_FALSE( + base::PathExists(gcm_store_path().Append(FILE_PATH_LITERAL("CURRENT")))); + + // IMMEDIATE_START loading should successfully create a new store despite the + // empty directory. + reset_last_event(); + StartGCMClient(); + ASSERT_NO_FATAL_FAILURE( + CompleteCheckin(kDeviceAndroidId2, kDeviceSecurityToken2, std::string(), + std::map())); + EXPECT_EQ(LOADING_COMPLETED, last_event()); + EXPECT_EQ(GCMClientImpl::READY, gcm_client_state()); + EXPECT_EQ(kDeviceAndroidId2, mcs_client()->last_android_id()); + EXPECT_EQ(kDeviceSecurityToken2, mcs_client()->last_security_token()); +} + +TEST_F(GCMClientImplTest, DestroyStoreWhenNotNeeded) { + // Close the GCM store. + gcm_client()->Stop(); + PumpLoopUntilIdle(); + + // Restart GCM client. The store is loaded successfully. + reset_last_event(); + BuildGCMClient(base::TimeDelta()); + InitializeGCMClient(); + gcm_client()->Start(GCMClient::DELAYED_START); + PumpLoopUntilIdle(); + + EXPECT_EQ(GCMClientImpl::LOADED, gcm_client_state()); + EXPECT_TRUE(device_checkin_info().android_id); + EXPECT_TRUE(device_checkin_info().secret); + + // Fast forward the clock to trigger the store destroying logic. + FastForwardBy(base::Milliseconds(300000)); + PumpLoopUntilIdle(); + + EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state()); + EXPECT_FALSE(device_checkin_info().android_id); + EXPECT_FALSE(device_checkin_info().secret); +} + +TEST_F(GCMClientImplTest, SerializeAndDeserialize) { + std::vector senders{"sender"}; + auto gcm_info = base::MakeRefCounted(); + gcm_info->app_id = kExtensionAppId; + gcm_info->sender_ids = senders; + gcm_info->last_validated = clock()->Now(); + + auto gcm_info_deserialized = base::MakeRefCounted(); + std::string gcm_registration_id_deserialized; + { + std::string serialized_key = gcm_info->GetSerializedKey(); + std::string serialized_value = + gcm_info->GetSerializedValue(kRegistrationId); + + ASSERT_TRUE(gcm_info_deserialized->Deserialize( + serialized_key, serialized_value, &gcm_registration_id_deserialized)); + } + + EXPECT_EQ(gcm_info->app_id, gcm_info_deserialized->app_id); + EXPECT_EQ(gcm_info->sender_ids, gcm_info_deserialized->sender_ids); + EXPECT_EQ(gcm_info->last_validated, gcm_info_deserialized->last_validated); + EXPECT_EQ(kRegistrationId, gcm_registration_id_deserialized); + + auto instance_id_info = base::MakeRefCounted(); + instance_id_info->app_id = kExtensionAppId; + instance_id_info->last_validated = clock()->Now(); + instance_id_info->authorized_entity = "different_sender"; + instance_id_info->scope = "scope"; + + auto instance_id_info_deserialized = + base::MakeRefCounted(); + std::string instance_id_registration_id_deserialized; + { + std::string serialized_key = instance_id_info->GetSerializedKey(); + std::string serialized_value = + instance_id_info->GetSerializedValue(kRegistrationId); + + ASSERT_TRUE(instance_id_info_deserialized->Deserialize( + serialized_key, serialized_value, + &instance_id_registration_id_deserialized)); + } + + EXPECT_EQ(instance_id_info->app_id, instance_id_info_deserialized->app_id); + EXPECT_EQ(instance_id_info->last_validated, + instance_id_info_deserialized->last_validated); + EXPECT_EQ(instance_id_info->authorized_entity, + instance_id_info_deserialized->authorized_entity); + EXPECT_EQ(instance_id_info->scope, instance_id_info_deserialized->scope); + EXPECT_EQ(kRegistrationId, instance_id_registration_id_deserialized); +} + +TEST_F(GCMClientImplTest, RegisterApp) { + EXPECT_FALSE(ExistsRegistration(kExtensionAppId)); + + std::vector senders; + senders.push_back("sender"); + Register(kExtensionAppId, senders); + ASSERT_NO_FATAL_FAILURE(CompleteRegistration("reg_id")); + + EXPECT_EQ(REGISTRATION_COMPLETED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ("reg_id", last_registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, last_result()); + EXPECT_TRUE(ExistsRegistration(kExtensionAppId)); +} + +TEST_F(GCMClientImplTest, RegisterAppFromCache) { + EXPECT_FALSE(ExistsRegistration(kExtensionAppId)); + + std::vector senders; + senders.push_back("sender"); + Register(kExtensionAppId, senders); + ASSERT_NO_FATAL_FAILURE(CompleteRegistration("reg_id")); + EXPECT_TRUE(ExistsRegistration(kExtensionAppId)); + + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ("reg_id", last_registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, last_result()); + EXPECT_EQ(REGISTRATION_COMPLETED, last_event()); + + // Recreate GCMClient in order to load from the persistent store. + BuildGCMClient(base::TimeDelta()); + InitializeGCMClient(); + StartGCMClient(); + + EXPECT_TRUE(ExistsRegistration(kExtensionAppId)); +} + +TEST_F(GCMClientImplTest, RegisterPreviousSenderAgain) { + EXPECT_FALSE(ExistsRegistration(kExtensionAppId)); + + // Register a sender. + std::vector senders; + senders.push_back("sender"); + Register(kExtensionAppId, senders); + ASSERT_NO_FATAL_FAILURE(CompleteRegistration("reg_id")); + + EXPECT_EQ(REGISTRATION_COMPLETED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ("reg_id", last_registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, last_result()); + EXPECT_TRUE(ExistsRegistration(kExtensionAppId)); + + reset_last_event(); + + // Register a different sender. Different registration ID from previous one + // should be returned. + std::vector senders2; + senders2.push_back("sender2"); + Register(kExtensionAppId, senders2); + ASSERT_NO_FATAL_FAILURE(CompleteRegistration("reg_id2")); + + EXPECT_EQ(REGISTRATION_COMPLETED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ("reg_id2", last_registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, last_result()); + EXPECT_TRUE(ExistsRegistration(kExtensionAppId)); + + reset_last_event(); + + // Register the 1st sender again. Different registration ID from previous one + // should be returned. + std::vector senders3; + senders3.push_back("sender"); + Register(kExtensionAppId, senders3); + ASSERT_NO_FATAL_FAILURE(CompleteRegistration("reg_id")); + + EXPECT_EQ(REGISTRATION_COMPLETED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ("reg_id", last_registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, last_result()); + EXPECT_TRUE(ExistsRegistration(kExtensionAppId)); +} + +TEST_F(GCMClientImplTest, DISABLED_RegisterAgainWhenTokenIsFresh) { + // Register a sender. + std::vector senders; + senders.push_back("sender"); + Register(kExtensionAppId, senders); + ASSERT_NO_FATAL_FAILURE(CompleteRegistration("reg_id")); + EXPECT_EQ(REGISTRATION_COMPLETED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ("reg_id", last_registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, last_result()); + EXPECT_TRUE(ExistsRegistration(kExtensionAppId)); + + reset_last_event(); + + // Advance time by (kTestTokenInvalidationPeriod)/2 + clock()->Advance(base::Days(kTestTokenInvalidationPeriod / 2)); + + // Register the same sender again. The same registration ID as the + // previous one should be returned, and we should *not* send a + // registration request to the GCM server. + Register(kExtensionAppId, senders); + PumpLoopUntilIdle(); + + EXPECT_EQ(REGISTRATION_COMPLETED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ("reg_id", last_registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, last_result()); + EXPECT_TRUE(ExistsRegistration(kExtensionAppId)); +} + +TEST_F(GCMClientImplTest, RegisterAgainWhenTokenIsStale) { + // Register a sender. + std::vector senders; + senders.push_back("sender"); + Register(kExtensionAppId, senders); + ASSERT_NO_FATAL_FAILURE(CompleteRegistration("reg_id")); + + EXPECT_EQ(REGISTRATION_COMPLETED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ("reg_id", last_registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, last_result()); + EXPECT_TRUE(ExistsRegistration(kExtensionAppId)); + + reset_last_event(); + + // Advance time by kTestTokenInvalidationPeriod + clock()->Advance(base::Days(kTestTokenInvalidationPeriod)); + + // Register the same sender again. Different registration ID from the + // previous one should be returned. + Register(kExtensionAppId, senders); + ASSERT_NO_FATAL_FAILURE(CompleteRegistration("reg_id2")); + + EXPECT_EQ(REGISTRATION_COMPLETED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ("reg_id2", last_registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, last_result()); + EXPECT_TRUE(ExistsRegistration(kExtensionAppId)); +} + +TEST_F(GCMClientImplTest, UnregisterApp) { + EXPECT_FALSE(ExistsRegistration(kExtensionAppId)); + + std::vector senders; + senders.push_back("sender"); + Register(kExtensionAppId, senders); + ASSERT_NO_FATAL_FAILURE(CompleteRegistration("reg_id")); + EXPECT_TRUE(ExistsRegistration(kExtensionAppId)); + + Unregister(kExtensionAppId); + ASSERT_NO_FATAL_FAILURE(CompleteUnregistration(kExtensionAppId)); + + EXPECT_EQ(UNREGISTRATION_COMPLETED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ(GCMClient::SUCCESS, last_result()); + EXPECT_FALSE(ExistsRegistration(kExtensionAppId)); +} + +// Tests that stopping the GCMClient also deletes pending registration requests. +// This is tested by checking that url fetcher contained in the request was +// deleted. +TEST_F(GCMClientImplTest, DeletePendingRequestsWhenStopping) { + std::vector senders; + senders.push_back("sender"); + Register(kExtensionAppId, senders); + + gcm_client()->Stop(); + PumpLoopUntilIdle(); + EXPECT_EQ(0, url_loader_factory()->NumPending()); +} + +TEST_F(GCMClientImplTest, DispatchDownstreamMessage) { + // Register to receive messages from kSender and kSender2 only. + std::vector senders; + senders.push_back(kSender); + senders.push_back(kSender2); + AddRegistration(kExtensionAppId, senders, "reg_id"); + + std::map expected_data; + expected_data["message_type"] = "gcm"; + expected_data["key"] = "value"; + expected_data["key2"] = "value2"; + + // Message for kSender will be received. + MCSMessage message(BuildDownstreamMessage( + kSender, kExtensionAppId, std::string() /* subtype */, expected_data, + std::string() /* raw_data */)); + EXPECT_TRUE(message.IsValid()); + ReceiveMessageFromMCS(message); + + expected_data.erase(expected_data.find("message_type")); + EXPECT_EQ(MESSAGE_RECEIVED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ(expected_data.size(), last_message().data.size()); + EXPECT_EQ(expected_data, last_message().data); + EXPECT_EQ(kSender, last_message().sender_id); + + reset_last_event(); + + // Message for kSender2 will be received. + MCSMessage message2(BuildDownstreamMessage( + kSender2, kExtensionAppId, std::string() /* subtype */, expected_data, + std::string() /* raw_data */)); + EXPECT_TRUE(message2.IsValid()); + ReceiveMessageFromMCS(message2); + + EXPECT_EQ(MESSAGE_RECEIVED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ(expected_data.size(), last_message().data.size()); + EXPECT_EQ(expected_data, last_message().data); + EXPECT_EQ(kSender2, last_message().sender_id); +} + +TEST_F(GCMClientImplTest, DispatchDownstreamMessageRawData) { + std::vector senders(1, kSender); + AddRegistration(kExtensionAppId, senders, "reg_id"); + + std::map expected_data; + + MCSMessage message(BuildDownstreamMessage(kSender, kExtensionAppId, + std::string() /* subtype */, + expected_data, kRawData)); + EXPECT_TRUE(message.IsValid()); + ReceiveMessageFromMCS(message); + + EXPECT_EQ(MESSAGE_RECEIVED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ(expected_data.size(), last_message().data.size()); + EXPECT_EQ(kSender, last_message().sender_id); + EXPECT_EQ(kRawData, last_message().raw_data); +} + +TEST_F(GCMClientImplTest, DISABLED_DispatchDownstreamMessageSendError) { + std::map expected_data = { + {"message_type", "send_error"}, {"error_details", "some details"}}; + + MCSMessage message(BuildDownstreamMessage( + kSender, kExtensionAppId, std::string() /* subtype */, expected_data, + std::string() /* raw_data */)); + EXPECT_TRUE(message.IsValid()); + ReceiveMessageFromMCS(message); + + EXPECT_EQ(MESSAGE_SEND_ERROR, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ(kMessageId, last_error_details().message_id); + EXPECT_EQ(1UL, last_error_details().additional_data.size()); + auto iter = last_error_details().additional_data.find("error_details"); + EXPECT_TRUE(iter != last_error_details().additional_data.end()); + EXPECT_EQ("some details", iter->second); +} + +TEST_F(GCMClientImplTest, DispatchDownstreamMessgaesDeleted) { + std::map expected_data; + expected_data["message_type"] = "deleted_messages"; + MCSMessage message(BuildDownstreamMessage( + kSender, kExtensionAppId, std::string() /* subtype */, expected_data, + std::string() /* raw_data */)); + EXPECT_TRUE(message.IsValid()); + ReceiveMessageFromMCS(message); + + EXPECT_EQ(MESSAGES_DELETED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); +} + +TEST_F(GCMClientImplTest, SendMessage) { + OutgoingMessage message; + message.id = "007"; + message.time_to_live = 500; + message.data["key"] = "value"; + gcm_client()->Send(kExtensionAppId, kSender, message); + + EXPECT_EQ(kDataMessageStanzaTag, mcs_client()->last_message_tag()); + EXPECT_EQ(kExtensionAppId, + mcs_client()->last_data_message_stanza().category()); + EXPECT_EQ(kSender, mcs_client()->last_data_message_stanza().to()); + EXPECT_EQ(500, mcs_client()->last_data_message_stanza().ttl()); + EXPECT_EQ(CurrentTime(), mcs_client()->last_data_message_stanza().sent()); + EXPECT_EQ("007", mcs_client()->last_data_message_stanza().id()); + EXPECT_EQ("gcm@chrome.com", mcs_client()->last_data_message_stanza().from()); + EXPECT_EQ(kSender, mcs_client()->last_data_message_stanza().to()); + EXPECT_EQ("key", mcs_client()->last_data_message_stanza().app_data(0).key()); + EXPECT_EQ("value", + mcs_client()->last_data_message_stanza().app_data(0).value()); +} + +TEST_F(GCMClientImplTest, SendMessageAcknowledged) { + ReceiveOnMessageSentToMCS(kExtensionAppId, "007", MCSClient::SENT); + EXPECT_EQ(MESSAGE_SEND_ACK, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ("007", last_message_id()); +} + +class GCMClientImplCheckinTest : public GCMClientImplTest { + public: + GCMClientImplCheckinTest(); + ~GCMClientImplCheckinTest() override; + + void SetUp() override; +}; + +GCMClientImplCheckinTest::GCMClientImplCheckinTest() { +} + +GCMClientImplCheckinTest::~GCMClientImplCheckinTest() { +} + +void GCMClientImplCheckinTest::SetUp() { + testing::Test::SetUp(); + // Creating unique temp directory that will be used by GCMStore shared between + // GCM Client and G-services settings. + ASSERT_TRUE(CreateUniqueTempDir()); + // Time will be advancing one hour every time it is checked. + BuildGCMClient(base::Seconds(kSettingsCheckinInterval)); + InitializeGCMClient(); + StartGCMClient(); +} + +TEST_F(GCMClientImplCheckinTest, GServicesSettingsAfterInitialCheckin) { + std::map settings; + settings["checkin_interval"] = base::NumberToString(kSettingsCheckinInterval); + settings["checkin_url"] = "http://alternative.url/checkin"; + settings["gcm_hostname"] = "alternative.gcm.host"; + settings["gcm_secure_port"] = "7777"; + settings["gcm_registration_url"] = "http://alternative.url/registration"; + ASSERT_NO_FATAL_FAILURE( + CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken, + GServicesSettings::CalculateDigest(settings), settings)); + EXPECT_EQ(base::Seconds(kSettingsCheckinInterval), + gservices_settings().GetCheckinInterval()); + EXPECT_EQ(GURL("http://alternative.url/checkin"), + gservices_settings().GetCheckinURL()); + EXPECT_EQ(GURL("http://alternative.url/registration"), + gservices_settings().GetRegistrationURL()); + EXPECT_EQ(GURL("https://alternative.gcm.host:7777"), + gservices_settings().GetMCSMainEndpoint()); + EXPECT_EQ(GURL("https://alternative.gcm.host:443"), + gservices_settings().GetMCSFallbackEndpoint()); +} + +// This test only checks that periodic checkin happens. +TEST_F(GCMClientImplCheckinTest, PeriodicCheckin) { + std::map settings; + settings["checkin_interval"] = base::NumberToString(kSettingsCheckinInterval); + settings["checkin_url"] = "http://alternative.url/checkin"; + settings["gcm_hostname"] = "alternative.gcm.host"; + settings["gcm_secure_port"] = "7777"; + settings["gcm_registration_url"] = "http://alternative.url/registration"; + ASSERT_NO_FATAL_FAILURE( + CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken, + GServicesSettings::CalculateDigest(settings), settings)); + + EXPECT_EQ(2, clock()->call_count()); + + PumpLoopUntilIdle(); + ASSERT_NO_FATAL_FAILURE( + CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken, + GServicesSettings::CalculateDigest(settings), settings)); +} + +TEST_F(GCMClientImplCheckinTest, LoadGSettingsFromStore) { + std::map settings; + settings["checkin_interval"] = base::NumberToString(kSettingsCheckinInterval); + settings["checkin_url"] = "http://alternative.url/checkin"; + settings["gcm_hostname"] = "alternative.gcm.host"; + settings["gcm_secure_port"] = "7777"; + settings["gcm_registration_url"] = "http://alternative.url/registration"; + ASSERT_NO_FATAL_FAILURE( + CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken, + GServicesSettings::CalculateDigest(settings), settings)); + + BuildGCMClient(base::TimeDelta()); + InitializeGCMClient(); + StartGCMClient(); + + EXPECT_EQ(base::Seconds(kSettingsCheckinInterval), + gservices_settings().GetCheckinInterval()); + EXPECT_EQ(GURL("http://alternative.url/checkin"), + gservices_settings().GetCheckinURL()); + EXPECT_EQ(GURL("http://alternative.url/registration"), + gservices_settings().GetRegistrationURL()); + EXPECT_EQ(GURL("https://alternative.gcm.host:7777"), + gservices_settings().GetMCSMainEndpoint()); + EXPECT_EQ(GURL("https://alternative.gcm.host:443"), + gservices_settings().GetMCSFallbackEndpoint()); +} + +// This test only checks that periodic checkin happens. +TEST_F(GCMClientImplCheckinTest, CheckinWithAccounts) { + std::map settings; + settings["checkin_interval"] = base::NumberToString(kSettingsCheckinInterval); + settings["checkin_url"] = "http://alternative.url/checkin"; + settings["gcm_hostname"] = "alternative.gcm.host"; + settings["gcm_secure_port"] = "7777"; + settings["gcm_registration_url"] = "http://alternative.url/registration"; + ASSERT_NO_FATAL_FAILURE( + CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken, + GServicesSettings::CalculateDigest(settings), settings)); + + std::vector account_tokens; + account_tokens.push_back(MakeAccountToken("test_user1@gmail.com", "token1")); + account_tokens.push_back(MakeAccountToken("test_user2@gmail.com", "token2")); + gcm_client()->SetAccountTokens(account_tokens); + + EXPECT_TRUE(device_checkin_info().last_checkin_accounts.empty()); + EXPECT_TRUE(device_checkin_info().accounts_set); + EXPECT_EQ(MakeEmailToTokenMap(account_tokens), + device_checkin_info().account_tokens); + + PumpLoopUntilIdle(); + ASSERT_NO_FATAL_FAILURE( + CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken, + GServicesSettings::CalculateDigest(settings), settings)); + + std::set accounts; + accounts.insert("test_user1@gmail.com"); + accounts.insert("test_user2@gmail.com"); + EXPECT_EQ(accounts, device_checkin_info().last_checkin_accounts); + EXPECT_TRUE(device_checkin_info().accounts_set); + EXPECT_EQ(MakeEmailToTokenMap(account_tokens), + device_checkin_info().account_tokens); +} + +// This test only checks that periodic checkin happens. +TEST_F(GCMClientImplCheckinTest, CheckinWhenAccountRemoved) { + std::map settings; + settings["checkin_interval"] = base::NumberToString(kSettingsCheckinInterval); + settings["checkin_url"] = "http://alternative.url/checkin"; + settings["gcm_hostname"] = "alternative.gcm.host"; + settings["gcm_secure_port"] = "7777"; + settings["gcm_registration_url"] = "http://alternative.url/registration"; + ASSERT_NO_FATAL_FAILURE( + CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken, + GServicesSettings::CalculateDigest(settings), settings)); + + std::vector account_tokens; + account_tokens.push_back(MakeAccountToken("test_user1@gmail.com", "token1")); + account_tokens.push_back(MakeAccountToken("test_user2@gmail.com", "token2")); + gcm_client()->SetAccountTokens(account_tokens); + PumpLoopUntilIdle(); + ASSERT_NO_FATAL_FAILURE( + CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken, + GServicesSettings::CalculateDigest(settings), settings)); + + EXPECT_EQ(2UL, device_checkin_info().last_checkin_accounts.size()); + EXPECT_TRUE(device_checkin_info().accounts_set); + EXPECT_EQ(MakeEmailToTokenMap(account_tokens), + device_checkin_info().account_tokens); + + account_tokens.erase(account_tokens.begin() + 1); + gcm_client()->SetAccountTokens(account_tokens); + + PumpLoopUntilIdle(); + ASSERT_NO_FATAL_FAILURE( + CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken, + GServicesSettings::CalculateDigest(settings), settings)); + + std::set accounts; + accounts.insert("test_user1@gmail.com"); + EXPECT_EQ(accounts, device_checkin_info().last_checkin_accounts); + EXPECT_TRUE(device_checkin_info().accounts_set); + EXPECT_EQ(MakeEmailToTokenMap(account_tokens), + device_checkin_info().account_tokens); +} + +// This test only checks that periodic checkin happens. +TEST_F(GCMClientImplCheckinTest, CheckinWhenAccountReplaced) { + std::map settings; + settings["checkin_interval"] = base::NumberToString(kSettingsCheckinInterval); + settings["checkin_url"] = "http://alternative.url/checkin"; + settings["gcm_hostname"] = "alternative.gcm.host"; + settings["gcm_secure_port"] = "7777"; + settings["gcm_registration_url"] = "http://alternative.url/registration"; + ASSERT_NO_FATAL_FAILURE( + CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken, + GServicesSettings::CalculateDigest(settings), settings)); + + std::vector account_tokens; + account_tokens.push_back(MakeAccountToken("test_user1@gmail.com", "token1")); + gcm_client()->SetAccountTokens(account_tokens); + + PumpLoopUntilIdle(); + ASSERT_NO_FATAL_FAILURE( + CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken, + GServicesSettings::CalculateDigest(settings), settings)); + + std::set accounts; + accounts.insert("test_user1@gmail.com"); + EXPECT_EQ(accounts, device_checkin_info().last_checkin_accounts); + + // This should trigger another checkin, because the list of accounts is + // different. + account_tokens.clear(); + account_tokens.push_back(MakeAccountToken("test_user2@gmail.com", "token2")); + gcm_client()->SetAccountTokens(account_tokens); + + PumpLoopUntilIdle(); + ASSERT_NO_FATAL_FAILURE( + CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken, + GServicesSettings::CalculateDigest(settings), settings)); + + accounts.clear(); + accounts.insert("test_user2@gmail.com"); + EXPECT_EQ(accounts, device_checkin_info().last_checkin_accounts); + EXPECT_TRUE(device_checkin_info().accounts_set); + EXPECT_EQ(MakeEmailToTokenMap(account_tokens), + device_checkin_info().account_tokens); +} + +TEST_F(GCMClientImplCheckinTest, ResetStoreWhenCheckinRejected) { + base::HistogramTester histogram_tester; + std::map settings; + ASSERT_NO_FATAL_FAILURE(FailCheckin(net::HTTP_UNAUTHORIZED)); + PumpLoopUntilIdle(); + + // Store should have been destroyed. Restart client and verify the initial + // checkin response is persisted. + BuildGCMClient(base::TimeDelta()); + InitializeGCMClient(); + StartGCMClient(); + ASSERT_NO_FATAL_FAILURE( + CompleteCheckin(kDeviceAndroidId2, kDeviceSecurityToken2, + GServicesSettings::CalculateDigest(settings), settings)); + + EXPECT_EQ(LOADING_COMPLETED, last_event()); + EXPECT_EQ(kDeviceAndroidId2, mcs_client()->last_android_id()); + EXPECT_EQ(kDeviceSecurityToken2, mcs_client()->last_security_token()); +} + +class GCMClientImplStartAndStopTest : public GCMClientImplTest { + public: + GCMClientImplStartAndStopTest(); + ~GCMClientImplStartAndStopTest() override; + + void SetUp() override; + + void DefaultCompleteCheckin(); +}; + +GCMClientImplStartAndStopTest::GCMClientImplStartAndStopTest() { +} + +GCMClientImplStartAndStopTest::~GCMClientImplStartAndStopTest() { +} + +void GCMClientImplStartAndStopTest::SetUp() { + testing::Test::SetUp(); + ASSERT_TRUE(CreateUniqueTempDir()); + BuildGCMClient(base::TimeDelta()); + InitializeGCMClient(); +} + +void GCMClientImplStartAndStopTest::DefaultCompleteCheckin() { + ASSERT_NO_FATAL_FAILURE( + CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken, std::string(), + std::map())); + PumpLoopUntilIdle(); +} + +TEST_F(GCMClientImplStartAndStopTest, DISABLED_StartStopAndRestart) { + // GCMClientImpl should be in INITIALIZED state at first. + EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state()); + + // Delay start the GCM. + gcm_client()->Start(GCMClient::DELAYED_START); + PumpLoopUntilIdle(); + EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state()); + + // Stop the GCM. + gcm_client()->Stop(); + PumpLoopUntilIdle(); + EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state()); + + // Restart the GCM without delay. + gcm_client()->Start(GCMClient::IMMEDIATE_START); + PumpLoopUntilIdle(); + EXPECT_EQ(GCMClientImpl::INITIAL_DEVICE_CHECKIN, gcm_client_state()); +} + +TEST_F(GCMClientImplStartAndStopTest, DelayedStartAndStopImmediately) { + // GCMClientImpl should be in INITIALIZED state at first. + EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state()); + + // Delay start the GCM and then stop it immediately. + gcm_client()->Start(GCMClient::DELAYED_START); + gcm_client()->Stop(); + PumpLoopUntilIdle(); + EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state()); +} + +TEST_F(GCMClientImplStartAndStopTest, ImmediateStartAndStopImmediately) { + // GCMClientImpl should be in INITIALIZED state at first. + EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state()); + + // Start the GCM and then stop it immediately. + gcm_client()->Start(GCMClient::IMMEDIATE_START); + gcm_client()->Stop(); + PumpLoopUntilIdle(); + EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state()); +} + +TEST_F(GCMClientImplStartAndStopTest, DelayedStartStopAndRestart) { + // GCMClientImpl should be in INITIALIZED state at first. + EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state()); + + // Delay start the GCM and then stop and restart it immediately. + gcm_client()->Start(GCMClient::DELAYED_START); + gcm_client()->Stop(); + gcm_client()->Start(GCMClient::DELAYED_START); + PumpLoopUntilIdle(); + EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state()); +} + +TEST_F(GCMClientImplStartAndStopTest, ImmediateStartStopAndRestart) { + // GCMClientImpl should be in INITIALIZED state at first. + EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state()); + + // Start the GCM and then stop and restart it immediately. + gcm_client()->Start(GCMClient::IMMEDIATE_START); + gcm_client()->Stop(); + gcm_client()->Start(GCMClient::IMMEDIATE_START); + PumpLoopUntilIdle(); + EXPECT_EQ(GCMClientImpl::INITIAL_DEVICE_CHECKIN, gcm_client_state()); +} + +TEST_F(GCMClientImplStartAndStopTest, ImmediateStartAndThenImmediateStart) { + // GCMClientImpl should be in INITIALIZED state at first. + EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state()); + + // Start the GCM immediately and complete the checkin. + gcm_client()->Start(GCMClient::IMMEDIATE_START); + PumpLoopUntilIdle(); + EXPECT_EQ(GCMClientImpl::INITIAL_DEVICE_CHECKIN, gcm_client_state()); + ASSERT_NO_FATAL_FAILURE(DefaultCompleteCheckin()); + EXPECT_EQ(GCMClientImpl::READY, gcm_client_state()); + + // Stop the GCM. + gcm_client()->Stop(); + PumpLoopUntilIdle(); + EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state()); + + // Start the GCM immediately. GCMClientImpl should be in READY state. + BuildGCMClient(base::TimeDelta()); + InitializeGCMClient(); + gcm_client()->Start(GCMClient::IMMEDIATE_START); + PumpLoopUntilIdle(); + EXPECT_EQ(GCMClientImpl::READY, gcm_client_state()); +} + +TEST_F(GCMClientImplStartAndStopTest, ImmediateStartAndThenDelayStart) { + // GCMClientImpl should be in INITIALIZED state at first. + EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state()); + + // Start the GCM immediately and complete the checkin. + gcm_client()->Start(GCMClient::IMMEDIATE_START); + PumpLoopUntilIdle(); + EXPECT_EQ(GCMClientImpl::INITIAL_DEVICE_CHECKIN, gcm_client_state()); + ASSERT_NO_FATAL_FAILURE(DefaultCompleteCheckin()); + EXPECT_EQ(GCMClientImpl::READY, gcm_client_state()); + + // Stop the GCM. + gcm_client()->Stop(); + PumpLoopUntilIdle(); + EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state()); + + // Delay start the GCM. GCMClientImpl should be in LOADED state. + BuildGCMClient(base::TimeDelta()); + InitializeGCMClient(); + gcm_client()->Start(GCMClient::DELAYED_START); + PumpLoopUntilIdle(); + EXPECT_EQ(GCMClientImpl::LOADED, gcm_client_state()); +} + +TEST_F(GCMClientImplStartAndStopTest, DISABLED_DelayedStartRace) { + // GCMClientImpl should be in INITIALIZED state at first. + EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state()); + + // Delay start the GCM, then start it immediately while it's still loading. + gcm_client()->Start(GCMClient::DELAYED_START); + gcm_client()->Start(GCMClient::IMMEDIATE_START); + PumpLoopUntilIdle(); + EXPECT_EQ(GCMClientImpl::INITIAL_DEVICE_CHECKIN, gcm_client_state()); + ASSERT_NO_FATAL_FAILURE(DefaultCompleteCheckin()); + EXPECT_EQ(GCMClientImpl::READY, gcm_client_state()); +} + +TEST_F(GCMClientImplStartAndStopTest, DelayedStart) { + // GCMClientImpl should be in INITIALIZED state at first. + EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state()); + + // Delay start the GCM. The store will not be loaded and GCMClientImpl should + // still be in INITIALIZED state. + gcm_client()->Start(GCMClient::DELAYED_START); + PumpLoopUntilIdle(); + EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state()); + + // Start the GCM immediately and complete the checkin. + gcm_client()->Start(GCMClient::IMMEDIATE_START); + PumpLoopUntilIdle(); + EXPECT_EQ(GCMClientImpl::INITIAL_DEVICE_CHECKIN, gcm_client_state()); + ASSERT_NO_FATAL_FAILURE(DefaultCompleteCheckin()); + EXPECT_EQ(GCMClientImpl::READY, gcm_client_state()); + + // Registration. + std::vector senders; + senders.push_back("sender"); + Register(kExtensionAppId, senders); + ASSERT_NO_FATAL_FAILURE(CompleteRegistration("reg_id")); + EXPECT_EQ(GCMClientImpl::READY, gcm_client_state()); + + // Stop the GCM. + gcm_client()->Stop(); + PumpLoopUntilIdle(); + EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state()); + + // Delay start the GCM. GCM is indeed started without delay because the + // registration record has been found. + BuildGCMClient(base::TimeDelta()); + InitializeGCMClient(); + gcm_client()->Start(GCMClient::DELAYED_START); + PumpLoopUntilIdle(); + EXPECT_EQ(GCMClientImpl::READY, gcm_client_state()); +} + +// Test for known account mappings and last token fetching time being passed +// to OnGCMReady. +TEST_F(GCMClientImplStartAndStopTest, OnGCMReadyAccountsAndTokenFetchingTime) { + // Start the GCM and wait until it is ready. + gcm_client()->Start(GCMClient::IMMEDIATE_START); + PumpLoopUntilIdle(); + ASSERT_NO_FATAL_FAILURE(DefaultCompleteCheckin()); + + base::Time expected_time = base::Time::Now(); + gcm_client()->SetLastTokenFetchTime(expected_time); + AccountMapping expected_mapping; + expected_mapping.account_id = CoreAccountId("accId"); + expected_mapping.email = "email@gmail.com"; + expected_mapping.status = AccountMapping::MAPPED; + expected_mapping.status_change_timestamp = expected_time; + gcm_client()->UpdateAccountMapping(expected_mapping); + PumpLoopUntilIdle(); + + // Stop the GCM. + gcm_client()->Stop(); + PumpLoopUntilIdle(); + + // Restart the GCM. + gcm_client()->Start(GCMClient::IMMEDIATE_START); + PumpLoopUntilIdle(); + + EXPECT_EQ(LOADING_COMPLETED, last_event()); + EXPECT_EQ(expected_time, last_token_fetch_time()); + ASSERT_EQ(1UL, last_account_mappings().size()); + const AccountMapping& actual_mapping = last_account_mappings()[0]; + EXPECT_EQ(expected_mapping.account_id, actual_mapping.account_id); + EXPECT_EQ(expected_mapping.email, actual_mapping.email); + EXPECT_EQ(expected_mapping.status, actual_mapping.status); + EXPECT_EQ(expected_mapping.status_change_timestamp, + actual_mapping.status_change_timestamp); +} + + +class GCMClientInstanceIDTest : public GCMClientImplTest { + public: + GCMClientInstanceIDTest(); + ~GCMClientInstanceIDTest() override; + + void AddInstanceID(const std::string& app_id, + const std::string& instance_id); + void RemoveInstanceID(const std::string& app_id); + void GetToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope); + void DeleteToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope); + void CompleteDeleteToken(); + bool ExistsToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope) const; +}; + +GCMClientInstanceIDTest::GCMClientInstanceIDTest() { +} + +GCMClientInstanceIDTest::~GCMClientInstanceIDTest() { +} + +void GCMClientInstanceIDTest::AddInstanceID(const std::string& app_id, + const std::string& instance_id) { + gcm_client()->AddInstanceIDData(app_id, instance_id, "123"); +} + +void GCMClientInstanceIDTest::RemoveInstanceID(const std::string& app_id) { + gcm_client()->RemoveInstanceIDData(app_id); +} + +void GCMClientInstanceIDTest::GetToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope) { + auto instance_id_info = base::MakeRefCounted(); + instance_id_info->app_id = app_id; + instance_id_info->authorized_entity = authorized_entity; + instance_id_info->scope = scope; + gcm_client()->Register(std::move(instance_id_info)); +} + +void GCMClientInstanceIDTest::DeleteToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope) { + auto instance_id_info = base::MakeRefCounted(); + instance_id_info->app_id = app_id; + instance_id_info->authorized_entity = authorized_entity; + instance_id_info->scope = scope; + gcm_client()->Unregister(std::move(instance_id_info)); +} + +void GCMClientInstanceIDTest::CompleteDeleteToken() { + std::string response(kDeleteTokenResponse); + + EXPECT_TRUE(url_loader_factory()->SimulateResponseForPendingRequest( + GURL(kRegisterUrl), network::URLLoaderCompletionStatus(net::OK), + network::CreateURLResponseHead(net::HTTP_OK), response)); + + // Give a chance for GCMStoreImpl::Backend to finish persisting data. + PumpLoopUntilIdle(); +} + +bool GCMClientInstanceIDTest::ExistsToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope) const { + auto instance_id_info = base::MakeRefCounted(); + instance_id_info->app_id = app_id; + instance_id_info->authorized_entity = authorized_entity; + instance_id_info->scope = scope; + return gcm_client()->registrations_.count(std::move(instance_id_info)) > 0; +} + +TEST_F(GCMClientInstanceIDTest, GetToken) { + AddInstanceID(kExtensionAppId, kInstanceID); + + // Get a token. + EXPECT_FALSE(ExistsToken(kExtensionAppId, kSender, kScope)); + GetToken(kExtensionAppId, kSender, kScope); + ASSERT_NO_FATAL_FAILURE(CompleteRegistration("token1")); + + EXPECT_EQ(REGISTRATION_COMPLETED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ("token1", last_registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, last_result()); + EXPECT_TRUE(ExistsToken(kExtensionAppId, kSender, kScope)); + + // Get another token. + EXPECT_FALSE(ExistsToken(kExtensionAppId, kSender2, kScope)); + GetToken(kExtensionAppId, kSender2, kScope); + ASSERT_NO_FATAL_FAILURE(CompleteRegistration("token2")); + + EXPECT_EQ(REGISTRATION_COMPLETED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ("token2", last_registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, last_result()); + EXPECT_TRUE(ExistsToken(kExtensionAppId, kSender2, kScope)); + // The 1st token still exists. + EXPECT_TRUE(ExistsToken(kExtensionAppId, kSender, kScope)); +} + +// Most tests in this file use kExtensionAppId which is special-cased by +// InstanceIDUsesSubtypeForAppId in gcm_client_impl.cc. This test uses +// kSubtypeAppId to cover the alternate case. +TEST_F(GCMClientInstanceIDTest, GetTokenWithSubtype) { + ASSERT_EQ(GCMClientImpl::READY, gcm_client_state()); + + AddInstanceID(kSubtypeAppId, kInstanceID); + + EXPECT_FALSE(ExistsToken(kSubtypeAppId, kSender, kScope)); + + // Get a token. + GetToken(kSubtypeAppId, kSender, kScope); + ASSERT_NO_FATAL_FAILURE(CompleteRegistration("token1")); + EXPECT_EQ(REGISTRATION_COMPLETED, last_event()); + EXPECT_EQ(kSubtypeAppId, last_app_id()); + EXPECT_EQ("token1", last_registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, last_result()); + EXPECT_TRUE(ExistsToken(kSubtypeAppId, kSender, kScope)); + + // Delete the token. + DeleteToken(kSubtypeAppId, kSender, kScope); + ASSERT_NO_FATAL_FAILURE(CompleteDeleteToken()); + EXPECT_FALSE(ExistsToken(kSubtypeAppId, kSender, kScope)); +} + +TEST_F(GCMClientInstanceIDTest, DeleteInvalidToken) { + AddInstanceID(kExtensionAppId, kInstanceID); + + // Delete an invalid token. + DeleteToken(kExtensionAppId, "Foo@#$", kScope); + PumpLoopUntilIdle(); + + EXPECT_EQ(UNREGISTRATION_COMPLETED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ(GCMClient::INVALID_PARAMETER, last_result()); + + reset_last_event(); + + // Delete a non-existing token. + DeleteToken(kExtensionAppId, kSender, kScope); + PumpLoopUntilIdle(); + + EXPECT_EQ(UNREGISTRATION_COMPLETED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ(GCMClient::INVALID_PARAMETER, last_result()); +} + +TEST_F(GCMClientInstanceIDTest, DeleteSingleToken) { + AddInstanceID(kExtensionAppId, kInstanceID); + + // Get a token. + EXPECT_FALSE(ExistsToken(kExtensionAppId, kSender, kScope)); + GetToken(kExtensionAppId, kSender, kScope); + ASSERT_NO_FATAL_FAILURE(CompleteRegistration("token1")); + + EXPECT_EQ(REGISTRATION_COMPLETED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ("token1", last_registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, last_result()); + EXPECT_TRUE(ExistsToken(kExtensionAppId, kSender, kScope)); + + reset_last_event(); + + // Get another token. + EXPECT_FALSE(ExistsToken(kExtensionAppId, kSender2, kScope)); + GetToken(kExtensionAppId, kSender2, kScope); + ASSERT_NO_FATAL_FAILURE(CompleteRegistration("token2")); + + EXPECT_EQ(REGISTRATION_COMPLETED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ("token2", last_registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, last_result()); + EXPECT_TRUE(ExistsToken(kExtensionAppId, kSender2, kScope)); + // The 1st token still exists. + EXPECT_TRUE(ExistsToken(kExtensionAppId, kSender, kScope)); + + reset_last_event(); + + // Delete the 2nd token. + DeleteToken(kExtensionAppId, kSender2, kScope); + ASSERT_NO_FATAL_FAILURE(CompleteDeleteToken()); + + EXPECT_EQ(UNREGISTRATION_COMPLETED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ(GCMClient::SUCCESS, last_result()); + // The 2nd token is gone while the 1st token still exists. + EXPECT_TRUE(ExistsToken(kExtensionAppId, kSender, kScope)); + EXPECT_FALSE(ExistsToken(kExtensionAppId, kSender2, kScope)); + + reset_last_event(); + + // Delete the 1st token. + DeleteToken(kExtensionAppId, kSender, kScope); + ASSERT_NO_FATAL_FAILURE(CompleteDeleteToken()); + + EXPECT_EQ(UNREGISTRATION_COMPLETED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ(GCMClient::SUCCESS, last_result()); + // Both tokens are gone now. + EXPECT_FALSE(ExistsToken(kExtensionAppId, kSender, kScope)); + EXPECT_FALSE(ExistsToken(kExtensionAppId, kSender, kScope)); + + reset_last_event(); + + // Trying to delete the token again will get an error. + DeleteToken(kExtensionAppId, kSender, kScope); + PumpLoopUntilIdle(); + + EXPECT_EQ(UNREGISTRATION_COMPLETED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ(GCMClient::INVALID_PARAMETER, last_result()); +} + +TEST_F(GCMClientInstanceIDTest, DISABLED_DeleteAllTokens) { + AddInstanceID(kExtensionAppId, kInstanceID); + + // Get a token. + EXPECT_FALSE(ExistsToken(kExtensionAppId, kSender, kScope)); + GetToken(kExtensionAppId, kSender, kScope); + ASSERT_NO_FATAL_FAILURE(CompleteRegistration("token1")); + + EXPECT_EQ(REGISTRATION_COMPLETED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ("token1", last_registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, last_result()); + EXPECT_TRUE(ExistsToken(kExtensionAppId, kSender, kScope)); + + reset_last_event(); + + // Get another token. + EXPECT_FALSE(ExistsToken(kExtensionAppId, kSender2, kScope)); + GetToken(kExtensionAppId, kSender2, kScope); + ASSERT_NO_FATAL_FAILURE(CompleteRegistration("token2")); + + EXPECT_EQ(REGISTRATION_COMPLETED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ("token2", last_registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, last_result()); + EXPECT_TRUE(ExistsToken(kExtensionAppId, kSender2, kScope)); + // The 1st token still exists. + EXPECT_TRUE(ExistsToken(kExtensionAppId, kSender, kScope)); + + reset_last_event(); + + // Delete all tokens. + DeleteToken(kExtensionAppId, "*", "*"); + ASSERT_NO_FATAL_FAILURE(CompleteDeleteToken()); + + EXPECT_EQ(UNREGISTRATION_COMPLETED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ(GCMClient::SUCCESS, last_result()); + // All tokens are gone now. + EXPECT_FALSE(ExistsToken(kExtensionAppId, kSender, kScope)); + EXPECT_FALSE(ExistsToken(kExtensionAppId, kSender, kScope)); +} + +TEST_F(GCMClientInstanceIDTest, DeleteAllTokensBeforeGetAnyToken) { + AddInstanceID(kExtensionAppId, kInstanceID); + + // Delete all tokens without getting a token first. + DeleteToken(kExtensionAppId, "*", "*"); + // No need to call CompleteDeleteToken since unregistration request should + // not be triggered. + PumpLoopUntilIdle(); + + EXPECT_EQ(UNREGISTRATION_COMPLETED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ(GCMClient::SUCCESS, last_result()); +} + +TEST_F(GCMClientInstanceIDTest, DispatchDownstreamMessageWithoutSubtype) { + AddInstanceID(kExtensionAppId, kInstanceID); + GetToken(kExtensionAppId, kSender, kScope); + ASSERT_NO_FATAL_FAILURE(CompleteRegistration("token1")); + + std::map expected_data; + + MCSMessage message(BuildDownstreamMessage( + kSender, kExtensionAppId, std::string() /* subtype */, expected_data, + std::string() /* raw_data */)); + EXPECT_TRUE(message.IsValid()); + ReceiveMessageFromMCS(message); + + EXPECT_EQ(MESSAGE_RECEIVED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ(expected_data.size(), last_message().data.size()); + EXPECT_EQ(expected_data, last_message().data); + EXPECT_EQ(kSender, last_message().sender_id); +} + +TEST_F(GCMClientInstanceIDTest, DispatchDownstreamMessageWithSubtype) { + AddInstanceID(kSubtypeAppId, kInstanceID); + GetToken(kSubtypeAppId, kSender, kScope); + ASSERT_NO_FATAL_FAILURE(CompleteRegistration("token1")); + + std::map expected_data; + + MCSMessage message(BuildDownstreamMessage( + kSender, kProductCategoryForSubtypes, kSubtypeAppId /* subtype */, + expected_data, std::string() /* raw_data */)); + EXPECT_TRUE(message.IsValid()); + ReceiveMessageFromMCS(message); + + EXPECT_EQ(MESSAGE_RECEIVED, last_event()); + EXPECT_EQ(kSubtypeAppId, last_app_id()); + EXPECT_EQ(expected_data.size(), last_message().data.size()); + EXPECT_EQ(expected_data, last_message().data); + EXPECT_EQ(kSender, last_message().sender_id); +} + +TEST_F(GCMClientInstanceIDTest, DispatchDownstreamMessageWithFakeSubtype) { + // Victim non-extension registration. + AddInstanceID(kSubtypeAppId, "iid_1"); + GetToken(kSubtypeAppId, kSender, kScope); + ASSERT_NO_FATAL_FAILURE(CompleteRegistration("token1")); + + // Malicious extension registration. + AddInstanceID(kExtensionAppId, "iid_2"); + GetToken(kExtensionAppId, kSender, kScope); + ASSERT_NO_FATAL_FAILURE(CompleteRegistration("token2")); + + std::map expected_data; + + // Message for kExtensionAppId should be delivered to the extension rather + // than the victim app, despite the malicious subtype property attempting to + // impersonate victim app. + MCSMessage message(BuildDownstreamMessage( + kSender, kExtensionAppId /* category */, kSubtypeAppId /* subtype */, + expected_data, std::string() /* raw_data */)); + EXPECT_TRUE(message.IsValid()); + ReceiveMessageFromMCS(message); + + EXPECT_EQ(MESSAGE_RECEIVED, last_event()); + EXPECT_EQ(kExtensionAppId, last_app_id()); + EXPECT_EQ(expected_data.size(), last_message().data.size()); + EXPECT_EQ(expected_data, last_message().data); + EXPECT_EQ(kSender, last_message().sender_id); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_connection_observer.cc b/chromium/components/gcm_driver/gcm_connection_observer.cc new file mode 100644 index 00000000000..5cea0b46b2a --- /dev/null +++ b/chromium/components/gcm_driver/gcm_connection_observer.cc @@ -0,0 +1,18 @@ +// 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 "components/gcm_driver/gcm_connection_observer.h" + +namespace gcm { + +GCMConnectionObserver::GCMConnectionObserver() {} +GCMConnectionObserver::~GCMConnectionObserver() {} + +void GCMConnectionObserver::OnConnected(const net::IPEndPoint& ip_endpoint) { +} + +void GCMConnectionObserver::OnDisconnected() { +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_connection_observer.h b/chromium/components/gcm_driver/gcm_connection_observer.h new file mode 100644 index 00000000000..15a192efcdc --- /dev/null +++ b/chromium/components/gcm_driver/gcm_connection_observer.h @@ -0,0 +1,34 @@ +// 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 COMPONENTS_GCM_DRIVER_GCM_CONNECTION_OBSERVER_H_ +#define COMPONENTS_GCM_DRIVER_GCM_CONNECTION_OBSERVER_H_ + + +namespace net { +class IPEndPoint; +} + +namespace gcm { + +// Interface for objects observing GCM connection events. +class GCMConnectionObserver { + public: + GCMConnectionObserver(); + virtual ~GCMConnectionObserver(); + + // Called when a new connection is established and a successful handshake + // has been performed. Note that |ip_endpoint| is only set if available for + // the current platform. + // Default implementation does nothing. + virtual void OnConnected(const net::IPEndPoint& ip_endpoint); + + // Called when the connection is interrupted. + // Default implementation does nothing. + virtual void OnDisconnected(); +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_GCM_CONNECTION_OBSERVER_H_ diff --git a/chromium/components/gcm_driver/gcm_delayed_task_controller.cc b/chromium/components/gcm_driver/gcm_delayed_task_controller.cc new file mode 100644 index 00000000000..15006cf52c4 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_delayed_task_controller.cc @@ -0,0 +1,39 @@ +// 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 "components/gcm_driver/gcm_delayed_task_controller.h" + +#include + +#include "base/check.h" + +namespace gcm { + +GCMDelayedTaskController::GCMDelayedTaskController() : ready_(false) { +} + +GCMDelayedTaskController::~GCMDelayedTaskController() = default; + +void GCMDelayedTaskController::AddTask(base::OnceClosure task) { + delayed_tasks_.push_back(std::move(task)); +} + +void GCMDelayedTaskController::SetReady() { + ready_ = true; + RunTasks(); +} + +bool GCMDelayedTaskController::CanRunTaskWithoutDelay() const { + return ready_; +} + +void GCMDelayedTaskController::RunTasks() { + DCHECK(ready_); + + for (size_t i = 0; i < delayed_tasks_.size(); ++i) + std::move(delayed_tasks_[i]).Run(); + delayed_tasks_.clear(); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_delayed_task_controller.h b/chromium/components/gcm_driver/gcm_delayed_task_controller.h new file mode 100644 index 00000000000..bf1e15fa86e --- /dev/null +++ b/chromium/components/gcm_driver/gcm_delayed_task_controller.h @@ -0,0 +1,44 @@ +// 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 COMPONENTS_GCM_DRIVER_GCM_DELAYED_TASK_CONTROLLER_H_ +#define COMPONENTS_GCM_DRIVER_GCM_DELAYED_TASK_CONTROLLER_H_ + +#include + +#include "base/callback.h" + +namespace gcm { + +// Helper class to save tasks to run until we're ready to execute them. +class GCMDelayedTaskController { + public: + GCMDelayedTaskController(); + + GCMDelayedTaskController(const GCMDelayedTaskController&) = delete; + GCMDelayedTaskController& operator=(const GCMDelayedTaskController&) = delete; + + ~GCMDelayedTaskController(); + + // Adds a task that will be invoked once we're ready. + void AddTask(base::OnceClosure task); + + // Sets ready status, which will release all of the pending tasks. + void SetReady(); + + // Returns true if it is ready to perform tasks. + bool CanRunTaskWithoutDelay() const; + + private: + void RunTasks(); + + // Flag that indicates that controlled component is ready. + bool ready_; + + std::vector delayed_tasks_; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_GCM_DELAYED_TASK_CONTROLLER_H_ diff --git a/chromium/components/gcm_driver/gcm_delayed_task_controller_unittest.cc b/chromium/components/gcm_driver/gcm_delayed_task_controller_unittest.cc new file mode 100644 index 00000000000..e311d96eecc --- /dev/null +++ b/chromium/components/gcm_driver/gcm_delayed_task_controller_unittest.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 "components/gcm_driver/gcm_delayed_task_controller.h" + +#include + +#include "base/bind.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { + +class GCMDelayedTaskControllerTest : public testing::Test { + public: + GCMDelayedTaskControllerTest(); + ~GCMDelayedTaskControllerTest() override; + + void TestTask(); + + GCMDelayedTaskController* controller() { return controller_.get(); } + + int number_of_triggered_tasks() const { return number_of_triggered_tasks_; } + + private: + std::unique_ptr controller_; + int number_of_triggered_tasks_; +}; + +GCMDelayedTaskControllerTest::GCMDelayedTaskControllerTest() + : controller_(new GCMDelayedTaskController), number_of_triggered_tasks_(0) { +} + +GCMDelayedTaskControllerTest::~GCMDelayedTaskControllerTest() { +} + +void GCMDelayedTaskControllerTest::TestTask() { + ++number_of_triggered_tasks_; +} + +// Tests that a newly created controller forced tasks to be delayed, while +// calling SetReady allows tasks to execute. +TEST_F(GCMDelayedTaskControllerTest, SetReadyWithNoTasks) { + EXPECT_FALSE(controller()->CanRunTaskWithoutDelay()); + EXPECT_EQ(0, number_of_triggered_tasks()); + + controller()->SetReady(); + EXPECT_TRUE(controller()->CanRunTaskWithoutDelay()); + EXPECT_EQ(0, number_of_triggered_tasks()); +} + +// Tests that tasks are triggered when controlles is set to ready. +TEST_F(GCMDelayedTaskControllerTest, PendingTasksTriggeredWhenSetReady) { + controller()->AddTask(base::BindOnce(&GCMDelayedTaskControllerTest::TestTask, + base::Unretained(this))); + controller()->AddTask(base::BindOnce(&GCMDelayedTaskControllerTest::TestTask, + base::Unretained(this))); + + controller()->SetReady(); + EXPECT_EQ(2, number_of_triggered_tasks()); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_desktop_utils.cc b/chromium/components/gcm_driver/gcm_desktop_utils.cc new file mode 100644 index 00000000000..b24d10ec526 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_desktop_utils.cc @@ -0,0 +1,101 @@ +// 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 "components/gcm_driver/gcm_desktop_utils.h" + +#include + +#include "base/command_line.h" +#include "base/task/sequenced_task_runner.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "components/gcm_driver/gcm_client_factory.h" +#include "components/gcm_driver/gcm_driver.h" +#include "components/gcm_driver/gcm_driver_desktop.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#include "url/gurl.h" + +namespace gcm { + +namespace { + +GCMClient::ChromePlatform GetPlatform() { +#if defined(OS_WIN) + return GCMClient::PLATFORM_WIN; +#elif defined(OS_APPLE) + return GCMClient::PLATFORM_MAC; +#elif defined(OS_IOS) + return GCMClient::PLATFORM_IOS; +#elif defined(OS_ANDROID) + return GCMClient::PLATFORM_ANDROID; +#elif BUILDFLAG(IS_CHROMEOS_ASH) + return GCMClient::PLATFORM_CROS; +#elif defined(OS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS) + return GCMClient::PLATFORM_LINUX; +#else + // For all other platforms, return as LINUX. + return GCMClient::PLATFORM_LINUX; +#endif +} + +GCMClient::ChromeChannel GetChannel(version_info::Channel channel) { + switch (channel) { + case version_info::Channel::UNKNOWN: + return GCMClient::CHANNEL_UNKNOWN; + case version_info::Channel::CANARY: + return GCMClient::CHANNEL_CANARY; + case version_info::Channel::DEV: + return GCMClient::CHANNEL_DEV; + case version_info::Channel::BETA: + return GCMClient::CHANNEL_BETA; + case version_info::Channel::STABLE: + return GCMClient::CHANNEL_STABLE; + } + NOTREACHED(); + return GCMClient::CHANNEL_UNKNOWN; +} + +std::string GetVersion() { + return version_info::GetVersionNumber(); +} + +GCMClient::ChromeBuildInfo GetChromeBuildInfo( + version_info::Channel channel, + const std::string& product_category_for_subtypes) { + GCMClient::ChromeBuildInfo chrome_build_info; + chrome_build_info.platform = GetPlatform(); + chrome_build_info.channel = GetChannel(channel); + chrome_build_info.version = GetVersion(); + chrome_build_info.product_category_for_subtypes = + product_category_for_subtypes; + return chrome_build_info; +} + +} // namespace + +std::unique_ptr CreateGCMDriverDesktop( + std::unique_ptr gcm_client_factory, + PrefService* prefs, + const base::FilePath& store_path, + bool remove_account_mappings_with_email_key, + base::RepeatingCallback)> + get_socket_factory_callback, + scoped_refptr url_loader_factory, + network::NetworkConnectionTracker* network_connection_tracker, + version_info::Channel channel, + const std::string& product_category_for_subtypes, + const scoped_refptr& ui_task_runner, + const scoped_refptr& io_task_runner, + const scoped_refptr& blocking_task_runner) { + return std::unique_ptr(new GCMDriverDesktop( + std::move(gcm_client_factory), + GetChromeBuildInfo(channel, product_category_for_subtypes), prefs, + store_path, remove_account_mappings_with_email_key, + get_socket_factory_callback, std::move(url_loader_factory), + network_connection_tracker, ui_task_runner, io_task_runner, + blocking_task_runner)); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_desktop_utils.h b/chromium/components/gcm_driver/gcm_desktop_utils.h new file mode 100644 index 00000000000..a2ed4780c53 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_desktop_utils.h @@ -0,0 +1,49 @@ +// 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 COMPONENTS_GCM_DRIVER_GCM_DESKTOP_UTILS_H_ +#define COMPONENTS_GCM_DRIVER_GCM_DESKTOP_UTILS_H_ + +#include + +#include "base/memory/ref_counted.h" +#include "base/task/sequenced_task_runner.h" +#include "components/version_info/version_info.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "services/network/public/mojom/proxy_resolving_socket.mojom-forward.h" + +class PrefService; +namespace base { +class FilePath; +} + +namespace network { +class NetworkConnectionTracker; +class SharedURLLoaderFactory; +} + +namespace gcm { + +class GCMDriver; +class GCMClientFactory; + +std::unique_ptr CreateGCMDriverDesktop( + std::unique_ptr gcm_client_factory, + PrefService* prefs, + const base::FilePath& store_path, + bool remove_account_mappings_with_email_key, + base::RepeatingCallback)> + get_socket_factory_callback, + scoped_refptr url_loader_factory, + network::NetworkConnectionTracker* network_connection_tracker, + version_info::Channel channel, + const std::string& product_category_for_subtypes, + const scoped_refptr& ui_task_runner, + const scoped_refptr& io_task_runner, + const scoped_refptr& blocking_task_runner); + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_GCM_DESKTOP_UTILS_H_ diff --git a/chromium/components/gcm_driver/gcm_driver.cc b/chromium/components/gcm_driver/gcm_driver.cc new file mode 100644 index 00000000000..35960120e38 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_driver.cc @@ -0,0 +1,373 @@ +// 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 "components/gcm_driver/gcm_driver.h" + +#include + +#include + +#include "base/bind.h" +#include "base/files/file_path.h" +#include "base/logging.h" +#include "base/metrics/histogram_macros.h" +#include "components/gcm_driver/crypto/gcm_decryption_result.h" +#include "components/gcm_driver/crypto/gcm_encryption_result.h" +#include "components/gcm_driver/gcm_app_handler.h" + +namespace gcm { + +InstanceIDHandler::InstanceIDHandler() = default; + +InstanceIDHandler::~InstanceIDHandler() = default; + +void InstanceIDHandler::DeleteAllTokensForApp(const std::string& app_id, + DeleteTokenCallback callback) { + DeleteToken(app_id, "*", "*", std::move(callback)); +} + +GCMDriver::GCMDriver( + const base::FilePath& store_path, + const scoped_refptr& blocking_task_runner) { + // The |blocking_task_runner| can be nullptr for tests that do not need the + // encryption capabilities of the GCMDriver class. + if (blocking_task_runner) + encryption_provider_.Init(store_path, blocking_task_runner); +} + +GCMDriver::~GCMDriver() = default; + +void GCMDriver::Register(const std::string& app_id, + const std::vector& sender_ids, + RegisterCallback callback) { + DCHECK(!app_id.empty()); + DCHECK(!sender_ids.empty() && sender_ids.size() <= kMaxSenders); + DCHECK(!callback.is_null()); + + GCMClient::Result result = EnsureStarted(GCMClient::IMMEDIATE_START); + if (result != GCMClient::SUCCESS) { + std::move(callback).Run(std::string(), result); + return; + } + + // If previous register operation is still in progress, bail out. + if (register_callbacks_.find(app_id) != register_callbacks_.end()) { + std::move(callback).Run(std::string(), GCMClient::ASYNC_OPERATION_PENDING); + return; + } + + // Normalize the sender IDs by making them sorted. + std::vector normalized_sender_ids = sender_ids; + std::sort(normalized_sender_ids.begin(), normalized_sender_ids.end()); + + register_callbacks_[app_id] = std::move(callback); + + // If previous unregister operation is still in progress, wait until it + // finishes. We don't want to throw ASYNC_OPERATION_PENDING when the user + // uninstalls an app (ungistering) and then reinstalls the app again + // (registering). + auto unregister_iter = unregister_callbacks_.find(app_id); + if (unregister_iter != unregister_callbacks_.end()) { + // Replace the original unregister callback with an intermediate callback + // that will invoke the original unregister callback and trigger the pending + // registration after the unregistration finishes. + // Note that some parameters to RegisterAfterUnregister are specified here + // when the callback is created (base::Bind supports the partial binding + // of parameters). + unregister_iter->second = base::BindOnce( + &GCMDriver::RegisterAfterUnregister, weak_ptr_factory_.GetWeakPtr(), + app_id, normalized_sender_ids, std::move(unregister_iter->second)); + return; + } + + RegisterImpl(app_id, normalized_sender_ids); +} + +void GCMDriver::Unregister(const std::string& app_id, + UnregisterCallback callback) { + UnregisterInternal(app_id, nullptr /* sender_id */, std::move(callback)); +} + +void GCMDriver::UnregisterWithSenderId(const std::string& app_id, + const std::string& sender_id, + UnregisterCallback callback) { + DCHECK(!sender_id.empty()); + UnregisterInternal(app_id, &sender_id, std::move(callback)); +} + +void GCMDriver::UnregisterInternal(const std::string& app_id, + const std::string* sender_id, + UnregisterCallback callback) { + DCHECK(!app_id.empty()); + DCHECK(!callback.is_null()); + + GCMClient::Result result = EnsureStarted(GCMClient::IMMEDIATE_START); + if (result != GCMClient::SUCCESS) { + std::move(callback).Run(result); + return; + } + + // If previous un/register operation is still in progress, bail out. + if (register_callbacks_.find(app_id) != register_callbacks_.end() || + unregister_callbacks_.find(app_id) != unregister_callbacks_.end()) { + std::move(callback).Run(GCMClient::ASYNC_OPERATION_PENDING); + return; + } + + unregister_callbacks_[app_id] = std::move(callback); + + if (sender_id) + UnregisterWithSenderIdImpl(app_id, *sender_id); + else + UnregisterImpl(app_id); +} + +void GCMDriver::Send(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message, + SendCallback callback) { + DCHECK(!app_id.empty()); + DCHECK(!receiver_id.empty()); + DCHECK(!callback.is_null()); + + GCMClient::Result result = EnsureStarted(GCMClient::IMMEDIATE_START); + if (result != GCMClient::SUCCESS) { + std::move(callback).Run(std::string(), result); + return; + } + + // If the message with send ID is still in progress, bail out. + std::pair key(app_id, message.id); + if (send_callbacks_.find(key) != send_callbacks_.end()) { + std::move(callback).Run(message.id, GCMClient::INVALID_PARAMETER); + return; + } + + send_callbacks_[key] = std::move(callback); + + SendImpl(app_id, receiver_id, message); +} + +void GCMDriver::GetEncryptionInfo(const std::string& app_id, + GetEncryptionInfoCallback callback) { + encryption_provider_.GetEncryptionInfo(app_id, "" /* authorized_entity */, + std::move(callback)); +} + +void GCMDriver::UnregisterWithSenderIdImpl(const std::string& app_id, + const std::string& sender_id) { + NOTREACHED(); +} + +void GCMDriver::RegisterFinished(const std::string& app_id, + const std::string& registration_id, + GCMClient::Result result) { + auto callback_iter = register_callbacks_.find(app_id); + if (callback_iter == register_callbacks_.end()) { + // The callback could have been removed when the app is uninstalled. + return; + } + + RegisterCallback callback = std::move(callback_iter->second); + register_callbacks_.erase(callback_iter); + std::move(callback).Run(registration_id, result); +} + +void GCMDriver::RemoveEncryptionInfoAfterUnregister(const std::string& app_id, + GCMClient::Result result) { + encryption_provider_.RemoveEncryptionInfo( + app_id, "" /* authorized_entity */, + base::BindOnce(&GCMDriver::UnregisterFinished, + weak_ptr_factory_.GetWeakPtr(), app_id, result)); +} + +void GCMDriver::UnregisterFinished(const std::string& app_id, + GCMClient::Result result) { + auto callback_iter = unregister_callbacks_.find(app_id); + if (callback_iter == unregister_callbacks_.end()) + return; + + UnregisterCallback callback = std::move(callback_iter->second); + unregister_callbacks_.erase(callback_iter); + std::move(callback).Run(result); +} + +void GCMDriver::SendFinished(const std::string& app_id, + const std::string& message_id, + GCMClient::Result result) { + auto callback_iter = send_callbacks_.find( + std::pair(app_id, message_id)); + if (callback_iter == send_callbacks_.end()) { + // The callback could have been removed when the app is uninstalled. + return; + } + + SendCallback callback = std::move(callback_iter->second); + send_callbacks_.erase(callback_iter); + std::move(callback).Run(message_id, result); +} + +void GCMDriver::Shutdown() { + for (GCMAppHandlerMap::const_iterator iter = app_handlers_.begin(); + iter != app_handlers_.end(); ++iter) { + DVLOG(1) << "Calling ShutdownHandler for: " << iter->first; + iter->second->ShutdownHandler(); + } + app_handlers_.clear(); +} + +void GCMDriver::AddAppHandler(const std::string& app_id, + GCMAppHandler* handler) { + DCHECK(!app_id.empty()); + DCHECK(handler); + DCHECK_EQ(app_handlers_.count(app_id), 0u); + app_handlers_[app_id] = handler; + DVLOG(1) << "App handler added for: " << app_id; +} + +void GCMDriver::RemoveAppHandler(const std::string& app_id) { + DCHECK(!app_id.empty()); + app_handlers_.erase(app_id); + DVLOG(1) << "App handler removed for: " << app_id; +} + +GCMAppHandler* GCMDriver::GetAppHandler(const std::string& app_id) { + // Look for exact match. + GCMAppHandlerMap::const_iterator iter = app_handlers_.find(app_id); + if (iter != app_handlers_.end()) + return iter->second; + + // Ask the handlers whether they know how to handle it. + for (iter = app_handlers_.begin(); iter != app_handlers_.end(); ++iter) { + if (iter->second->CanHandle(app_id)) + return iter->second; + } + + return nullptr; +} + +GCMEncryptionProvider* GCMDriver::GetEncryptionProviderInternal() { + return &encryption_provider_; +} + +bool GCMDriver::HasRegisterCallback(const std::string& app_id) { + return register_callbacks_.find(app_id) != register_callbacks_.end(); +} + +void GCMDriver::ClearCallbacks() { + register_callbacks_.clear(); + unregister_callbacks_.clear(); + send_callbacks_.clear(); +} + +void GCMDriver::DispatchMessage(const std::string& app_id, + const IncomingMessage& message) { + encryption_provider_.DecryptMessage( + app_id, message, + base::BindOnce(&GCMDriver::DispatchMessageInternal, + weak_ptr_factory_.GetWeakPtr(), app_id)); +} + +void GCMDriver::DispatchMessageInternal(const std::string& app_id, + GCMDecryptionResult result, + IncomingMessage message) { + UMA_HISTOGRAM_ENUMERATION("GCM.Crypto.DecryptMessageResult", result, + GCMDecryptionResult::ENUM_SIZE); + + switch (result) { + case GCMDecryptionResult::UNENCRYPTED: + case GCMDecryptionResult::DECRYPTED_DRAFT_03: + case GCMDecryptionResult::DECRYPTED_DRAFT_08: { + GCMAppHandler* handler = GetAppHandler(app_id); + UMA_HISTOGRAM_BOOLEAN("GCM.DeliveredToAppHandler", !!handler); + + if (handler) + handler->OnMessage(app_id, message); + + // TODO(peter/harkness): Surface unavailable app handlers on + // chrome://gcm-internals and send a delivery receipt. + return; + } + case GCMDecryptionResult::INVALID_ENCRYPTION_HEADER: + case GCMDecryptionResult::INVALID_CRYPTO_KEY_HEADER: + case GCMDecryptionResult::NO_KEYS: + case GCMDecryptionResult::INVALID_SHARED_SECRET: + case GCMDecryptionResult::INVALID_PAYLOAD: + case GCMDecryptionResult::INVALID_BINARY_HEADER_PAYLOAD_LENGTH: + case GCMDecryptionResult::INVALID_BINARY_HEADER_RECORD_SIZE: + case GCMDecryptionResult::INVALID_BINARY_HEADER_PUBLIC_KEY_LENGTH: + case GCMDecryptionResult::INVALID_BINARY_HEADER_PUBLIC_KEY_FORMAT: { + RecordDecryptionFailure(app_id, result); + GCMAppHandler* handler = GetAppHandler(app_id); + if (handler) { + handler->OnMessageDecryptionFailed( + app_id, message.message_id, + ToGCMDecryptionResultDetailsString(result)); + } + return; + } + case GCMDecryptionResult::ENUM_SIZE: + break; // deliberate fall-through + } + + NOTREACHED(); +} + +void GCMDriver::RegisterAfterUnregister( + const std::string& app_id, + const std::vector& normalized_sender_ids, + UnregisterCallback unregister_callback, + GCMClient::Result result) { + // Invoke the original unregister callback. + std::move(unregister_callback).Run(result); + + // Trigger the pending registration. + DCHECK(register_callbacks_.find(app_id) != register_callbacks_.end()); + RegisterImpl(app_id, normalized_sender_ids); +} + +void GCMDriver::EncryptMessage(const std::string& app_id, + const std::string& authorized_entity, + const std::string& p256dh, + const std::string& auth_secret, + const std::string& message, + EncryptMessageCallback callback) { + encryption_provider_.EncryptMessage( + app_id, authorized_entity, p256dh, auth_secret, message, + base::BindOnce(&GCMDriver::OnMessageEncrypted, + weak_ptr_factory_.GetWeakPtr(), std::move(callback))); +} + +void GCMDriver::OnMessageEncrypted(EncryptMessageCallback callback, + GCMEncryptionResult result, + std::string message) { + UMA_HISTOGRAM_ENUMERATION("GCM.Crypto.EncryptMessageResult", result, + GCMEncryptionResult::ENUM_SIZE); + std::move(callback).Run(result, std::move(message)); +} + +void GCMDriver::DecryptMessage(const std::string& app_id, + const std::string& authorized_entity, + const std::string& message, + DecryptMessageCallback callback) { + IncomingMessage incoming_message; + incoming_message.sender_id = authorized_entity; + incoming_message.raw_data = message; + incoming_message.data[GCMEncryptionProvider::kContentEncodingProperty] = + GCMEncryptionProvider::kContentCodingAes128Gcm; + encryption_provider_.DecryptMessage( + app_id, incoming_message, + base::BindOnce(&GCMDriver::OnMessageDecrypted, + weak_ptr_factory_.GetWeakPtr(), std::move(callback))); +} + +void GCMDriver::OnMessageDecrypted(DecryptMessageCallback callback, + GCMDecryptionResult result, + IncomingMessage message) { + UMA_HISTOGRAM_ENUMERATION("GCM.Crypto.DecryptMessageResult", result, + GCMDecryptionResult::ENUM_SIZE); + std::move(callback).Run(result, std::move(message.raw_data)); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_driver.h b/chromium/components/gcm_driver/gcm_driver.h new file mode 100644 index 00000000000..fbbf86d2064 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_driver.h @@ -0,0 +1,395 @@ +// 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 COMPONENTS_GCM_DRIVER_GCM_DRIVER_H_ +#define COMPONENTS_GCM_DRIVER_GCM_DRIVER_H_ + +#include +#include +#include + +#include "base/callback.h" +#include "base/memory/ref_counted.h" +#include "base/memory/weak_ptr.h" +#include "base/threading/thread_checker.h" +#include "base/time/time.h" +#include "components/gcm_driver/common/gcm_message.h" +#include "components/gcm_driver/crypto/gcm_encryption_provider.h" +#include "components/gcm_driver/gcm_client.h" + +namespace base { +class FilePath; +class SequencedTaskRunner; +} // namespace base + +namespace gcm { + +class GCMAppHandler; +class GCMConnectionObserver; +enum class GCMDecryptionResult; +enum class GCMEncryptionResult; +struct AccountMapping; + +// Provides the InstanceID support via GCMDriver. +class InstanceIDHandler { + public: + using GetTokenCallback = base::OnceCallback; + using ValidateTokenCallback = base::OnceCallback; + using DeleteTokenCallback = + base::OnceCallback; + using GetInstanceIDDataCallback = + base::OnceCallback; + + InstanceIDHandler(); + + InstanceIDHandler(const InstanceIDHandler&) = delete; + InstanceIDHandler& operator=(const InstanceIDHandler&) = delete; + + virtual ~InstanceIDHandler(); + + // Token service. + virtual void GetToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + base::TimeDelta time_to_live, + GetTokenCallback callback) = 0; + virtual void ValidateToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + const std::string& token, + ValidateTokenCallback callback) = 0; + virtual void DeleteToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + DeleteTokenCallback callback) = 0; + void DeleteAllTokensForApp(const std::string& app_id, + DeleteTokenCallback callback); + + // Persistence support. + virtual void AddInstanceIDData(const std::string& app_id, + const std::string& instance_id, + const std::string& extra_data) = 0; + virtual void RemoveInstanceIDData(const std::string& app_id) = 0; + virtual void GetInstanceIDData(const std::string& app_id, + GetInstanceIDDataCallback callback) = 0; +}; + +// Bridge between GCM users in Chrome and the platform-specific implementation. +// Obtain instances of this object by using |GCMProfileServiceFactory|. +class GCMDriver { + public: + // Max number of sender IDs that can be passed to |Register| on desktop. + constexpr static size_t kMaxSenders = 100; + + using GCMAppHandlerMap = std::map; + using RegisterCallback = + base::OnceCallback; + using ValidateRegistrationCallback = base::OnceCallback; + using UnregisterCallback = base::OnceCallback; + using SendCallback = base::OnceCallback; + using GetEncryptionInfoCallback = + base::OnceCallback; + using EncryptMessageCallback = + base::OnceCallback; + using DecryptMessageCallback = + base::OnceCallback; + + using GetGCMStatisticsCallback = + base::OnceCallback; + using GCMStatisticsRecordingCallback = + base::RepeatingCallback; + + // Enumeration to be used with GetGCMStatistics() for indicating whether the + // existing logs should be cleared or kept. + enum ClearActivityLogs { CLEAR_LOGS, KEEP_LOGS }; + + GCMDriver( + const base::FilePath& store_path, + const scoped_refptr& blocking_task_runner); + + GCMDriver(const GCMDriver&) = delete; + GCMDriver& operator=(const GCMDriver&) = delete; + + virtual ~GCMDriver(); + + // Registers |sender_ids| for an app. *Use |InstanceID| instead in new code.* + // + // A registration ID will be returned by the GCM server. On Android, only a + // single sender ID is supported, but instead multiple simultaneous + // registrations are allowed. + // |app_id|: application ID. + // |sender_ids|: list of IDs of the servers allowed to send messages to the + // application. The IDs are assigned by the Google API Console. + // Max number of IDs is 1 on Android, |kMaxSenders| on desktop. + // |callback|: to be called once the asynchronous operation is done. + void Register(const std::string& app_id, + const std::vector& sender_ids, + RegisterCallback callback); + + // Checks that the provided |sender_ids| and |registration_id| matches the + // stored registration info for |app_id|. + virtual void ValidateRegistration(const std::string& app_id, + const std::vector& sender_ids, + const std::string& registration_id, + ValidateRegistrationCallback callback) = 0; + + // Unregisters all sender_ids for an app. Only works on non-Android. Will also + // remove any encryption keys associated with the |app_id|. + // |app_id|: application ID. + // |callback|: to be called once the asynchronous operation is done. + void Unregister(const std::string& app_id, UnregisterCallback callback); + + // Unregisters an (app_id, sender_id) pair from using GCM. Only works on + // Android. Will also remove any encryption keys associated with the |app_id|. + // TODO(jianli): Switch to using GCM's unsubscribe API. + // |app_id|: application ID. + // |sender_id|: the sender ID that was passed when registering. + // |callback|: to be called once the asynchronous operation is done. + void UnregisterWithSenderId(const std::string& app_id, + const std::string& sender_id, + UnregisterCallback callback); + + // Sends a message to a given receiver. + // |app_id|: application ID. + // |receiver_id|: registration ID of the receiver party. + // |message|: message to be sent. + // |callback|: to be called once the asynchronous operation is done. + void Send(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message, + SendCallback callback); + + // Get the public encryption key and the authentication secret associated with + // |app_id|. If none have been associated with |app_id| yet, they will be + // created. The |callback| will be invoked when it is available. Only use with + // GCM registrations; use InstanceID::GetEncryptionInfo for InstanceID tokens. + virtual void GetEncryptionInfo(const std::string& app_id, + GetEncryptionInfoCallback callback); + + // Attempts to encrypt the |message| using draft-ietf-webpush-encryption-08 + // scheme using keys from internal key store. Either GetEncryptionInfo or + // InstanceID::GetEncryptionInfo must be called once for keys to be available. + // |callback| will be called asynchronously when |message| has been encrypted. + // A dispatchable message will be used in case of success, an empty message in + // case of failure. + virtual void EncryptMessage(const std::string& app_id, + const std::string& authorized_entity, + const std::string& p256dh, + const std::string& auth_secret, + const std::string& message, + EncryptMessageCallback callback); + + // Attempts to decrypt the |message|using draft-ietf-webpush-encryption-08 + // scheme using keys from internal key store. Either GetEncryptionInfo or + // InstanceID::GetEncryptionInfo must be called once for keys to be available. + // |callback| will be called asynchronously when |message| has been decrypted. + // A dispatchable message will be used in case of success, an empty message in + // case of failure. + // TODO(crbug/1045907): Decouple this from GCMDriver. + virtual void DecryptMessage(const std::string& app_id, + const std::string& authorized_entity, + const std::string& message, + DecryptMessageCallback callback); + + const GCMAppHandlerMap& app_handlers() const { return app_handlers_; } + + // This method must be called before destroying the GCMDriver. Once it has + // been called, no other GCMDriver methods may be used. + virtual void Shutdown(); + + // Called when the user signs in to or out of a GAIA account. + virtual void OnSignedIn() = 0; + virtual void OnSignedOut() = 0; + + // Adds a handler for a given app. + virtual void AddAppHandler(const std::string& app_id, GCMAppHandler* handler); + + // Remove the handler for a given app. + virtual void RemoveAppHandler(const std::string& app_id); + + // Returns the handler for the given app. May return a nullptr when no handler + // could be found for the |app_id|. + GCMAppHandler* GetAppHandler(const std::string& app_id); + + // Adds a connection state observer. + virtual void AddConnectionObserver(GCMConnectionObserver* observer) = 0; + + // Removes a connection state observer. + virtual void RemoveConnectionObserver(GCMConnectionObserver* observer) = 0; + + // For testing purpose. Always NULL on Android. + virtual GCMClient* GetGCMClientForTesting() const = 0; + + // Returns true if the service was started. + virtual bool IsStarted() const = 0; + + // Returns true if the gcm client has an open and active connection. + virtual bool IsConnected() const = 0; + + // Get GCM client internal states and statistics. The activity logs will be + // cleared before returning the stats when |clear_logs| is set to CLEAR_LOGS. + virtual void GetGCMStatistics(GetGCMStatisticsCallback callback, + ClearActivityLogs clear_logs) = 0; + + // Enables/disables GCM activity recording, and then returns the stats. + // |callback| will be called for new activity. + virtual void SetGCMRecording(const GCMStatisticsRecordingCallback& callback, + bool recording) = 0; + + // sets a list of signed in accounts with OAuth2 access tokens, when GCMDriver + // works in context of a signed in entity (e.g. browser profile where user is + // signed into sync). + // |account_tokens|: list of email addresses, account IDs and OAuth2 access + // tokens. + virtual void SetAccountTokens( + const std::vector& account_tokens) = 0; + + // Updates the |account_mapping| information in persistent store. + virtual void UpdateAccountMapping(const AccountMapping& account_mapping) = 0; + + // Removes the account mapping information reated to |account_id| from + // persistent store. + virtual void RemoveAccountMapping(const CoreAccountId& account_id) = 0; + + // Getter and setter of last token fetch time. + virtual base::Time GetLastTokenFetchTime() = 0; + virtual void SetLastTokenFetchTime(const base::Time& time) = 0; + + // These methods must only be used by the InstanceID system. + // The InstanceIDHandler provides an implementation for the InstanceID system. + virtual InstanceIDHandler* GetInstanceIDHandlerInternal() = 0; + // Allows the InstanceID system to integrate with GCM encryption storage. + virtual GCMEncryptionProvider* GetEncryptionProviderInternal(); + + // Adds or removes a custom client requested heartbeat interval. If multiple + // components set that setting, the lowest setting will be used. If the + // setting is outside of GetMax/MinClientHeartbeatIntervalMs() it will be + // ignored. If a new setting is less than the currently used, the connection + // will be reset with the new heartbeat. Client that no longer require + // aggressive heartbeats, should remove their requested interval. Heartbeats + // set this way survive connection/Chrome restart. + // + // GCM Driver can decide to postpone the action until Client is properly + // initialized, hence this setting can be called at any time. + // + // Server can overwrite the setting to a different value. + // + // |scope| is used to identify the component that requests a custom interval + // to be set, and allows that component to later revoke the setting. + // |interval_ms| should be between 2 minues and 15 minues (28 minues on + // cellular networks). For details check + // GetMin/MaxClientHeartbeatItnervalMs() in HeartbeatManager. + virtual void AddHeartbeatInterval(const std::string& scope, + int interval_ms) = 0; + virtual void RemoveHeartbeatInterval(const std::string& scope) = 0; + + protected: + // Ensures that the GCM service starts (if necessary conditions are met). + virtual GCMClient::Result EnsureStarted(GCMClient::StartMode start_mode) = 0; + + // Platform-specific implementation of Register. + virtual void RegisterImpl(const std::string& app_id, + const std::vector& sender_ids) = 0; + + // Platform-specific implementation of Unregister. + virtual void UnregisterImpl(const std::string& app_id) = 0; + + // Platform-specific implementation of UnregisterWithSenderId. + virtual void UnregisterWithSenderIdImpl(const std::string& app_id, + const std::string& sender_id); + + // Platform-specific implementation of Send. + virtual void SendImpl(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message) = 0; + + // Platform-specific implementation of recording message decryption failures. + virtual void RecordDecryptionFailure(const std::string& app_id, + GCMDecryptionResult result) = 0; + + // Runs the Register callback. + void RegisterFinished(const std::string& app_id, + const std::string& registration_id, + GCMClient::Result result); + + // To be called when a registration for |app_id| has been unregistered, having + // |result| as the result of the unregistration. Will remove any encryption + // information associated with the |app_id| and then calls UnregisterFinished. + void RemoveEncryptionInfoAfterUnregister(const std::string& app_id, + GCMClient::Result result); + + // Runs the Unregister callback. + void UnregisterFinished(const std::string& app_id, GCMClient::Result result); + + // Runs the Send callback. + void SendFinished(const std::string& app_id, + const std::string& message_id, + GCMClient::Result result); + + bool HasRegisterCallback(const std::string& app_id); + + void ClearCallbacks(); + + // Dispatches the OnMessage event to the app handler associated with |app_id|. + // If |message| has been encrypted, it will be decrypted asynchronously and + // dispatched when the decryption operation was successful. Otherwise, the + // |message| will be dispatched immediately to the handler for |app_id|. + void DispatchMessage(const std::string& app_id, + const IncomingMessage& message); + + private: + // Common code shared by Unregister and UnregisterWithSenderId. + void UnregisterInternal(const std::string& app_id, + const std::string* sender_id, + UnregisterCallback callback); + + // Dispatches the OnMessage event to the app handler associated with |app_id| + // if |result| indicates that it is safe to do so, or will report a decryption + // failure for the |app_id| otherwise. + void DispatchMessageInternal(const std::string& app_id, + GCMDecryptionResult result, + IncomingMessage message); + + // Called after unregistration completes in order to trigger the pending + // registration. + void RegisterAfterUnregister( + const std::string& app_id, + const std::vector& normalized_sender_ids, + UnregisterCallback unregister_callback, + GCMClient::Result result); + + void OnMessageEncrypted(EncryptMessageCallback callback, + GCMEncryptionResult result, + std::string message); + + void OnMessageDecrypted(DecryptMessageCallback callback, + GCMDecryptionResult result, + IncomingMessage message); + + // Callback map (from app_id to callback) for Register. + std::map register_callbacks_; + + // Callback map (from app_id to callback) for Unregister. + std::map unregister_callbacks_; + + // Callback map (from to callback) for Send. + std::map, SendCallback> send_callbacks_; + + // The encryption provider, used for key management and decryption of + // encrypted, incoming messages. + GCMEncryptionProvider encryption_provider_; + + // App handler map (from app_id to handler pointer). The handler is not owned. + GCMAppHandlerMap app_handlers_; + + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_GCM_DRIVER_H_ diff --git a/chromium/components/gcm_driver/gcm_driver_android.cc b/chromium/components/gcm_driver/gcm_driver_android.cc new file mode 100644 index 00000000000..89280e122f1 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_driver_android.cc @@ -0,0 +1,275 @@ +// 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 "components/gcm_driver/gcm_driver_android.h" + +#include +#include + +#include "base/android/jni_android.h" +#include "base/android/jni_array.h" +#include "base/android/jni_string.h" +#include "base/compiler_specific.h" +#include "base/logging.h" +#include "base/threading/thread_task_runner_handle.h" +#include "components/gcm_driver/android/jni_headers/GCMDriver_jni.h" + +using base::android::AppendJavaStringArrayToStringVector; +using base::android::AttachCurrentThread; +using base::android::ConvertJavaStringToUTF8; +using base::android::ConvertUTF8ToJavaString; +using base::android::JavaByteArrayToString; +using base::android::JavaParamRef; + +namespace gcm { + +GCMDriverAndroid::GCMDriverAndroid( + const base::FilePath& store_path, + const scoped_refptr& blocking_task_runner) + : GCMDriver(store_path, blocking_task_runner), recorder_(this) { + JNIEnv* env = AttachCurrentThread(); + java_ref_.Reset(Java_GCMDriver_create(env, reinterpret_cast(this))); +} + +GCMDriverAndroid::~GCMDriverAndroid() { + JNIEnv* env = AttachCurrentThread(); + Java_GCMDriver_destroy(env, java_ref_); +} + +void GCMDriverAndroid::OnRegisterFinished( + JNIEnv* env, + const JavaParamRef& obj, + const JavaParamRef& j_app_id, + const JavaParamRef& j_registration_id, + jboolean success) { + std::string app_id = ConvertJavaStringToUTF8(env, j_app_id); + std::string registration_id = ConvertJavaStringToUTF8(env, j_registration_id); + GCMClient::Result result = + success ? GCMClient::SUCCESS : GCMClient::UNKNOWN_ERROR; + + recorder_.RecordRegistrationResponse(app_id, success); + + RegisterFinished(app_id, registration_id, result); +} + +void GCMDriverAndroid::OnUnregisterFinished( + JNIEnv* env, + const JavaParamRef& obj, + const JavaParamRef& j_app_id, + jboolean success) { + std::string app_id = ConvertJavaStringToUTF8(env, j_app_id); + GCMClient::Result result = + success ? GCMClient::SUCCESS : GCMClient::UNKNOWN_ERROR; + + recorder_.RecordUnregistrationResponse(app_id, success); + + RemoveEncryptionInfoAfterUnregister(app_id, result); +} + +void GCMDriverAndroid::OnMessageReceived( + JNIEnv* env, + const JavaParamRef& obj, + const JavaParamRef& j_app_id, + const JavaParamRef& j_sender_id, + const JavaParamRef& j_message_id, + const JavaParamRef& j_collapse_key, + const JavaParamRef& j_raw_data, + const JavaParamRef& j_data_keys_and_values) { + std::string app_id = ConvertJavaStringToUTF8(env, j_app_id); + + int message_byte_size = 0; + + IncomingMessage message; + message.sender_id = ConvertJavaStringToUTF8(env, j_sender_id); + + if (!j_message_id.is_null()) + ConvertJavaStringToUTF8(env, j_message_id, &message.message_id); + if (!j_collapse_key.is_null()) + ConvertJavaStringToUTF8(env, j_collapse_key, &message.collapse_key); + + // Expand j_data_keys_and_values from array to map. + std::vector data_keys_and_values; + AppendJavaStringArrayToStringVector(env, j_data_keys_and_values, + &data_keys_and_values); + for (size_t i = 0; i + 1 < data_keys_and_values.size(); i += 2) { + message.data[data_keys_and_values[i]] = data_keys_and_values[i + 1]; + message_byte_size += data_keys_and_values[i + 1].size(); + } + // Convert j_raw_data from byte[] to binary std::string. + if (j_raw_data) { + JavaByteArrayToString(env, j_raw_data, &message.raw_data); + + message_byte_size += message.raw_data.size(); + } + + recorder_.RecordDataMessageReceived(app_id, message.sender_id, + message_byte_size); + + DispatchMessage(app_id, message); +} + +void GCMDriverAndroid::ValidateRegistration( + const std::string& app_id, + const std::vector& sender_ids, + const std::string& registration_id, + ValidateRegistrationCallback callback) { + // gcm_driver doesn't store registration IDs on Android, so assume it's valid. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), true /* is_valid */)); +} + +void GCMDriverAndroid::OnSignedIn() {} + +void GCMDriverAndroid::OnSignedOut() {} + +void GCMDriverAndroid::AddAppHandler(const std::string& app_id, + GCMAppHandler* handler) { + GCMDriver::AddAppHandler(app_id, handler); + JNIEnv* env = AttachCurrentThread(); + // TODO(melandory, mamir): check if messages were persisted + // and only then go to java. + Java_GCMDriver_replayPersistedMessages(env, java_ref_, + ConvertUTF8ToJavaString(env, app_id)); +} + +void GCMDriverAndroid::AddConnectionObserver(GCMConnectionObserver* observer) {} + +void GCMDriverAndroid::RemoveConnectionObserver( + GCMConnectionObserver* observer) {} + +GCMClient* GCMDriverAndroid::GetGCMClientForTesting() const { + NOTIMPLEMENTED(); + return NULL; +} + +bool GCMDriverAndroid::IsStarted() const { + return true; +} + +bool GCMDriverAndroid::IsConnected() const { + // TODO(gcm): hook up to GCM connected status + return true; +} + +void GCMDriverAndroid::GetGCMStatistics(GetGCMStatisticsCallback callback, + ClearActivityLogs clear_logs) { + DCHECK(!callback.is_null()); + + if (clear_logs == CLEAR_LOGS) + recorder_.Clear(); + + GCMClient::GCMStatistics stats; + stats.is_recording = recorder_.is_recording(); + + recorder_.CollectActivities(&stats.recorded_activities); + + std::move(callback).Run(stats); +} + +void GCMDriverAndroid::SetGCMRecording( + const GCMStatisticsRecordingCallback& callback, + bool recording) { + DCHECK(!callback.is_null()); + + gcm_statistics_recording_callback_ = callback; + recorder_.set_is_recording(recording); + + GCMClient::GCMStatistics stats; + stats.is_recording = recording; + + recorder_.CollectActivities(&stats.recorded_activities); + + callback.Run(stats); +} + +void GCMDriverAndroid::SetAccountTokens( + const std::vector& account_tokens) { + NOTIMPLEMENTED(); +} + +void GCMDriverAndroid::UpdateAccountMapping( + const AccountMapping& account_mapping) { + NOTIMPLEMENTED(); +} + +void GCMDriverAndroid::RemoveAccountMapping(const CoreAccountId& account_id) { + NOTIMPLEMENTED(); +} + +base::Time GCMDriverAndroid::GetLastTokenFetchTime() { + NOTIMPLEMENTED(); + return base::Time(); +} + +void GCMDriverAndroid::SetLastTokenFetchTime(const base::Time& time) { + NOTIMPLEMENTED(); +} + +InstanceIDHandler* GCMDriverAndroid::GetInstanceIDHandlerInternal() { + // Not supported for Android. + return NULL; +} + +void GCMDriverAndroid::AddHeartbeatInterval(const std::string& scope, + int interval_ms) {} + +void GCMDriverAndroid::RemoveHeartbeatInterval(const std::string& scope) {} + +void GCMDriverAndroid::OnActivityRecorded() { + DCHECK(gcm_statistics_recording_callback_); + + GCMClient::GCMStatistics stats; + stats.is_recording = recorder_.is_recording(); + + recorder_.CollectActivities(&stats.recorded_activities); + + gcm_statistics_recording_callback_.Run(stats); +} + +GCMClient::Result GCMDriverAndroid::EnsureStarted( + GCMClient::StartMode start_mode) { + // TODO(johnme): Maybe we should check if GMS is available? + return GCMClient::SUCCESS; +} + +void GCMDriverAndroid::RegisterImpl( + const std::string& app_id, + const std::vector& sender_ids) { + DCHECK_EQ(1u, sender_ids.size()); + JNIEnv* env = AttachCurrentThread(); + + recorder_.RecordRegistrationSent(app_id); + + Java_GCMDriver_register(env, java_ref_, ConvertUTF8ToJavaString(env, app_id), + ConvertUTF8ToJavaString(env, sender_ids[0])); +} + +void GCMDriverAndroid::UnregisterImpl(const std::string& app_id) { + NOTREACHED(); +} + +void GCMDriverAndroid::UnregisterWithSenderIdImpl( + const std::string& app_id, + const std::string& sender_id) { + JNIEnv* env = AttachCurrentThread(); + + recorder_.RecordUnregistrationSent(app_id); + + Java_GCMDriver_unregister(env, java_ref_, + ConvertUTF8ToJavaString(env, app_id), + ConvertUTF8ToJavaString(env, sender_id)); +} + +void GCMDriverAndroid::SendImpl(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message) { + NOTIMPLEMENTED(); +} + +void GCMDriverAndroid::RecordDecryptionFailure(const std::string& app_id, + GCMDecryptionResult result) { + recorder_.RecordDecryptionFailure(app_id, result); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_driver_android.h b/chromium/components/gcm_driver/gcm_driver_android.h new file mode 100644 index 00000000000..d4cac51ce2a --- /dev/null +++ b/chromium/components/gcm_driver/gcm_driver_android.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 COMPONENTS_GCM_DRIVER_GCM_DRIVER_ANDROID_H_ +#define COMPONENTS_GCM_DRIVER_GCM_DRIVER_ANDROID_H_ + +#include + +#include "base/android/scoped_java_ref.h" +#include "base/bind.h" +#include "base/compiler_specific.h" +#include "base/memory/ref_counted.h" +#include "components/gcm_driver/gcm_driver.h" +#include "components/gcm_driver/gcm_stats_recorder_android.h" + +namespace base { +class FilePath; +class SequencedTaskRunner; +} + +namespace gcm { + +// GCMDriver implementation for Android, using Android GCM APIs. +class GCMDriverAndroid : public GCMDriver, + public GCMStatsRecorderAndroid::Delegate { + public: + GCMDriverAndroid( + const base::FilePath& store_path, + const scoped_refptr& blocking_task_runner); + + GCMDriverAndroid(const GCMDriverAndroid&) = delete; + GCMDriverAndroid& operator=(const GCMDriverAndroid&) = delete; + + ~GCMDriverAndroid() override; + + // Methods called from Java via JNI: + void OnRegisterFinished( + JNIEnv* env, + const base::android::JavaParamRef& obj, + const base::android::JavaParamRef& app_id, + const base::android::JavaParamRef& registration_id, + jboolean success); + void OnUnregisterFinished(JNIEnv* env, + const base::android::JavaParamRef& obj, + const base::android::JavaParamRef& app_id, + jboolean success); + void OnMessageReceived( + JNIEnv* env, + const base::android::JavaParamRef& obj, + const base::android::JavaParamRef& app_id, + const base::android::JavaParamRef& sender_id, + const base::android::JavaParamRef& j_message_id, + const base::android::JavaParamRef& collapse_key, + const base::android::JavaParamRef& raw_data, + const base::android::JavaParamRef& data_keys_and_values); + + // GCMDriver implementation: + void ValidateRegistration(const std::string& app_id, + const std::vector& sender_ids, + const std::string& registration_id, + ValidateRegistrationCallback callback) override; + void OnSignedIn() override; + void OnSignedOut() override; + void AddConnectionObserver(GCMConnectionObserver* observer) override; + void RemoveConnectionObserver(GCMConnectionObserver* observer) override; + GCMClient* GetGCMClientForTesting() const override; + bool IsStarted() const override; + bool IsConnected() const override; + void GetGCMStatistics(GetGCMStatisticsCallback callback, + ClearActivityLogs clear_logs) override; + void SetGCMRecording(const GCMStatisticsRecordingCallback& callback, + bool recording) override; + void SetAccountTokens( + const std::vector& account_tokens) override; + void UpdateAccountMapping(const AccountMapping& account_mapping) override; + void RemoveAccountMapping(const CoreAccountId& account_id) override; + base::Time GetLastTokenFetchTime() override; + void SetLastTokenFetchTime(const base::Time& time) override; + InstanceIDHandler* GetInstanceIDHandlerInternal() override; + void AddHeartbeatInterval(const std::string& scope, int interval_ms) override; + void RemoveHeartbeatInterval(const std::string& scope) override; + void AddAppHandler(const std::string& app_id, + GCMAppHandler* handler) override; + + // GCMStatsRecorder::Delegate implementation: + void OnActivityRecorded() override; + + protected: + // GCMDriver implementation: + GCMClient::Result EnsureStarted(GCMClient::StartMode start_mode) override; + void RegisterImpl(const std::string& app_id, + const std::vector& sender_ids) override; + void UnregisterImpl(const std::string& app_id) override; + void UnregisterWithSenderIdImpl(const std::string& app_id, + const std::string& sender_id) override; + void SendImpl(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message) override; + void RecordDecryptionFailure(const std::string& app_id, + GCMDecryptionResult result) override; + + private: + base::android::ScopedJavaGlobalRef java_ref_; + + // Callback for SetGCMRecording. + GCMStatisticsRecordingCallback gcm_statistics_recording_callback_; + + // Recorder that logs GCM activities. + GCMStatsRecorderAndroid recorder_; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_GCM_DRIVER_ANDROID_H_ diff --git a/chromium/components/gcm_driver/gcm_driver_constants.cc b/chromium/components/gcm_driver/gcm_driver_constants.cc new file mode 100644 index 00000000000..1b3675d501f --- /dev/null +++ b/chromium/components/gcm_driver/gcm_driver_constants.cc @@ -0,0 +1,15 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/gcm_driver_constants.h" + +#define FPL FILE_PATH_LITERAL + +namespace gcm_driver { + +const base::FilePath::CharType kGCMStoreDirname[] = FPL("GCM Store"); + +} // namespace gcm_driver + +#undef FPL diff --git a/chromium/components/gcm_driver/gcm_driver_constants.h b/chromium/components/gcm_driver/gcm_driver_constants.h new file mode 100644 index 00000000000..3501ead17a6 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_driver_constants.h @@ -0,0 +1,20 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// A handful of resource-like constants related to the GCM(Google Cloud +// Messaging) Driver. + +#ifndef COMPONENTS_GCM_DRIVER_GCM_DRIVER_CONSTANTS_H_ +#define COMPONENTS_GCM_DRIVER_GCM_DRIVER_CONSTANTS_H_ + +#include "base/files/file_path.h" + +namespace gcm_driver { + +// File path for GCM Store. +extern const base::FilePath::CharType kGCMStoreDirname[]; + +} // namespace gcm_driver + +#endif // COMPONENTS_GCM_DRIVER_GCM_DRIVER_CONSTANTS_H_ diff --git a/chromium/components/gcm_driver/gcm_driver_desktop.cc b/chromium/components/gcm_driver/gcm_driver_desktop.cc new file mode 100644 index 00000000000..319422bf39b --- /dev/null +++ b/chromium/components/gcm_driver/gcm_driver_desktop.cc @@ -0,0 +1,1333 @@ +// 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 "components/gcm_driver/gcm_driver_desktop.h" + +#include +#include + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/files/file_path.h" +#include "base/location.h" +#include "base/logging.h" +#include "base/metrics/histogram_macros.h" +#include "base/task/sequenced_task_runner.h" +#include "base/task/task_runner_util.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/timer/timer.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "components/gcm_driver/gcm_account_mapper.h" +#include "components/gcm_driver/gcm_app_handler.h" +#include "components/gcm_driver/gcm_client_factory.h" +#include "components/gcm_driver/gcm_delayed_task_controller.h" +#include "components/gcm_driver/instance_id/instance_id_impl.h" +#include "components/gcm_driver/system_encryptor.h" +#include "google_apis/gcm/engine/account_mapping.h" +#include "net/base/ip_endpoint.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" + +namespace gcm { + +class GCMDriverDesktop::IOWorker : public GCMClient::Delegate { + public: + // Called on UI thread. + IOWorker(const scoped_refptr& ui_thread, + const scoped_refptr& io_thread); + + IOWorker(const IOWorker&) = delete; + IOWorker& operator=(const IOWorker&) = delete; + + virtual ~IOWorker(); + + // Overridden from GCMClient::Delegate: + // Called on IO thread. + void OnRegisterFinished(scoped_refptr registration_info, + const std::string& registration_id, + GCMClient::Result result) override; + void OnUnregisterFinished(scoped_refptr registration_info, + GCMClient::Result result) override; + void OnSendFinished(const std::string& app_id, + const std::string& message_id, + GCMClient::Result result) override; + void OnMessageReceived(const std::string& app_id, + const IncomingMessage& message) override; + void OnMessagesDeleted(const std::string& app_id) override; + void OnMessageSendError( + const std::string& app_id, + const GCMClient::SendErrorDetails& send_error_details) override; + void OnSendAcknowledged(const std::string& app_id, + const std::string& message_id) override; + void OnGCMReady(const std::vector& account_mappings, + const base::Time& last_token_fetch_time) override; + void OnActivityRecorded() override; + void OnConnected(const net::IPEndPoint& ip_endpoint) override; + void OnDisconnected() override; + void OnStoreReset() override; + + // Called on IO thread. + void Initialize( + std::unique_ptr gcm_client_factory, + const GCMClient::ChromeBuildInfo& chrome_build_info, + const base::FilePath& store_path, + bool remove_account_mappings_with_email_key, + base::RepeatingCallback)> + get_socket_factory_callback, + std::unique_ptr + pending_loader_factory, + network::NetworkConnectionTracker* network_connection_tracker, + const scoped_refptr blocking_task_runner); + void Start(GCMClient::StartMode start_mode, + const base::WeakPtr& service); + void Stop(); + void Register(const std::string& app_id, + const std::vector& sender_ids); + void Unregister(const std::string& app_id); + void Send(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message); + void GetGCMStatistics(GetGCMStatisticsCallback callback, + GCMDriver::ClearActivityLogs clear_logs); + void SetGCMRecording(GetGCMStatisticsCallback callback, bool recording); + + void SetAccountTokens( + const std::vector& account_tokens); + void UpdateAccountMapping(const AccountMapping& account_mapping); + void RemoveAccountMapping(const CoreAccountId& account_id); + void SetLastTokenFetchTime(const base::Time& time); + void AddHeartbeatInterval(const std::string& scope, int interval_ms); + void RemoveHeartbeatInterval(const std::string& scope); + + void AddInstanceIDData(const std::string& app_id, + const std::string& instance_id, + const std::string& extra_data); + void RemoveInstanceIDData(const std::string& app_id); + void GetInstanceIDData(const std::string& app_id); + void GetToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + base::TimeDelta time_to_live); + bool ValidateRegistration(scoped_refptr registration_info, + const std::string& registration_id); + void DeleteToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope); + + void RecordDecryptionFailure(const std::string& app_id, + GCMDecryptionResult result); + + // For testing purpose. Can be called from UI thread. Use with care. + GCMClient* gcm_client_for_testing() const { return gcm_client_.get(); } + + private: + scoped_refptr ui_thread_; + scoped_refptr io_thread_; + + base::WeakPtr service_; + + std::unique_ptr gcm_client_; +}; + +GCMDriverDesktop::IOWorker::IOWorker( + const scoped_refptr& ui_thread, + const scoped_refptr& io_thread) + : ui_thread_(ui_thread), + io_thread_(io_thread) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); +} + +GCMDriverDesktop::IOWorker::~IOWorker() { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); +} + +void GCMDriverDesktop::IOWorker::Initialize( + std::unique_ptr gcm_client_factory, + const GCMClient::ChromeBuildInfo& chrome_build_info, + const base::FilePath& store_path, + bool remove_account_mappings_with_email_key, + base::RepeatingCallback)> + get_socket_factory_callback, + std::unique_ptr + pending_loader_factory, + network::NetworkConnectionTracker* network_connection_tracker, + const scoped_refptr blocking_task_runner) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + gcm_client_ = gcm_client_factory->BuildInstance(); + + scoped_refptr url_loader_factory_for_io = + network::SharedURLLoaderFactory::Create( + std::move(pending_loader_factory)); + + gcm_client_->Initialize( + chrome_build_info, store_path, remove_account_mappings_with_email_key, + blocking_task_runner, io_thread_, std::move(get_socket_factory_callback), + url_loader_factory_for_io, network_connection_tracker, + std::make_unique(), this); +} + +void GCMDriverDesktop::IOWorker::OnRegisterFinished( + scoped_refptr registration_info, + const std::string& registration_id, + GCMClient::Result result) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + const GCMRegistrationInfo* gcm_registration_info = + GCMRegistrationInfo::FromRegistrationInfo(registration_info.get()); + if (gcm_registration_info) { + ui_thread_->PostTask( + FROM_HERE, + base::BindOnce(&GCMDriverDesktop::RegisterFinished, service_, + gcm_registration_info->app_id, registration_id, result)); + } + + const InstanceIDTokenInfo* instance_id_token_info = + InstanceIDTokenInfo::FromRegistrationInfo(registration_info.get()); + if (instance_id_token_info) { + ui_thread_->PostTask( + FROM_HERE, + base::BindOnce(&GCMDriverDesktop::GetTokenFinished, service_, + instance_id_token_info->app_id, + instance_id_token_info->authorized_entity, + instance_id_token_info->scope, registration_id, result)); + } +} + +void GCMDriverDesktop::IOWorker::OnUnregisterFinished( + scoped_refptr registration_info, + GCMClient::Result result) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + const GCMRegistrationInfo* gcm_registration_info = + GCMRegistrationInfo::FromRegistrationInfo(registration_info.get()); + if (gcm_registration_info) { + ui_thread_->PostTask( + FROM_HERE, + base::BindOnce(&GCMDriverDesktop::RemoveEncryptionInfoAfterUnregister, + service_, gcm_registration_info->app_id, result)); + } + + const InstanceIDTokenInfo* instance_id_token_info = + InstanceIDTokenInfo::FromRegistrationInfo(registration_info.get()); + if (instance_id_token_info) { + ui_thread_->PostTask( + FROM_HERE, base::BindOnce(&GCMDriverDesktop::DeleteTokenFinished, + service_, instance_id_token_info->app_id, + instance_id_token_info->authorized_entity, + instance_id_token_info->scope, result)); + } +} + +void GCMDriverDesktop::IOWorker::OnSendFinished(const std::string& app_id, + const std::string& message_id, + GCMClient::Result result) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + ui_thread_->PostTask(FROM_HERE, + base::BindOnce(&GCMDriverDesktop::SendFinished, service_, + app_id, message_id, result)); +} + +void GCMDriverDesktop::IOWorker::OnMessageReceived( + const std::string& app_id, + const IncomingMessage& message) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + ui_thread_->PostTask( + FROM_HERE, base::BindOnce(&GCMDriverDesktop::MessageReceived, service_, + app_id, message)); +} + +void GCMDriverDesktop::IOWorker::OnMessagesDeleted(const std::string& app_id) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + ui_thread_->PostTask( + FROM_HERE, + base::BindOnce(&GCMDriverDesktop::MessagesDeleted, service_, app_id)); +} + +void GCMDriverDesktop::IOWorker::OnMessageSendError( + const std::string& app_id, + const GCMClient::SendErrorDetails& send_error_details) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + ui_thread_->PostTask( + FROM_HERE, base::BindOnce(&GCMDriverDesktop::MessageSendError, service_, + app_id, send_error_details)); +} + +void GCMDriverDesktop::IOWorker::OnSendAcknowledged( + const std::string& app_id, + const std::string& message_id) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + ui_thread_->PostTask( + FROM_HERE, base::BindOnce(&GCMDriverDesktop::SendAcknowledged, service_, + app_id, message_id)); +} + +void GCMDriverDesktop::IOWorker::OnGCMReady( + const std::vector& account_mappings, + const base::Time& last_token_fetch_time) { + ui_thread_->PostTask( + FROM_HERE, base::BindOnce(&GCMDriverDesktop::GCMClientReady, service_, + account_mappings, last_token_fetch_time)); +} + +void GCMDriverDesktop::IOWorker::OnActivityRecorded() { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + // When an activity is recorded, get all the stats and refresh the UI of + // gcm-internals page. + gcm::GCMClient::GCMStatistics stats; + if (gcm_client_) { + stats = gcm_client_->GetStatistics(); + } + ui_thread_->PostTask( + FROM_HERE, + base::BindOnce(&GCMDriverDesktop::OnActivityRecorded, service_, stats)); +} + +void GCMDriverDesktop::IOWorker::OnConnected( + const net::IPEndPoint& ip_endpoint) { + ui_thread_->PostTask(FROM_HERE, base::BindOnce(&GCMDriverDesktop::OnConnected, + service_, ip_endpoint)); +} + +void GCMDriverDesktop::IOWorker::OnDisconnected() { + ui_thread_->PostTask( + FROM_HERE, base::BindOnce(&GCMDriverDesktop::OnDisconnected, service_)); +} + +void GCMDriverDesktop::IOWorker::OnStoreReset() { + ui_thread_->PostTask( + FROM_HERE, base::BindOnce(&GCMDriverDesktop::OnStoreReset, service_)); +} + +void GCMDriverDesktop::IOWorker::Start( + GCMClient::StartMode start_mode, + const base::WeakPtr& service) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + service_ = service; + gcm_client_->Start(start_mode); +} + +void GCMDriverDesktop::IOWorker::Stop() { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + gcm_client_->Stop(); +} + +void GCMDriverDesktop::IOWorker::Register( + const std::string& app_id, + const std::vector& sender_ids) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + auto gcm_info = base::MakeRefCounted(); + gcm_info->app_id = app_id; + gcm_info->sender_ids = sender_ids; + gcm_client_->Register(std::move(gcm_info)); +} + +bool GCMDriverDesktop::IOWorker::ValidateRegistration( + scoped_refptr registration_info, + const std::string& registration_id) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + return gcm_client_->ValidateRegistration(std::move(registration_info), + registration_id); +} + +void GCMDriverDesktop::IOWorker::Unregister(const std::string& app_id) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + auto gcm_info = base::MakeRefCounted(); + gcm_info->app_id = app_id; + gcm_client_->Unregister(std::move(gcm_info)); +} + +void GCMDriverDesktop::IOWorker::Send(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + gcm_client_->Send(app_id, receiver_id, message); +} + +void GCMDriverDesktop::IOWorker::GetGCMStatistics( + GetGCMStatisticsCallback callback, + ClearActivityLogs clear_logs) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + gcm::GCMClient::GCMStatistics stats; + + if (gcm_client_) { + if (clear_logs == GCMDriver::CLEAR_LOGS) + gcm_client_->ClearActivityLogs(); + stats = gcm_client_->GetStatistics(); + } + + ui_thread_->PostTask(FROM_HERE, base::BindOnce(std::move(callback), stats)); +} + +void GCMDriverDesktop::IOWorker::SetGCMRecording( + GetGCMStatisticsCallback callback, + bool recording) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + gcm::GCMClient::GCMStatistics stats; + + if (gcm_client_) { + gcm_client_->SetRecording(recording); + stats = gcm_client_->GetStatistics(); + stats.gcm_client_created = true; + } + + ui_thread_->PostTask(FROM_HERE, base::BindOnce(std::move(callback), stats)); +} + +void GCMDriverDesktop::IOWorker::SetAccountTokens( + const std::vector& account_tokens) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + if (gcm_client_) + gcm_client_->SetAccountTokens(account_tokens); +} + +void GCMDriverDesktop::IOWorker::UpdateAccountMapping( + const AccountMapping& account_mapping) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + if (gcm_client_) + gcm_client_->UpdateAccountMapping(account_mapping); +} + +void GCMDriverDesktop::IOWorker::RemoveAccountMapping( + const CoreAccountId& account_id) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + if (gcm_client_) + gcm_client_->RemoveAccountMapping(account_id); +} + +void GCMDriverDesktop::IOWorker::SetLastTokenFetchTime(const base::Time& time) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + if (gcm_client_) + gcm_client_->SetLastTokenFetchTime(time); +} + +void GCMDriverDesktop::IOWorker::AddInstanceIDData( + const std::string& app_id, + const std::string& instance_id, + const std::string& extra_data) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + if (gcm_client_) + gcm_client_->AddInstanceIDData(app_id, instance_id, extra_data); +} + +void GCMDriverDesktop::IOWorker::RemoveInstanceIDData( + const std::string& app_id) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + if (gcm_client_) + gcm_client_->RemoveInstanceIDData(app_id); +} + +void GCMDriverDesktop::IOWorker::GetInstanceIDData( + const std::string& app_id) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + std::string instance_id; + std::string extra_data; + if (gcm_client_) + gcm_client_->GetInstanceIDData(app_id, &instance_id, &extra_data); + + ui_thread_->PostTask( + FROM_HERE, base::BindOnce(&GCMDriverDesktop::GetInstanceIDDataFinished, + service_, app_id, instance_id, extra_data)); +} + +void GCMDriverDesktop::IOWorker::GetToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + base::TimeDelta time_to_live) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + + auto instance_id_token_info = base::MakeRefCounted(); + instance_id_token_info->app_id = app_id; + instance_id_token_info->authorized_entity = authorized_entity; + instance_id_token_info->scope = scope; + instance_id_token_info->time_to_live = time_to_live; + gcm_client_->Register(std::move(instance_id_token_info)); +} + +void GCMDriverDesktop::IOWorker::DeleteToken( + const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope) { + auto instance_id_token_info = base::MakeRefCounted(); + instance_id_token_info->app_id = app_id; + instance_id_token_info->authorized_entity = authorized_entity; + instance_id_token_info->scope = scope; + gcm_client_->Unregister(std::move(instance_id_token_info)); +} + +void GCMDriverDesktop::IOWorker::AddHeartbeatInterval(const std::string& scope, + int interval_ms) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + gcm_client_->AddHeartbeatInterval(scope, interval_ms); +} + +void GCMDriverDesktop::IOWorker::RemoveHeartbeatInterval( + const std::string& scope) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + gcm_client_->RemoveHeartbeatInterval(scope); +} + +void GCMDriverDesktop::IOWorker::RecordDecryptionFailure( + const std::string& app_id, + GCMDecryptionResult result) { + DCHECK(io_thread_->RunsTasksInCurrentSequence()); + gcm_client_->RecordDecryptionFailure(app_id, result); +} + +GCMDriverDesktop::GCMDriverDesktop( + std::unique_ptr gcm_client_factory, + const GCMClient::ChromeBuildInfo& chrome_build_info, + PrefService* prefs, + const base::FilePath& store_path, + bool remove_account_mappings_with_email_key, + base::RepeatingCallback)> + get_socket_factory_callback, + scoped_refptr url_loader_factory_for_ui, + network::NetworkConnectionTracker* network_connection_tracker, + const scoped_refptr& ui_thread, + const scoped_refptr& io_thread, + const scoped_refptr& blocking_task_runner) + : GCMDriver(store_path, blocking_task_runner), + signed_in_(false), + gcm_started_(false), + connected_(false), + account_mapper_(new GCMAccountMapper(this)), + // Setting to max, to make sure it does not prompt for token reporting + // Before reading a reasonable value from the DB, which might be never, + // in which case the fetching will be triggered. + last_token_fetch_time_(base::Time::Max()), + ui_thread_(ui_thread), + io_thread_(io_thread) { + // Create and initialize the GCMClient. Note that this does not initiate the + // GCM check-in. + io_worker_ = std::make_unique(ui_thread, io_thread); + io_thread_->PostTask( + FROM_HERE, + base::BindOnce( + &GCMDriverDesktop::IOWorker::Initialize, + base::Unretained(io_worker_.get()), std::move(gcm_client_factory), + chrome_build_info, store_path, remove_account_mappings_with_email_key, + std::move(get_socket_factory_callback), + // ->Clone() permits creation of an equivalent + // SharedURLLoaderFactory on IO thread. + url_loader_factory_for_ui->Clone(), + base::Unretained(network_connection_tracker), blocking_task_runner)); +} + +GCMDriverDesktop::~GCMDriverDesktop() { +} + +void GCMDriverDesktop::ValidateRegistration( + const std::string& app_id, + const std::vector& sender_ids, + const std::string& registration_id, + ValidateRegistrationCallback callback) { + DCHECK(!app_id.empty()); + DCHECK(!sender_ids.empty() && sender_ids.size() <= kMaxSenders); + DCHECK(!registration_id.empty()); + DCHECK(!callback.is_null()); + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + GCMClient::Result result = EnsureStarted(GCMClient::IMMEDIATE_START); + if (result != GCMClient::SUCCESS) { + // Can't tell whether the registration is valid or not, so don't run the + // callback (let it hang indefinitely). + return; + } + + // Only validating current state, so ignore pending register_callbacks_. + + auto gcm_info = base::MakeRefCounted(); + gcm_info->app_id = app_id; + gcm_info->sender_ids = sender_ids; + // Normalize the sender IDs by making them sorted. + std::sort(gcm_info->sender_ids.begin(), gcm_info->sender_ids.end()); + + if (!delayed_task_controller_->CanRunTaskWithoutDelay()) { + delayed_task_controller_->AddTask( + base::BindOnce(&GCMDriverDesktop::DoValidateRegistration, + weak_ptr_factory_.GetWeakPtr(), gcm_info, + registration_id, std::move(callback))); + return; + } + + DoValidateRegistration(std::move(gcm_info), registration_id, + std::move(callback)); +} + +void GCMDriverDesktop::DoValidateRegistration( + scoped_refptr registration_info, + const std::string& registration_id, + ValidateRegistrationCallback callback) { + base::PostTaskAndReplyWithResult( + io_thread_.get(), FROM_HERE, + base::BindOnce(&GCMDriverDesktop::IOWorker::ValidateRegistration, + base::Unretained(io_worker_.get()), + std::move(registration_info), registration_id), + std::move(callback)); +} + +void GCMDriverDesktop::Shutdown() { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + Stop(); + GCMDriver::Shutdown(); + + io_thread_->DeleteSoon(FROM_HERE, io_worker_.release()); +} + +void GCMDriverDesktop::OnSignedIn() { + signed_in_ = true; +} + +void GCMDriverDesktop::OnSignedOut() { + signed_in_ = false; +} + +void GCMDriverDesktop::AddAppHandler(const std::string& app_id, + GCMAppHandler* handler) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + GCMDriver::AddAppHandler(app_id, handler); + + // Ensures that the GCM service is started when there is an interest. + EnsureStarted(GCMClient::DELAYED_START); +} + +void GCMDriverDesktop::RemoveAppHandler(const std::string& app_id) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + GCMDriver::RemoveAppHandler(app_id); + + // Stops the GCM service when no app intends to consume it. Stop function will + // remove the last app handler - account mapper. + if (app_handlers().size() == 1) + Stop(); +} + +void GCMDriverDesktop::AddConnectionObserver(GCMConnectionObserver* observer) { + connection_observer_list_.AddObserver(observer); +} + +void GCMDriverDesktop::RemoveConnectionObserver( + GCMConnectionObserver* observer) { + connection_observer_list_.RemoveObserver(observer); +} + +void GCMDriverDesktop::Stop() { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + // No need to stop GCM service if not started yet. + if (!gcm_started_) + return; + + account_mapper_->ShutdownHandler(); + GCMDriver::RemoveAppHandler(kGCMAccountMapperAppId); + + RemoveCachedData(); + + io_thread_->PostTask(FROM_HERE, + base::BindOnce(&GCMDriverDesktop::IOWorker::Stop, + base::Unretained(io_worker_.get()))); +} + +void GCMDriverDesktop::RegisterImpl( + const std::string& app_id, + const std::vector& sender_ids) { + // Delay the register operation until GCMClient is ready. + if (!delayed_task_controller_->CanRunTaskWithoutDelay()) { + delayed_task_controller_->AddTask( + base::BindOnce(&GCMDriverDesktop::DoRegister, + weak_ptr_factory_.GetWeakPtr(), app_id, sender_ids)); + return; + } + + DoRegister(app_id, sender_ids); +} + +void GCMDriverDesktop::DoRegister(const std::string& app_id, + const std::vector& sender_ids) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + if (!HasRegisterCallback(app_id)) { + // The callback could have been removed when the app is uninstalled. + return; + } + + io_thread_->PostTask( + FROM_HERE, + base::BindOnce(&GCMDriverDesktop::IOWorker::Register, + base::Unretained(io_worker_.get()), app_id, sender_ids)); +} + +void GCMDriverDesktop::UnregisterImpl(const std::string& app_id) { + // Delay the unregister operation until GCMClient is ready. + if (!delayed_task_controller_->CanRunTaskWithoutDelay()) { + delayed_task_controller_->AddTask( + base::BindOnce(&GCMDriverDesktop::DoUnregister, + weak_ptr_factory_.GetWeakPtr(), app_id)); + return; + } + + DoUnregister(app_id); +} + +void GCMDriverDesktop::DoUnregister(const std::string& app_id) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + // Ask the server to unregister it. There could be a small chance that the + // unregister request fails. If this occurs, it does not bring any harm since + // we simply reject the messages/events received from the server. + io_thread_->PostTask( + FROM_HERE, base::BindOnce(&GCMDriverDesktop::IOWorker::Unregister, + base::Unretained(io_worker_.get()), app_id)); +} + +void GCMDriverDesktop::SendImpl(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message) { + // Delay the send operation until all GCMClient is ready. + if (!delayed_task_controller_->CanRunTaskWithoutDelay()) { + delayed_task_controller_->AddTask(base::BindOnce( + &GCMDriverDesktop::DoSend, weak_ptr_factory_.GetWeakPtr(), app_id, + receiver_id, message)); + return; + } + + DoSend(app_id, receiver_id, message); +} + +void GCMDriverDesktop::DoSend(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + io_thread_->PostTask( + FROM_HERE, base::BindOnce(&GCMDriverDesktop::IOWorker::Send, + base::Unretained(io_worker_.get()), app_id, + receiver_id, message)); +} + +void GCMDriverDesktop::RecordDecryptionFailure(const std::string& app_id, + GCMDecryptionResult result) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + io_thread_->PostTask( + FROM_HERE, + base::BindOnce(&GCMDriverDesktop::IOWorker::RecordDecryptionFailure, + base::Unretained(io_worker_.get()), app_id, result)); +} + +GCMClient* GCMDriverDesktop::GetGCMClientForTesting() const { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + return io_worker_ ? io_worker_->gcm_client_for_testing() : nullptr; +} + +bool GCMDriverDesktop::IsStarted() const { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + return gcm_started_; +} + +bool GCMDriverDesktop::IsConnected() const { + return connected_; +} + +void GCMDriverDesktop::GetGCMStatistics(GetGCMStatisticsCallback callback, + ClearActivityLogs clear_logs) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + DCHECK(!callback.is_null()); + + io_thread_->PostTask( + FROM_HERE, base::BindOnce(&GCMDriverDesktop::IOWorker::GetGCMStatistics, + base::Unretained(io_worker_.get()), + std::move(callback), clear_logs)); +} + +void GCMDriverDesktop::SetGCMRecording( + const GCMStatisticsRecordingCallback& callback, + bool recording) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + gcm_statistics_recording_callback_ = callback; + io_thread_->PostTask( + FROM_HERE, + base::BindOnce(&GCMDriverDesktop::IOWorker::SetGCMRecording, + base::Unretained(io_worker_.get()), callback, recording)); +} + +void GCMDriverDesktop::UpdateAccountMapping( + const AccountMapping& account_mapping) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + io_thread_->PostTask( + FROM_HERE, + base::BindOnce(&GCMDriverDesktop::IOWorker::UpdateAccountMapping, + base::Unretained(io_worker_.get()), account_mapping)); +} + +void GCMDriverDesktop::RemoveAccountMapping(const CoreAccountId& account_id) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + io_thread_->PostTask( + FROM_HERE, + base::BindOnce(&GCMDriverDesktop::IOWorker::RemoveAccountMapping, + base::Unretained(io_worker_.get()), account_id)); +} + +base::Time GCMDriverDesktop::GetLastTokenFetchTime() { + return last_token_fetch_time_; +} + +void GCMDriverDesktop::SetLastTokenFetchTime(const base::Time& time) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + last_token_fetch_time_ = time; + + io_thread_->PostTask( + FROM_HERE, + base::BindOnce(&GCMDriverDesktop::IOWorker::SetLastTokenFetchTime, + base::Unretained(io_worker_.get()), time)); +} + +InstanceIDHandler* GCMDriverDesktop::GetInstanceIDHandlerInternal() { + return this; +} + +void GCMDriverDesktop::GetToken( + const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + base::TimeDelta time_to_live, + GetTokenCallback callback) { + DCHECK(!app_id.empty()); + DCHECK(!authorized_entity.empty()); + DCHECK(!scope.empty()); + DCHECK(!callback.is_null()); + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + GCMClient::Result result = EnsureStarted(GCMClient::IMMEDIATE_START); + if (result != GCMClient::SUCCESS) { + DLOG(ERROR) + << "Unable to get the InstanceID token: cannot start the GCM Client"; + + std::move(callback).Run(std::string(), result); + return; + } + + // If previous GetToken operation is still in progress, bail out. + TokenTuple tuple_key(app_id, authorized_entity, scope); + if (get_token_callbacks_.find(tuple_key) != get_token_callbacks_.end()) { + std::move(callback).Run(std::string(), GCMClient::ASYNC_OPERATION_PENDING); + return; + } + + get_token_callbacks_[tuple_key] = std::move(callback); + + // Delay the GetToken operation until GCMClient is ready. + if (!delayed_task_controller_->CanRunTaskWithoutDelay()) { + delayed_task_controller_->AddTask(base::BindOnce( + &GCMDriverDesktop::DoGetToken, weak_ptr_factory_.GetWeakPtr(), app_id, + authorized_entity, scope, time_to_live)); + return; + } + + DoGetToken(app_id, authorized_entity, scope, time_to_live); +} + +void GCMDriverDesktop::DoGetToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + base::TimeDelta time_to_live) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + TokenTuple tuple_key(app_id, authorized_entity, scope); + auto callback_iter = get_token_callbacks_.find(tuple_key); + if (callback_iter == get_token_callbacks_.end()) { + // The callback could have been removed when the app is uninstalled. + return; + } + + io_thread_->PostTask( + FROM_HERE, base::BindOnce(&GCMDriverDesktop::IOWorker::GetToken, + base::Unretained(io_worker_.get()), app_id, + authorized_entity, scope, time_to_live)); +} + +void GCMDriverDesktop::ValidateToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + const std::string& token, + ValidateTokenCallback callback) { + DCHECK(!app_id.empty()); + DCHECK(!authorized_entity.empty()); + DCHECK(!scope.empty()); + DCHECK(!token.empty()); + DCHECK(callback); + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + GCMClient::Result result = EnsureStarted(GCMClient::IMMEDIATE_START); + if (result != GCMClient::SUCCESS) { + // Can't tell whether the registration is valid or not, so don't run the + // callback (let it hang indefinitely). + DLOG(ERROR) << "Unable to validate the InstanceID token: cannot start the " + "GCM Client"; + return; + } + + // Only validating current state, so ignore pending get_token_callbacks_. + + auto instance_id_info = base::MakeRefCounted(); + instance_id_info->app_id = app_id; + instance_id_info->authorized_entity = authorized_entity; + instance_id_info->scope = scope; + + if (!delayed_task_controller_->CanRunTaskWithoutDelay()) { + delayed_task_controller_->AddTask( + base::BindOnce(&GCMDriverDesktop::DoValidateRegistration, + weak_ptr_factory_.GetWeakPtr(), instance_id_info, token, + std::move(callback))); + return; + } + + DoValidateRegistration(std::move(instance_id_info), token, + std::move(callback)); +} + +void GCMDriverDesktop::DeleteToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + DeleteTokenCallback callback) { + DCHECK(!app_id.empty()); + DCHECK(!authorized_entity.empty()); + DCHECK(!scope.empty()); + DCHECK(!callback.is_null()); + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + GCMClient::Result result = EnsureStarted(GCMClient::IMMEDIATE_START); + if (result != GCMClient::SUCCESS) { + DLOG(ERROR) + << "Unable to delete the InstanceID token: cannot start the GCM Client"; + + std::move(callback).Run(result); + return; + } + + // If previous GetToken operation is still in progress, bail out. + TokenTuple tuple_key(app_id, authorized_entity, scope); + if (delete_token_callbacks_.find(tuple_key) != + delete_token_callbacks_.end()) { + std::move(callback).Run(GCMClient::ASYNC_OPERATION_PENDING); + return; + } + + delete_token_callbacks_[tuple_key] = std::move(callback); + + // Delay the DeleteToken operation until GCMClient is ready. + if (!delayed_task_controller_->CanRunTaskWithoutDelay()) { + delayed_task_controller_->AddTask(base::BindOnce( + &GCMDriverDesktop::DoDeleteToken, weak_ptr_factory_.GetWeakPtr(), + app_id, authorized_entity, scope)); + return; + } + + DoDeleteToken(app_id, authorized_entity, scope); +} + +void GCMDriverDesktop::DoDeleteToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + io_thread_->PostTask( + FROM_HERE, base::BindOnce(&GCMDriverDesktop::IOWorker::DeleteToken, + base::Unretained(io_worker_.get()), app_id, + authorized_entity, scope)); +} + +void GCMDriverDesktop::AddInstanceIDData( + const std::string& app_id, + const std::string& instance_id, + const std::string& extra_data) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + GCMClient::Result result = EnsureStarted(GCMClient::IMMEDIATE_START); + if (result != GCMClient::SUCCESS) { + DLOG(ERROR) + << "Unable to add the InstanceID data: cannot start the GCM Client"; + return; + } + + // Delay the operation until GCMClient is ready. + if (!delayed_task_controller_->CanRunTaskWithoutDelay()) { + delayed_task_controller_->AddTask(base::BindOnce( + &GCMDriverDesktop::DoAddInstanceIDData, weak_ptr_factory_.GetWeakPtr(), + app_id, instance_id, extra_data)); + return; + } + + DoAddInstanceIDData(app_id, instance_id, extra_data); +} + +void GCMDriverDesktop::DoAddInstanceIDData( + const std::string& app_id, + const std::string& instance_id, + const std::string& extra_data) { + io_thread_->PostTask( + FROM_HERE, base::BindOnce(&GCMDriverDesktop::IOWorker::AddInstanceIDData, + base::Unretained(io_worker_.get()), app_id, + instance_id, extra_data)); +} + +void GCMDriverDesktop::RemoveInstanceIDData(const std::string& app_id) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + GCMClient::Result result = EnsureStarted(GCMClient::IMMEDIATE_START); + if (result != GCMClient::SUCCESS) { + DLOG(ERROR) + << "Unable to remove the InstanceID data: cannot start the GCM Client"; + return; + } + + // Delay the operation until GCMClient is ready. + if (!delayed_task_controller_->CanRunTaskWithoutDelay()) { + delayed_task_controller_->AddTask( + base::BindOnce(&GCMDriverDesktop::DoRemoveInstanceIDData, + weak_ptr_factory_.GetWeakPtr(), app_id)); + return; + } + + DoRemoveInstanceIDData(app_id); +} + +void GCMDriverDesktop::DoRemoveInstanceIDData(const std::string& app_id) { + io_thread_->PostTask( + FROM_HERE, + base::BindOnce(&GCMDriverDesktop::IOWorker::RemoveInstanceIDData, + base::Unretained(io_worker_.get()), app_id)); +} + +void GCMDriverDesktop::GetInstanceIDData(const std::string& app_id, + GetInstanceIDDataCallback callback) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + GCMClient::Result result = EnsureStarted(GCMClient::IMMEDIATE_START); + // TODO(crbug/1028761): This method is only used by InstanceIDImpl to get the + // current instance ID from the store. As this method doesn't support error + // codes, the instance ID will assume no current ID and generate a new one + // if the gcm client is not ready and we pass an empty string to the callback + // below. We should fix this! + if (result != GCMClient::SUCCESS) { + DLOG(ERROR) + << "Unable to get the InstanceID data: cannot start the GCM Client"; + // Resolve the |callback| to not leave it hanging indefinitely. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(std::move(callback), std::string(), std::string())); + return; + } + + get_instance_id_data_callbacks_[app_id].push(std::move(callback)); + + // Delay the operation until GCMClient is ready. + if (!delayed_task_controller_->CanRunTaskWithoutDelay()) { + delayed_task_controller_->AddTask( + base::BindOnce(&GCMDriverDesktop::DoGetInstanceIDData, + weak_ptr_factory_.GetWeakPtr(), app_id)); + return; + } + + DoGetInstanceIDData(app_id); +} + +void GCMDriverDesktop::DoGetInstanceIDData(const std::string& app_id) { + io_thread_->PostTask( + FROM_HERE, base::BindOnce(&GCMDriverDesktop::IOWorker::GetInstanceIDData, + base::Unretained(io_worker_.get()), app_id)); +} + +void GCMDriverDesktop::GetInstanceIDDataFinished( + const std::string& app_id, + const std::string& instance_id, + const std::string& extra_data) { + auto iter = get_instance_id_data_callbacks_.find(app_id); + DCHECK(iter != get_instance_id_data_callbacks_.end()); + + base::queue& callbacks = iter->second; + std::move(callbacks.front()).Run(instance_id, extra_data); + + callbacks.pop(); + + if (!callbacks.size()) + get_instance_id_data_callbacks_.erase(iter); +} + +void GCMDriverDesktop::GetTokenFinished(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + const std::string& token, + GCMClient::Result result) { + TokenTuple tuple_key(app_id, authorized_entity, scope); + auto callback_iter = get_token_callbacks_.find(tuple_key); + if (callback_iter == get_token_callbacks_.end()) { + // The callback could have been removed when the app is uninstalled. + return; + } + + GetTokenCallback callback = std::move(callback_iter->second); + get_token_callbacks_.erase(callback_iter); + std::move(callback).Run(token, result); +} + +void GCMDriverDesktop::DeleteTokenFinished(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + GCMClient::Result result) { + TokenTuple tuple_key(app_id, authorized_entity, scope); + auto callback_iter = delete_token_callbacks_.find(tuple_key); + if (callback_iter == delete_token_callbacks_.end()) { + // The callback could have been removed when the app is uninstalled. + return; + } + + DeleteTokenCallback callback = std::move(callback_iter->second); + delete_token_callbacks_.erase(callback_iter); + std::move(callback).Run(result); +} + +void GCMDriverDesktop::AddHeartbeatInterval(const std::string& scope, + int interval_ms) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + // The GCM service has not been initialized. + if (!delayed_task_controller_) + return; + + if (!delayed_task_controller_->CanRunTaskWithoutDelay()) { + // The GCM service was initialized but has not started yet. + delayed_task_controller_->AddTask( + base::BindOnce(&GCMDriverDesktop::AddHeartbeatInterval, + weak_ptr_factory_.GetWeakPtr(), scope, interval_ms)); + return; + } + + io_thread_->PostTask( + FROM_HERE, + base::BindOnce(&GCMDriverDesktop::IOWorker::AddHeartbeatInterval, + base::Unretained(io_worker_.get()), scope, interval_ms)); +} + +void GCMDriverDesktop::RemoveHeartbeatInterval(const std::string& scope) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + // The GCM service has not been initialized. + if (!delayed_task_controller_) + return; + + if (!delayed_task_controller_->CanRunTaskWithoutDelay()) { + // The GCM service was initialized but has not started yet. + delayed_task_controller_->AddTask( + base::BindOnce(&GCMDriverDesktop::RemoveHeartbeatInterval, + weak_ptr_factory_.GetWeakPtr(), scope)); + return; + } + + io_thread_->PostTask( + FROM_HERE, + base::BindOnce(&GCMDriverDesktop::IOWorker::RemoveHeartbeatInterval, + base::Unretained(io_worker_.get()), scope)); +} + +void GCMDriverDesktop::SetAccountTokens( + const std::vector& account_tokens) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + account_mapper_->SetAccountTokens(account_tokens); + + io_thread_->PostTask( + FROM_HERE, + base::BindOnce(&GCMDriverDesktop::IOWorker::SetAccountTokens, + base::Unretained(io_worker_.get()), account_tokens)); +} + +GCMClient::Result GCMDriverDesktop::EnsureStarted( + GCMClient::StartMode start_mode) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + if (gcm_started_) + return GCMClient::SUCCESS; + + // Have any app requested the service? + if (app_handlers().empty()) + return GCMClient::UNKNOWN_ERROR; + + if (!delayed_task_controller_) + delayed_task_controller_ = std::make_unique(); + + // Note that we need to pass weak pointer again since the existing weak + // pointer in IOWorker might have been invalidated when GCM is stopped. + io_thread_->PostTask( + FROM_HERE, base::BindOnce(&GCMDriverDesktop::IOWorker::Start, + base::Unretained(io_worker_.get()), start_mode, + weak_ptr_factory_.GetWeakPtr())); + + return GCMClient::SUCCESS; +} + +void GCMDriverDesktop::RemoveCachedData() { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + // Remove all the queued tasks since they no longer make sense after + // GCM service is stopped. + weak_ptr_factory_.InvalidateWeakPtrs(); + + gcm_started_ = false; + delayed_task_controller_.reset(); + ClearCallbacks(); +} + +void GCMDriverDesktop::MessageReceived(const std::string& app_id, + const IncomingMessage& message) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + // Drop the event if the service has been stopped. + if (!gcm_started_) + return; + + DispatchMessage(app_id, message); +} + +void GCMDriverDesktop::MessagesDeleted(const std::string& app_id) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + // Drop the event if the service has been stopped. + if (!gcm_started_) + return; + + GCMAppHandler* handler = GetAppHandler(app_id); + if (handler) + handler->OnMessagesDeleted(app_id); +} + +void GCMDriverDesktop::MessageSendError( + const std::string& app_id, + const GCMClient::SendErrorDetails& send_error_details) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + // Drop the event if the service has been stopped. + if (!gcm_started_) + return; + + GCMAppHandler* handler = GetAppHandler(app_id); + if (handler) + handler->OnSendError(app_id, send_error_details); +} + +void GCMDriverDesktop::SendAcknowledged(const std::string& app_id, + const std::string& message_id) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + // Drop the event if the service has been stopped. + if (!gcm_started_) + return; + + GCMAppHandler* handler = GetAppHandler(app_id); + if (handler) + handler->OnSendAcknowledged(app_id, message_id); +} + +void GCMDriverDesktop::GCMClientReady( + const std::vector& account_mappings, + const base::Time& last_token_fetch_time) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + UMA_HISTOGRAM_BOOLEAN("GCM.UserSignedIn", signed_in_); + + gcm_started_ = true; + + last_token_fetch_time_ = last_token_fetch_time; + + GCMDriver::AddAppHandler(kGCMAccountMapperAppId, account_mapper_.get()); + account_mapper_->Initialize( + account_mappings, base::BindRepeating(&GCMDriverDesktop::MessageReceived, + weak_ptr_factory_.GetWeakPtr())); + + delayed_task_controller_->SetReady(); +} + +void GCMDriverDesktop::OnConnected(const net::IPEndPoint& ip_endpoint) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + connected_ = true; + + // Drop the event if the service has been stopped. + if (!gcm_started_) + return; + + for (GCMConnectionObserver& observer : connection_observer_list_) + observer.OnConnected(ip_endpoint); +} + +void GCMDriverDesktop::OnDisconnected() { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + connected_ = false; + + // Drop the event if the service has been stopped. + if (!gcm_started_) + return; + + for (GCMConnectionObserver& observer : connection_observer_list_) + observer.OnDisconnected(); +} + +void GCMDriverDesktop::OnStoreReset() { + // Defensive copy in case OnStoreReset calls Add/RemoveAppHandler. + std::vector app_handler_values; + for (const auto& key_value : app_handlers()) + app_handler_values.push_back(key_value.second); + for (GCMAppHandler* app_handler : app_handler_values) { + app_handler->OnStoreReset(); + // app_handler might now have been deleted. + } +} + +void GCMDriverDesktop::OnActivityRecorded( + const GCMClient::GCMStatistics& stats) { + DCHECK(ui_thread_->RunsTasksInCurrentSequence()); + + if (gcm_statistics_recording_callback_) + gcm_statistics_recording_callback_.Run(stats); +} + +bool GCMDriverDesktop::TokenTupleComparer::operator()( + const TokenTuple& a, const TokenTuple& b) const { + if (std::get<0>(a) < std::get<0>(b)) + return true; + if (std::get<0>(a) > std::get<0>(b)) + return false; + + if (std::get<1>(a) < std::get<1>(b)) + return true; + if (std::get<1>(a) > std::get<1>(b)) + return false; + + return std::get<2>(a) < std::get<2>(b); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_driver_desktop.h b/chromium/components/gcm_driver/gcm_driver_desktop.h new file mode 100644 index 00000000000..fc4e5005e11 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_driver_desktop.h @@ -0,0 +1,259 @@ +// 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 COMPONENTS_GCM_DRIVER_GCM_DRIVER_DESKTOP_H_ +#define COMPONENTS_GCM_DRIVER_GCM_DRIVER_DESKTOP_H_ + +#include +#include +#include +#include +#include + +#include "base/compiler_specific.h" +#include "base/containers/queue.h" +#include "base/memory/ref_counted.h" +#include "base/memory/weak_ptr.h" +#include "base/observer_list.h" +#include "base/tuple.h" +#include "components/gcm_driver/crypto/gcm_decryption_result.h" +#include "components/gcm_driver/gcm_client.h" +#include "components/gcm_driver/gcm_connection_observer.h" +#include "components/gcm_driver/gcm_driver.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "services/network/public/mojom/proxy_resolving_socket.mojom.h" + +class PrefService; + +namespace base { +class FilePath; +class SequencedTaskRunner; +} + +namespace network { +class NetworkConnectionTracker; +class SharedURLLoaderFactory; +} + +namespace gcm { + +class GCMAccountMapper; +class GCMAppHandler; +class GCMClientFactory; +enum class GCMDecryptionResult; +class GCMDelayedTaskController; + +// GCMDriver implementation for desktop and Chrome OS, using GCMClient. +class GCMDriverDesktop : public GCMDriver, + protected InstanceIDHandler { + public: + // |remove_account_mappings_with_email_key| indicates whether account mappings + // having email as account key should be removed while loading. This is + // required during the migration of account identifier from email to Gaia ID. + GCMDriverDesktop( + std::unique_ptr gcm_client_factory, + const GCMClient::ChromeBuildInfo& chrome_build_info, + PrefService* prefs, + const base::FilePath& store_path, + bool remove_account_mappings_with_email_key, + base::RepeatingCallback)> + get_socket_factory_callback, + scoped_refptr url_loader_factory_for_ui, + network::NetworkConnectionTracker* network_connection_tracker, + const scoped_refptr& ui_thread, + const scoped_refptr& io_thread, + const scoped_refptr& blocking_task_runner); + + GCMDriverDesktop(const GCMDriverDesktop&) = delete; + GCMDriverDesktop& operator=(const GCMDriverDesktop&) = delete; + + ~GCMDriverDesktop() override; + + // GCMDriver implementation: + void ValidateRegistration(const std::string& app_id, + const std::vector& sender_ids, + const std::string& registration_id, + ValidateRegistrationCallback callback) override; + void Shutdown() override; + void OnSignedIn() override; + void OnSignedOut() override; + void AddAppHandler(const std::string& app_id, + GCMAppHandler* handler) override; + void RemoveAppHandler(const std::string& app_id) override; + void AddConnectionObserver(GCMConnectionObserver* observer) override; + void RemoveConnectionObserver(GCMConnectionObserver* observer) override; + GCMClient* GetGCMClientForTesting() const override; + bool IsStarted() const override; + bool IsConnected() const override; + void GetGCMStatistics(GetGCMStatisticsCallback callback, + ClearActivityLogs clear_logs) override; + void SetGCMRecording(const GCMStatisticsRecordingCallback& callback, + bool recording) override; + void SetAccountTokens( + const std::vector& account_tokens) override; + void UpdateAccountMapping(const AccountMapping& account_mapping) override; + void RemoveAccountMapping(const CoreAccountId& account_id) override; + base::Time GetLastTokenFetchTime() override; + void SetLastTokenFetchTime(const base::Time& time) override; + InstanceIDHandler* GetInstanceIDHandlerInternal() override; + void AddHeartbeatInterval(const std::string& scope, int interval_ms) override; + void RemoveHeartbeatInterval(const std::string& scope) override; + + protected: + // GCMDriver implementation: + GCMClient::Result EnsureStarted(GCMClient::StartMode start_mode) override; + void RegisterImpl(const std::string& app_id, + const std::vector& sender_ids) override; + void UnregisterImpl(const std::string& app_id) override; + void SendImpl(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message) override; + void RecordDecryptionFailure(const std::string& app_id, + GCMDecryptionResult result) override; + + // InstanceIDHandler implementation: + void GetToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + base::TimeDelta time_to_live, + GetTokenCallback callback) override; + void ValidateToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + const std::string& token, + ValidateTokenCallback callback) override; + void DeleteToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + DeleteTokenCallback callback) override; + void AddInstanceIDData(const std::string& app_id, + const std::string& instance_id, + const std::string& extra_data) override; + void RemoveInstanceIDData(const std::string& app_id) override; + void GetInstanceIDData(const std::string& app_id, + GetInstanceIDDataCallback callback) override; + + private: + class IOWorker; + + typedef std::tuple TokenTuple; + struct TokenTupleComparer { + bool operator()(const TokenTuple& a, const TokenTuple& b) const; + }; + + void DoValidateRegistration(scoped_refptr registration_info, + const std::string& registration_id, + ValidateRegistrationCallback callback); + + // Stops the GCM service. It can be restarted by calling EnsureStarted again. + void Stop(); + + // Remove cached data when GCM service is stopped. + void RemoveCachedData(); + + void DoRegister(const std::string& app_id, + const std::vector& sender_ids); + void DoUnregister(const std::string& app_id); + void DoSend(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message); + void DoAddInstanceIDData(const std::string& app_id, + const std::string& instance_id, + const std::string& extra_data); + void DoRemoveInstanceIDData(const std::string& app_id); + void DoGetInstanceIDData(const std::string& app_id); + void DoGetToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + base::TimeDelta time_to_live); + void DoDeleteToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope); + + // Callbacks posted from IO thread to UI thread. + void MessageReceived(const std::string& app_id, + const IncomingMessage& message); + void MessagesDeleted(const std::string& app_id); + void MessageSendError(const std::string& app_id, + const GCMClient::SendErrorDetails& send_error_details); + void SendAcknowledged(const std::string& app_id, + const std::string& message_id); + void GCMClientReady(const std::vector& account_mappings, + const base::Time& last_token_fetch_time); + void OnConnected(const net::IPEndPoint& ip_endpoint); + void OnDisconnected(); + void OnStoreReset(); + void OnActivityRecorded(const GCMClient::GCMStatistics& stats); + + void GetInstanceIDDataFinished(const std::string& app_id, + const std::string& instance_id, + const std::string& extra_data); + void GetTokenFinished(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + const std::string& token, + GCMClient::Result result); + void DeleteTokenFinished(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + GCMClient::Result result); + + // Flag to indicate whether the user is signed in to a GAIA account. + bool signed_in_; + + // Flag to indicate if GCM is started. + bool gcm_started_; + + // Flag to indicate the last known state of the GCM client. Because this + // flag lives on the UI thread, while the GCM client lives on the IO thread, + // it may be out of date while connection changes are happening. + bool connected_; + + // List of observers to notify when connection state changes. + base::ObserverList::Unchecked + connection_observer_list_; + + // Account mapper. Only works when user is signed in. + std::unique_ptr account_mapper_; + + // Time of last token fetching. + base::Time last_token_fetch_time_; + + scoped_refptr ui_thread_; + scoped_refptr io_thread_; + + std::unique_ptr delayed_task_controller_; + + // For all the work occurring on the IO thread. Must be destroyed on the IO + // thread. + std::unique_ptr io_worker_; + + // Callback for SetGCMRecording. + GCMStatisticsRecordingCallback gcm_statistics_recording_callback_; + + // Callbacks for GetInstanceIDData. Initializing InstanceID is asynchronous, + // which leads to a race condition when recreating an InstanceID before such + // initialization has finished, causing multiple callbacks to be in flight. + // Expecting all InstanceID users to care for that is fragile and complicated, + // so allow for a queue of callbacks to be stored here instead. + // + // Note that other InstanceID callbacks don't have this concern, as they all + // wait for initialization of the InstanceID instance to have completed. + std::map> + get_instance_id_data_callbacks_; + + // Callbacks for GetToken/DeleteToken. + std::map + get_token_callbacks_; + std::map + delete_token_callbacks_; + + // Used to pass a weak pointer to the IO worker. + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_GCM_DRIVER_DESKTOP_H_ diff --git a/chromium/components/gcm_driver/gcm_driver_desktop_unittest.cc b/chromium/components/gcm_driver/gcm_driver_desktop_unittest.cc new file mode 100644 index 00000000000..e761bcb7f22 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_driver_desktop_unittest.cc @@ -0,0 +1,1139 @@ +// 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 "components/gcm_driver/gcm_driver_desktop.h" + +#include + +#include + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/files/scoped_temp_dir.h" +#include "base/location.h" +#include "base/run_loop.h" +#include "base/strings/string_util.h" +#include "base/task/current_thread.h" +#include "base/test/task_environment.h" +#include "base/test/test_simple_task_runner.h" +#include "base/threading/thread.h" +#include "base/threading/thread_task_runner_handle.h" +#include "components/gcm_driver/crypto/gcm_encryption_provider.h" +#include "components/gcm_driver/fake_gcm_app_handler.h" +#include "components/gcm_driver/fake_gcm_client.h" +#include "components/gcm_driver/fake_gcm_client_factory.h" +#include "components/gcm_driver/gcm_app_handler.h" +#include "components/gcm_driver/gcm_client_factory.h" +#include "components/gcm_driver/gcm_connection_observer.h" +#include "components/prefs/pref_registry_simple.h" +#include "components/prefs/testing_pref_service.h" +#include "net/url_request/url_request_context_getter.h" +#include "net/url_request/url_request_test_util.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" +#include "services/network/test/test_network_connection_tracker.h" +#include "services/network/test/test_url_loader_factory.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { + +namespace { + +const char kTestAppID1[] = "TestApp1"; +const char kTestAppID2[] = "TestApp2"; +const char kUserID1[] = "user1"; +const char kScope[] = "GCM"; +const char kInstanceID1[] = "IID1"; +const char kInstanceID2[] = "IID2"; + +class FakeGCMConnectionObserver : public GCMConnectionObserver { + public: + FakeGCMConnectionObserver(); + ~FakeGCMConnectionObserver() override; + + // gcm::GCMConnectionObserver implementation: + void OnConnected(const net::IPEndPoint& ip_endpoint) override; + void OnDisconnected() override; + + bool connected() const { return connected_; } + + private: + bool connected_; +}; + +FakeGCMConnectionObserver::FakeGCMConnectionObserver() : connected_(false) { +} + +FakeGCMConnectionObserver::~FakeGCMConnectionObserver() { +} + +void FakeGCMConnectionObserver::OnConnected( + const net::IPEndPoint& ip_endpoint) { + connected_ = true; +} + +void FakeGCMConnectionObserver::OnDisconnected() { + connected_ = false; +} + +void PumpCurrentLoop() { + base::RunLoop(base::RunLoop::Type::kNestableTasksAllowed).RunUntilIdle(); +} + +void PumpUILoop() { + PumpCurrentLoop(); +} + +std::vector ToSenderList(const std::string& sender_ids) { + return base::SplitString( + sender_ids, ",", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY); +} + +} // namespace + +class GCMDriverTest : public testing::Test { + public: + enum WaitToFinish { + DO_NOT_WAIT, + WAIT + }; + + GCMDriverTest(); + + GCMDriverTest(const GCMDriverTest&) = delete; + GCMDriverTest& operator=(const GCMDriverTest&) = delete; + + ~GCMDriverTest() override; + + // testing::Test: + void SetUp() override; + void TearDown() override; + + GCMDriverDesktop* driver() { return driver_.get(); } + FakeGCMAppHandler* gcm_app_handler() { return gcm_app_handler_.get(); } + FakeGCMConnectionObserver* gcm_connection_observer() { + return gcm_connection_observer_.get(); + } + const std::string& registration_id() const { return registration_id_; } + GCMClient::Result registration_result() const { return registration_result_; } + const std::string& send_message_id() const { return send_message_id_; } + GCMClient::Result send_result() const { return send_result_; } + GCMClient::Result unregistration_result() const { + return unregistration_result_; + } + const std::string& p256dh() const { return p256dh_; } + const std::string& auth_secret() const { return auth_secret_; } + + void PumpIOLoop(); + + void ClearResults(); + + bool HasAppHandlers() const; + FakeGCMClient* GetGCMClient(); + + void CreateDriver(); + void ShutdownDriver(); + void AddAppHandlers(); + void RemoveAppHandlers(); + + void Register(const std::string& app_id, + const std::vector& sender_ids, + WaitToFinish wait_to_finish); + void Send(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message, + WaitToFinish wait_to_finish); + void GetEncryptionInfo(const std::string& app_id, + WaitToFinish wait_to_finish); + void Unregister(const std::string& app_id, WaitToFinish wait_to_finish); + + void WaitForAsyncOperation(); + + void RegisterCompleted(const std::string& registration_id, + GCMClient::Result result); + void SendCompleted(const std::string& message_id, GCMClient::Result result); + void GetEncryptionInfoCompleted(std::string p256dh, std::string auth_secret); + void UnregisterCompleted(GCMClient::Result result); + + void AsyncOperationCompleted() { + if (async_operation_completed_callback_) + std::move(async_operation_completed_callback_).Run(); + } + void set_async_operation_completed_callback(base::OnceClosure callback) { + async_operation_completed_callback_ = std::move(callback); + } + + private: + base::ScopedTempDir temp_dir_; + TestingPrefServiceSimple prefs_; + base::test::SingleThreadTaskEnvironment task_environment_{ + base::test::SingleThreadTaskEnvironment::MainThreadType::UI}; + base::Thread io_thread_; + network::TestURLLoaderFactory test_url_loader_factory_; + + std::unique_ptr driver_; + std::unique_ptr gcm_app_handler_; + std::unique_ptr gcm_connection_observer_; + + base::OnceClosure async_operation_completed_callback_; + + std::string registration_id_; + GCMClient::Result registration_result_; + std::string send_message_id_; + GCMClient::Result send_result_; + GCMClient::Result unregistration_result_; + std::string p256dh_; + std::string auth_secret_; +}; + +GCMDriverTest::GCMDriverTest() + : io_thread_("IOThread"), + registration_result_(GCMClient::UNKNOWN_ERROR), + send_result_(GCMClient::UNKNOWN_ERROR), + unregistration_result_(GCMClient::UNKNOWN_ERROR) {} + +GCMDriverTest::~GCMDriverTest() { +} + +void GCMDriverTest::SetUp() { + io_thread_.Start(); + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); +} + +void GCMDriverTest::TearDown() { + if (!driver_) + return; + + ShutdownDriver(); + driver_.reset(); + PumpIOLoop(); + + io_thread_.Stop(); + task_environment_.RunUntilIdle(); + ASSERT_TRUE(temp_dir_.Delete()); +} + +void GCMDriverTest::PumpIOLoop() { + base::RunLoop run_loop; + io_thread_.task_runner()->PostTaskAndReply( + FROM_HERE, base::BindOnce(&PumpCurrentLoop), run_loop.QuitClosure()); + run_loop.Run(); +} + +void GCMDriverTest::ClearResults() { + registration_id_.clear(); + registration_result_ = GCMClient::UNKNOWN_ERROR; + + send_message_id_.clear(); + send_result_ = GCMClient::UNKNOWN_ERROR; + + unregistration_result_ = GCMClient::UNKNOWN_ERROR; +} + +bool GCMDriverTest::HasAppHandlers() const { + return !driver_->app_handlers().empty(); +} + +FakeGCMClient* GCMDriverTest::GetGCMClient() { + return static_cast(driver_->GetGCMClientForTesting()); +} + +void GCMDriverTest::CreateDriver() { + scoped_refptr request_context = + new net::TestURLRequestContextGetter(io_thread_.task_runner()); + GCMClient::ChromeBuildInfo chrome_build_info; + chrome_build_info.product_category_for_subtypes = "com.chrome.macosx"; + driver_ = std::make_unique( + std::unique_ptr(new FakeGCMClientFactory( + base::ThreadTaskRunnerHandle::Get(), io_thread_.task_runner())), + chrome_build_info, &prefs_, temp_dir_.GetPath(), + /*remove_account_mappings_with_email_key=*/true, base::DoNothing(), + base::MakeRefCounted( + &test_url_loader_factory_), + network::TestNetworkConnectionTracker::GetInstance(), + base::ThreadTaskRunnerHandle::Get(), io_thread_.task_runner(), + task_environment_.GetMainThreadTaskRunner()); + + gcm_app_handler_ = std::make_unique(); + gcm_connection_observer_ = std::make_unique(); + + driver_->AddConnectionObserver(gcm_connection_observer_.get()); +} + +void GCMDriverTest::ShutdownDriver() { + if (gcm_connection_observer()) + driver()->RemoveConnectionObserver(gcm_connection_observer()); + driver()->Shutdown(); +} + +void GCMDriverTest::AddAppHandlers() { + driver_->AddAppHandler(kTestAppID1, gcm_app_handler_.get()); + driver_->AddAppHandler(kTestAppID2, gcm_app_handler_.get()); +} + +void GCMDriverTest::RemoveAppHandlers() { + driver_->RemoveAppHandler(kTestAppID1); + driver_->RemoveAppHandler(kTestAppID2); +} + +void GCMDriverTest::Register(const std::string& app_id, + const std::vector& sender_ids, + WaitToFinish wait_to_finish) { + base::RunLoop run_loop; + async_operation_completed_callback_ = run_loop.QuitClosure(); + driver_->Register(app_id, sender_ids, + base::BindOnce(&GCMDriverTest::RegisterCompleted, + base::Unretained(this))); + if (wait_to_finish == WAIT) + run_loop.Run(); +} + +void GCMDriverTest::Send(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message, + WaitToFinish wait_to_finish) { + base::RunLoop run_loop; + async_operation_completed_callback_ = run_loop.QuitClosure(); + driver_->Send( + app_id, receiver_id, message, + base::BindOnce(&GCMDriverTest::SendCompleted, base::Unretained(this))); + if (wait_to_finish == WAIT) + run_loop.Run(); +} + +void GCMDriverTest::GetEncryptionInfo(const std::string& app_id, + WaitToFinish wait_to_finish) { + base::RunLoop run_loop; + async_operation_completed_callback_ = run_loop.QuitClosure(); + driver_->GetEncryptionInfo( + app_id, base::BindOnce(&GCMDriverTest::GetEncryptionInfoCompleted, + base::Unretained(this))); + if (wait_to_finish == WAIT) + run_loop.Run(); +} + +void GCMDriverTest::Unregister(const std::string& app_id, + WaitToFinish wait_to_finish) { + base::RunLoop run_loop; + async_operation_completed_callback_ = run_loop.QuitClosure(); + driver_->Unregister(app_id, + base::BindOnce(&GCMDriverTest::UnregisterCompleted, + base::Unretained(this))); + if (wait_to_finish == WAIT) + run_loop.Run(); +} + +void GCMDriverTest::WaitForAsyncOperation() { + base::RunLoop run_loop; + async_operation_completed_callback_ = run_loop.QuitClosure(); + run_loop.Run(); +} + +void GCMDriverTest::RegisterCompleted(const std::string& registration_id, + GCMClient::Result result) { + registration_id_ = registration_id; + registration_result_ = result; + AsyncOperationCompleted(); +} + +void GCMDriverTest::SendCompleted(const std::string& message_id, + GCMClient::Result result) { + send_message_id_ = message_id; + send_result_ = result; + AsyncOperationCompleted(); +} + +void GCMDriverTest::GetEncryptionInfoCompleted(std::string p256dh, + std::string auth_secret) { + p256dh_ = std::move(p256dh); + auth_secret_ = std::move(auth_secret); + AsyncOperationCompleted(); +} + +void GCMDriverTest::UnregisterCompleted(GCMClient::Result result) { + unregistration_result_ = result; + AsyncOperationCompleted(); +} + +TEST_F(GCMDriverTest, Create) { + // Create GCMDriver first. By default GCM is set to delay start. + CreateDriver(); + EXPECT_FALSE(driver()->IsStarted()); + + // Adding an app handler will not start GCM. + AddAppHandlers(); + PumpIOLoop(); + PumpUILoop(); + EXPECT_FALSE(driver()->IsStarted()); + EXPECT_FALSE(driver()->IsConnected()); + EXPECT_FALSE(gcm_connection_observer()->connected()); + + // The GCM registration will kick off the GCM. + Register(kTestAppID1, ToSenderList("sender"), GCMDriverTest::WAIT); + EXPECT_TRUE(driver()->IsStarted()); + EXPECT_TRUE(driver()->IsConnected()); + EXPECT_TRUE(gcm_connection_observer()->connected()); +} + +TEST_F(GCMDriverTest, Shutdown) { + CreateDriver(); + EXPECT_FALSE(HasAppHandlers()); + + AddAppHandlers(); + EXPECT_TRUE(HasAppHandlers()); + + ShutdownDriver(); + EXPECT_FALSE(HasAppHandlers()); + EXPECT_FALSE(driver()->IsConnected()); + EXPECT_FALSE(gcm_connection_observer()->connected()); +} + +TEST_F(GCMDriverTest, StartOrStopGCMOnDemand) { + CreateDriver(); + PumpIOLoop(); + PumpUILoop(); + EXPECT_FALSE(driver()->IsStarted()); + + // Adding an app handler will not start GCM. + driver()->AddAppHandler(kTestAppID1, gcm_app_handler()); + PumpIOLoop(); + PumpUILoop(); + EXPECT_FALSE(driver()->IsStarted()); + + // The GCM registration will kick off the GCM. + Register(kTestAppID1, ToSenderList("sender"), GCMDriverTest::WAIT); + EXPECT_TRUE(driver()->IsStarted()); + + // Add another app handler. + driver()->AddAppHandler(kTestAppID2, gcm_app_handler()); + PumpIOLoop(); + PumpUILoop(); + EXPECT_TRUE(driver()->IsStarted()); + + // GCMClient remains active after one app handler is gone. + driver()->RemoveAppHandler(kTestAppID1); + PumpIOLoop(); + PumpUILoop(); + EXPECT_TRUE(driver()->IsStarted()); + + // GCMClient should be stopped after the last app handler is gone. + driver()->RemoveAppHandler(kTestAppID2); + PumpIOLoop(); + PumpUILoop(); + EXPECT_FALSE(driver()->IsStarted()); + + // GCMClient is restarted after an app handler has been added. + driver()->AddAppHandler(kTestAppID2, gcm_app_handler()); + PumpIOLoop(); + PumpUILoop(); + EXPECT_TRUE(driver()->IsStarted()); +} + +TEST_F(GCMDriverTest, RegisterFailed) { + std::vector sender_ids; + sender_ids.push_back("sender1"); + + CreateDriver(); + + // Registration fails when the no app handler is added. + Register(kTestAppID1, sender_ids, GCMDriverTest::WAIT); + EXPECT_TRUE(registration_id().empty()); + EXPECT_EQ(GCMClient::UNKNOWN_ERROR, registration_result()); +} + +TEST_F(GCMDriverTest, UnregisterFailed) { + CreateDriver(); + + // Unregistration fails when the no app handler is added. + Unregister(kTestAppID1, GCMDriverTest::WAIT); + EXPECT_EQ(GCMClient::UNKNOWN_ERROR, unregistration_result()); +} + +TEST_F(GCMDriverTest, SendFailed) { + OutgoingMessage message; + message.id = "1"; + message.data["key1"] = "value1"; + + CreateDriver(); + + // Sending fails when the no app handler is added. + Send(kTestAppID1, kUserID1, message, GCMDriverTest::WAIT); + EXPECT_TRUE(send_message_id().empty()); + EXPECT_EQ(GCMClient::UNKNOWN_ERROR, send_result()); +} + +TEST_F(GCMDriverTest, DISABLED_GCMClientNotReadyBeforeRegistration) { + CreateDriver(); + PumpIOLoop(); + PumpUILoop(); + + // Make GCMClient not ready until PerformDelayedStart is called. + GetGCMClient()->set_start_mode_overridding( + FakeGCMClient::FORCE_TO_ALWAYS_DELAY_START_GCM); + + AddAppHandlers(); + + // The registration is on hold until GCMClient is ready. + std::vector sender_ids; + sender_ids.push_back("sender1"); + Register(kTestAppID1, + sender_ids, + GCMDriverTest::DO_NOT_WAIT); + PumpIOLoop(); + PumpUILoop(); + EXPECT_TRUE(registration_id().empty()); + EXPECT_EQ(GCMClient::UNKNOWN_ERROR, registration_result()); + + // Register operation will be invoked after GCMClient becomes ready. + GetGCMClient()->PerformDelayedStart(); + WaitForAsyncOperation(); + EXPECT_FALSE(registration_id().empty()); + EXPECT_EQ(GCMClient::SUCCESS, registration_result()); +} + +TEST_F(GCMDriverTest, GCMClientNotReadyBeforeSending) { + CreateDriver(); + PumpIOLoop(); + PumpUILoop(); + + // Make GCMClient not ready until PerformDelayedStart is called. + GetGCMClient()->set_start_mode_overridding( + FakeGCMClient::FORCE_TO_ALWAYS_DELAY_START_GCM); + + AddAppHandlers(); + + // The sending is on hold until GCMClient is ready. + OutgoingMessage message; + message.id = "1"; + message.data["key1"] = "value1"; + message.data["key2"] = "value2"; + Send(kTestAppID1, kUserID1, message, GCMDriverTest::DO_NOT_WAIT); + PumpIOLoop(); + PumpUILoop(); + + EXPECT_TRUE(send_message_id().empty()); + EXPECT_EQ(GCMClient::UNKNOWN_ERROR, send_result()); + + // Send operation will be invoked after GCMClient becomes ready. + GetGCMClient()->PerformDelayedStart(); + WaitForAsyncOperation(); + EXPECT_EQ(message.id, send_message_id()); + EXPECT_EQ(GCMClient::SUCCESS, send_result()); +} + +// Tests a single instance of GCMDriver. +class GCMDriverFunctionalTest : public GCMDriverTest { + public: + GCMDriverFunctionalTest(); + + GCMDriverFunctionalTest(const GCMDriverFunctionalTest&) = delete; + GCMDriverFunctionalTest& operator=(const GCMDriverFunctionalTest&) = delete; + + ~GCMDriverFunctionalTest() override; + + // GCMDriverTest: + void SetUp() override; +}; + +GCMDriverFunctionalTest::GCMDriverFunctionalTest() { +} + +GCMDriverFunctionalTest::~GCMDriverFunctionalTest() { +} + +void GCMDriverFunctionalTest::SetUp() { + GCMDriverTest::SetUp(); + + CreateDriver(); + AddAppHandlers(); + PumpIOLoop(); + PumpUILoop(); +} + +TEST_F(GCMDriverFunctionalTest, DISABLED_Register) { + std::vector sender_ids; + sender_ids.push_back("sender1"); + Register(kTestAppID1, sender_ids, GCMDriverTest::WAIT); + const std::string expected_registration_id = + FakeGCMClient::GenerateGCMRegistrationID(sender_ids); + + EXPECT_EQ(expected_registration_id, registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, registration_result()); +} + +// This test is flaky, see https://crbug.com/1010462 +TEST_F(GCMDriverFunctionalTest, DISABLED_RegisterError) { + std::vector sender_ids; + sender_ids.push_back("sender1@error"); + Register(kTestAppID1, sender_ids, GCMDriverTest::WAIT); + + EXPECT_TRUE(registration_id().empty()); + EXPECT_NE(GCMClient::SUCCESS, registration_result()); +} + +// This test is flaky, see https://crbug.com/1010462 +TEST_F(GCMDriverFunctionalTest, DISABLED_RegisterAgainWithSameSenderIDs) { + std::vector sender_ids; + sender_ids.push_back("sender1"); + sender_ids.push_back("sender2"); + Register(kTestAppID1, sender_ids, GCMDriverTest::WAIT); + const std::string expected_registration_id = + FakeGCMClient::GenerateGCMRegistrationID(sender_ids); + + EXPECT_EQ(expected_registration_id, registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, registration_result()); + + // Clears the results the would be set by the Register callback in preparation + // to call register 2nd time. + ClearResults(); + + // Calling register 2nd time with the same set of sender IDs but different + // ordering will get back the same registration ID. + std::vector another_sender_ids; + another_sender_ids.push_back("sender2"); + another_sender_ids.push_back("sender1"); + Register(kTestAppID1, another_sender_ids, GCMDriverTest::WAIT); + + EXPECT_EQ(expected_registration_id, registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, registration_result()); +} + +// This test is flaky, see https://crbug.com/1010462 +TEST_F(GCMDriverFunctionalTest, DISABLED_RegisterAgainWithDifferentSenderIDs) { + std::vector sender_ids; + sender_ids.push_back("sender1"); + Register(kTestAppID1, sender_ids, GCMDriverTest::WAIT); + const std::string expected_registration_id = + FakeGCMClient::GenerateGCMRegistrationID(sender_ids); + + EXPECT_EQ(expected_registration_id, registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, registration_result()); + + // Make sender IDs different. + sender_ids.push_back("sender2"); + const std::string expected_registration_id2 = + FakeGCMClient::GenerateGCMRegistrationID(sender_ids); + + // Calling register 2nd time with the different sender IDs will get back a new + // registration ID. + Register(kTestAppID1, sender_ids, GCMDriverTest::WAIT); + EXPECT_EQ(expected_registration_id2, registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, registration_result()); +} + +TEST_F(GCMDriverFunctionalTest, UnregisterExplicitly) { + std::vector sender_ids; + sender_ids.push_back("sender1"); + Register(kTestAppID1, sender_ids, GCMDriverTest::WAIT); + + EXPECT_FALSE(registration_id().empty()); + EXPECT_EQ(GCMClient::SUCCESS, registration_result()); + + Unregister(kTestAppID1, GCMDriverTest::WAIT); + + EXPECT_EQ(GCMClient::SUCCESS, unregistration_result()); +} + +// TODO(crbug.com/1009185): Test is failing on ASan build. +#if defined(ADDRESS_SANITIZER) +TEST_F(GCMDriverFunctionalTest, DISABLED_UnregisterRemovesEncryptionInfo) { +#else +TEST_F(GCMDriverFunctionalTest, UnregisterRemovesEncryptionInfo) { +#endif + std::vector sender_ids; + sender_ids.push_back("sender1"); + Register(kTestAppID1, sender_ids, GCMDriverTest::WAIT); + + EXPECT_FALSE(registration_id().empty()); + EXPECT_EQ(GCMClient::SUCCESS, registration_result()); + + GetEncryptionInfo(kTestAppID1, GCMDriverTest::WAIT); + + EXPECT_FALSE(p256dh().empty()); + EXPECT_FALSE(auth_secret().empty()); + + const std::string app_p256dh = p256dh(); + const std::string app_auth_secret = auth_secret(); + + GetEncryptionInfo(kTestAppID1, GCMDriverTest::WAIT); + + EXPECT_EQ(app_p256dh, p256dh()); + EXPECT_EQ(app_auth_secret, auth_secret()); + + Unregister(kTestAppID1, GCMDriverTest::WAIT); + + EXPECT_EQ(GCMClient::SUCCESS, unregistration_result()); + + GetEncryptionInfo(kTestAppID1, GCMDriverTest::WAIT); + + // The GCMKeyStore eagerly creates new keying material for registrations that + // don't have any associated with them, so the most appropriate check to do is + // to verify that the returned material is different from before. + + EXPECT_NE(app_p256dh, p256dh()); + EXPECT_NE(app_auth_secret, auth_secret()); +} + +TEST_F(GCMDriverFunctionalTest, DISABLED_UnregisterWhenAsyncOperationPending) { + std::vector sender_ids; + sender_ids.push_back("sender1"); + // First start registration without waiting for it to complete. + Register(kTestAppID1, sender_ids, GCMDriverTest::DO_NOT_WAIT); + + // Test that unregistration fails with async operation pending when there is a + // registration already in progress. + Unregister(kTestAppID1, GCMDriverTest::WAIT); + EXPECT_EQ(GCMClient::ASYNC_OPERATION_PENDING, + unregistration_result()); + + // Complete the unregistration. + WaitForAsyncOperation(); + EXPECT_EQ(GCMClient::SUCCESS, registration_result()); + + // Start unregistration without waiting for it to complete. This time no async + // operation is pending. + Unregister(kTestAppID1, GCMDriverTest::DO_NOT_WAIT); + + // Test that unregistration fails with async operation pending when there is + // an unregistration already in progress. + Unregister(kTestAppID1, GCMDriverTest::WAIT); + EXPECT_EQ(GCMClient::ASYNC_OPERATION_PENDING, + unregistration_result()); + ClearResults(); + + // Complete unregistration. + WaitForAsyncOperation(); + EXPECT_EQ(GCMClient::SUCCESS, unregistration_result()); +} + +TEST_F(GCMDriverFunctionalTest, RegisterWhenAsyncOperationPending) { + std::vector sender_ids; + sender_ids.push_back("sender1"); + // First start registration without waiting for it to complete. + Register(kTestAppID1, sender_ids, GCMDriverTest::DO_NOT_WAIT); + + // Test that registration fails with async operation pending when there is a + // registration already in progress. + Register(kTestAppID1, sender_ids, GCMDriverTest::WAIT); + EXPECT_EQ(GCMClient::ASYNC_OPERATION_PENDING, + registration_result()); + ClearResults(); + + // Complete the registration. + WaitForAsyncOperation(); + EXPECT_EQ(GCMClient::SUCCESS, registration_result()); +} + +// This test is flaky, see https://crbug.com/1010462 +TEST_F(GCMDriverFunctionalTest, DISABLED_RegisterAfterUnfinishedUnregister) { + // Register and wait for it to complete. + std::vector sender_ids; + sender_ids.push_back("sender1"); + Register(kTestAppID1, sender_ids, GCMDriverTest::WAIT); + EXPECT_EQ(GCMClient::SUCCESS, registration_result()); + EXPECT_EQ(FakeGCMClient::GenerateGCMRegistrationID(sender_ids), + registration_id()); + + // Clears the results the would be set by the Register callback in preparation + // to call register 2nd time. + ClearResults(); + + // Start unregistration without waiting for it to complete. + Unregister(kTestAppID1, GCMDriverTest::DO_NOT_WAIT); + + // Register immediately after unregistration is not completed. + sender_ids.push_back("sender2"); + Register(kTestAppID1, sender_ids, GCMDriverTest::WAIT); + + // We need one more waiting since the waiting in Register is indeed for + // uncompleted Unregister. + WaitForAsyncOperation(); + EXPECT_EQ(GCMClient::SUCCESS, registration_result()); + EXPECT_EQ(FakeGCMClient::GenerateGCMRegistrationID(sender_ids), + registration_id()); +} + +TEST_F(GCMDriverFunctionalTest, Send) { + OutgoingMessage message; + message.id = "1@ack"; + message.data["key1"] = "value1"; + message.data["key2"] = "value2"; + Send(kTestAppID1, kUserID1, message, GCMDriverTest::WAIT); + + EXPECT_EQ(message.id, send_message_id()); + EXPECT_EQ(GCMClient::SUCCESS, send_result()); + + gcm_app_handler()->WaitForNotification(); + EXPECT_EQ(message.id, gcm_app_handler()->acked_message_id()); + EXPECT_EQ(kTestAppID1, gcm_app_handler()->app_id()); +} + +TEST_F(GCMDriverFunctionalTest, SendError) { + OutgoingMessage message; + // Embedding error in id will tell the mock to simulate the send error. + message.id = "1@error"; + message.data["key1"] = "value1"; + message.data["key2"] = "value2"; + Send(kTestAppID1, kUserID1, message, GCMDriverTest::WAIT); + + EXPECT_EQ(message.id, send_message_id()); + EXPECT_EQ(GCMClient::SUCCESS, send_result()); + + // Wait for the send error. + gcm_app_handler()->WaitForNotification(); + EXPECT_EQ(FakeGCMAppHandler::SEND_ERROR_EVENT, + gcm_app_handler()->received_event()); + EXPECT_EQ(kTestAppID1, gcm_app_handler()->app_id()); + EXPECT_EQ(message.id, + gcm_app_handler()->send_error_details().message_id); + EXPECT_NE(GCMClient::SUCCESS, + gcm_app_handler()->send_error_details().result); + EXPECT_EQ(message.data, + gcm_app_handler()->send_error_details().additional_data); +} + +// This test is flaky, see https://crbug.com/1010462 +TEST_F(GCMDriverFunctionalTest, DISABLED_MessageReceived) { + // GCM registration has to be performed otherwise GCM will not be started. + Register(kTestAppID1, ToSenderList("sender"), GCMDriverTest::WAIT); + + IncomingMessage message; + message.data["key1"] = "value1"; + message.data["key2"] = "value2"; + message.sender_id = "sender"; + GetGCMClient()->ReceiveMessage(kTestAppID1, message); + gcm_app_handler()->WaitForNotification(); + EXPECT_EQ(FakeGCMAppHandler::MESSAGE_EVENT, + gcm_app_handler()->received_event()); + EXPECT_EQ(kTestAppID1, gcm_app_handler()->app_id()); + EXPECT_EQ(message.data, gcm_app_handler()->message().data); + EXPECT_TRUE(gcm_app_handler()->message().collapse_key.empty()); + EXPECT_EQ(message.sender_id, gcm_app_handler()->message().sender_id); +} + +// This test is flaky, see https://crbug.com/1010462 +TEST_F(GCMDriverFunctionalTest, DISABLED_MessageWithCollapseKeyReceived) { + // GCM registration has to be performed otherwise GCM will not be started. + Register(kTestAppID1, ToSenderList("sender"), GCMDriverTest::WAIT); + + IncomingMessage message; + message.data["key1"] = "value1"; + message.collapse_key = "collapse_key_value"; + message.sender_id = "sender"; + GetGCMClient()->ReceiveMessage(kTestAppID1, message); + gcm_app_handler()->WaitForNotification(); + EXPECT_EQ(FakeGCMAppHandler::MESSAGE_EVENT, + gcm_app_handler()->received_event()); + EXPECT_EQ(kTestAppID1, gcm_app_handler()->app_id()); + EXPECT_EQ(message.data, gcm_app_handler()->message().data); + EXPECT_EQ(message.collapse_key, + gcm_app_handler()->message().collapse_key); +} + +TEST_F(GCMDriverFunctionalTest, EncryptedMessageReceivedError) { + // GCM registration has to be performed otherwise GCM will not be started. + Register(kTestAppID1, ToSenderList("sender"), GCMDriverTest::WAIT); + + IncomingMessage message; + + // All required information to trigger the encryption path, but with an + // invalid Crypto-Key header value to trigger an error. + message.data["encryption"] = "salt=ysyxqlYTgE0WvcZrmHbUbg"; + message.data["crypto-key"] = "hey=thereisnopublickey"; + message.sender_id = "sender"; + message.raw_data = "foobar"; + + GetGCMClient()->SetRecording(true); + GetGCMClient()->ReceiveMessage(kTestAppID1, message); + + PumpIOLoop(); + PumpUILoop(); + PumpIOLoop(); + + EXPECT_EQ(FakeGCMAppHandler::DECRYPTION_FAILED_EVENT, + gcm_app_handler()->received_event()); + + GCMClient::GCMStatistics statistics = GetGCMClient()->GetStatistics(); + EXPECT_TRUE(statistics.is_recording); + EXPECT_EQ( + 1u, statistics.recorded_activities.decryption_failure_activities.size()); +} + +TEST_F(GCMDriverFunctionalTest, MessagesDeleted) { + // GCM registration has to be performed otherwise GCM will not be started. + Register(kTestAppID1, ToSenderList("sender"), GCMDriverTest::WAIT); + + GetGCMClient()->DeleteMessages(kTestAppID1); + gcm_app_handler()->WaitForNotification(); + EXPECT_EQ(FakeGCMAppHandler::MESSAGES_DELETED_EVENT, + gcm_app_handler()->received_event()); + EXPECT_EQ(kTestAppID1, gcm_app_handler()->app_id()); +} + +TEST_F(GCMDriverFunctionalTest, LastTokenFetchTime) { + // GCM registration has to be performed otherwise GCM will not be started. + Register(kTestAppID1, ToSenderList("sender"), GCMDriverTest::WAIT); + + EXPECT_EQ(base::Time(), driver()->GetLastTokenFetchTime()); + base::Time fetch_time = base::Time::Now(); + driver()->SetLastTokenFetchTime(fetch_time); + EXPECT_EQ(fetch_time, driver()->GetLastTokenFetchTime()); +} + +class GCMDriverInstanceIDTest : public GCMDriverTest { + public: + GCMDriverInstanceIDTest(); + + GCMDriverInstanceIDTest(const GCMDriverInstanceIDTest&) = delete; + GCMDriverInstanceIDTest& operator=(const GCMDriverInstanceIDTest&) = delete; + + ~GCMDriverInstanceIDTest() override; + + void GetReady(); + void GetInstanceID(const std::string& app_id, WaitToFinish wait_to_finish); + void GetInstanceIDDataCompleted(const std::string& instance_id, + const std::string& extra_data); + void GetToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + WaitToFinish wait_to_finish); + void DeleteToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + WaitToFinish wait_to_finish); + void AddInstanceIDData(const std::string& app_id, + const std::string& instance_id, + const std::string& extra_data); + void RemoveInstanceIDData(const std::string& app_id); + + std::string instance_id() const { return instance_id_; } + std::string extra_data() const { return extra_data_; } + + int instance_id_resolved_counter() const { + return instance_id_resolved_counter_; + } + + private: + std::string instance_id_; + std::string extra_data_; + + int instance_id_resolved_counter_ = 0; +}; + +GCMDriverInstanceIDTest::GCMDriverInstanceIDTest() { +} + +GCMDriverInstanceIDTest::~GCMDriverInstanceIDTest() { +} + +void GCMDriverInstanceIDTest::GetReady() { + CreateDriver(); + AddAppHandlers(); + PumpIOLoop(); + PumpUILoop(); +} + +void GCMDriverInstanceIDTest::GetInstanceID(const std::string& app_id, + WaitToFinish wait_to_finish) { + base::RunLoop run_loop; + set_async_operation_completed_callback(run_loop.QuitClosure()); + driver()->GetInstanceIDHandlerInternal()->GetInstanceIDData( + app_id, + base::BindOnce(&GCMDriverInstanceIDTest::GetInstanceIDDataCompleted, + base::Unretained(this))); + if (wait_to_finish == WAIT) + run_loop.Run(); +} + +void GCMDriverInstanceIDTest::GetInstanceIDDataCompleted( + const std::string& instance_id, const std::string& extra_data) { + instance_id_ = instance_id; + extra_data_ = extra_data; + + instance_id_resolved_counter_++; + + AsyncOperationCompleted(); +} + +void GCMDriverInstanceIDTest::GetToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + WaitToFinish wait_to_finish) { + base::RunLoop run_loop; + set_async_operation_completed_callback(run_loop.QuitClosure()); + driver()->GetInstanceIDHandlerInternal()->GetToken( + app_id, authorized_entity, scope, /*time_to_live=*/base::TimeDelta(), + base::BindOnce(&GCMDriverTest::RegisterCompleted, + base::Unretained(this))); + if (wait_to_finish == WAIT) + run_loop.Run(); +} + +void GCMDriverInstanceIDTest::DeleteToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + WaitToFinish wait_to_finish) { + base::RunLoop run_loop; + set_async_operation_completed_callback(run_loop.QuitClosure()); + driver()->GetInstanceIDHandlerInternal()->DeleteToken( + app_id, authorized_entity, scope, + base::BindOnce(&GCMDriverTest::UnregisterCompleted, + base::Unretained(this))); + if (wait_to_finish == WAIT) + run_loop.Run(); +} + +void GCMDriverInstanceIDTest::AddInstanceIDData(const std::string& app_id, + const std::string& instance_id, + const std::string& extra_data) { + driver()->GetInstanceIDHandlerInternal()->AddInstanceIDData( + app_id, instance_id, extra_data); +} + +void GCMDriverInstanceIDTest::RemoveInstanceIDData(const std::string& app_id) { + driver()->GetInstanceIDHandlerInternal()->RemoveInstanceIDData(app_id); +} + +TEST_F(GCMDriverInstanceIDTest, InstanceIDData) { + GetReady(); + + AddInstanceIDData(kTestAppID1, kInstanceID1, "Foo"); + GetInstanceID(kTestAppID1, GCMDriverTest::WAIT); + + EXPECT_EQ(kInstanceID1, instance_id()); + EXPECT_EQ("Foo", extra_data()); + EXPECT_EQ(1, instance_id_resolved_counter()); + + RemoveInstanceIDData(kTestAppID1); + GetInstanceID(kTestAppID1, GCMDriverTest::WAIT); + + EXPECT_TRUE(instance_id().empty()); + EXPECT_TRUE(extra_data().empty()); + EXPECT_EQ(2, instance_id_resolved_counter()); + + AddInstanceIDData(kTestAppID1, kInstanceID1, "Bar"); + GetInstanceID(kTestAppID1, GCMDriverTest::DO_NOT_WAIT); + GetInstanceID(kTestAppID1, GCMDriverTest::DO_NOT_WAIT); + + WaitForAsyncOperation(); + WaitForAsyncOperation(); + + EXPECT_EQ(kInstanceID1, instance_id()); + EXPECT_EQ("Bar", extra_data()); + EXPECT_EQ(4, instance_id_resolved_counter()); +} + +// This test is flaky, see https://crbug.com/1010462 +TEST_F(GCMDriverInstanceIDTest, + DISABLED_GCMClientNotReadyBeforeInstanceIDData) { + CreateDriver(); + PumpIOLoop(); + PumpUILoop(); + + // Make GCMClient not ready until PerformDelayedStart is called. + GetGCMClient()->set_start_mode_overridding( + FakeGCMClient::FORCE_TO_ALWAYS_DELAY_START_GCM); + + AddAppHandlers(); + + // All operations are on hold until GCMClient is ready. + AddInstanceIDData(kTestAppID1, kInstanceID1, "Foo"); + AddInstanceIDData(kTestAppID2, kInstanceID2, "Bar"); + RemoveInstanceIDData(kTestAppID1); + GetInstanceID(kTestAppID2, GCMDriverTest::DO_NOT_WAIT); + PumpIOLoop(); + PumpUILoop(); + EXPECT_TRUE(instance_id().empty()); + EXPECT_TRUE(extra_data().empty()); + + // All operations will be performed after GCMClient becomes ready. + GetGCMClient()->PerformDelayedStart(); + WaitForAsyncOperation(); + EXPECT_EQ(kInstanceID2, instance_id()); + EXPECT_EQ("Bar", extra_data()); +} + +TEST_F(GCMDriverInstanceIDTest, DISABLED_GetToken) { + GetReady(); + + const std::string expected_token = + FakeGCMClient::GenerateInstanceIDToken(kUserID1, kScope); + GetToken(kTestAppID1, kUserID1, kScope, GCMDriverTest::WAIT); + + EXPECT_EQ(expected_token, registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, registration_result()); +} + +TEST_F(GCMDriverInstanceIDTest, GetTokenError) { + GetReady(); + + std::string error_entity = "sender@error"; + GetToken(kTestAppID1, error_entity, kScope, GCMDriverTest::WAIT); + + EXPECT_TRUE(registration_id().empty()); + EXPECT_NE(GCMClient::SUCCESS, registration_result()); +} + +TEST_F(GCMDriverInstanceIDTest, GCMClientNotReadyBeforeGetToken) { + CreateDriver(); + PumpIOLoop(); + PumpUILoop(); + + // Make GCMClient not ready until PerformDelayedStart is called. + GetGCMClient()->set_start_mode_overridding( + FakeGCMClient::FORCE_TO_ALWAYS_DELAY_START_GCM); + + AddAppHandlers(); + + // GetToken operation is on hold until GCMClient is ready. + GetToken(kTestAppID1, kUserID1, kScope, GCMDriverTest::DO_NOT_WAIT); + PumpIOLoop(); + PumpUILoop(); + EXPECT_TRUE(registration_id().empty()); + EXPECT_EQ(GCMClient::UNKNOWN_ERROR, registration_result()); + + // GetToken operation will be invoked after GCMClient becomes ready. + GetGCMClient()->PerformDelayedStart(); + WaitForAsyncOperation(); + EXPECT_FALSE(registration_id().empty()); + EXPECT_EQ(GCMClient::SUCCESS, registration_result()); +} + +TEST_F(GCMDriverInstanceIDTest, DeleteToken) { + GetReady(); + + const std::string expected_token = + FakeGCMClient::GenerateInstanceIDToken(kUserID1, kScope); + GetToken(kTestAppID1, kUserID1, kScope, GCMDriverTest::WAIT); + EXPECT_EQ(expected_token, registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, registration_result()); + + DeleteToken(kTestAppID1, kUserID1, kScope, GCMDriverTest::WAIT); + EXPECT_EQ(GCMClient::SUCCESS, unregistration_result()); +} + +TEST_F(GCMDriverInstanceIDTest, GCMClientNotReadyBeforeDeleteToken) { + CreateDriver(); + PumpIOLoop(); + PumpUILoop(); + + // Make GCMClient not ready until PerformDelayedStart is called. + GetGCMClient()->set_start_mode_overridding( + FakeGCMClient::FORCE_TO_ALWAYS_DELAY_START_GCM); + + AddAppHandlers(); + + // DeleteToken operation is on hold until GCMClient is ready. + DeleteToken(kTestAppID1, kUserID1, kScope, GCMDriverTest::DO_NOT_WAIT); + PumpIOLoop(); + PumpUILoop(); + EXPECT_EQ(GCMClient::UNKNOWN_ERROR, unregistration_result()); + + // DeleteToken operation will be invoked after GCMClient becomes ready. + GetGCMClient()->PerformDelayedStart(); + WaitForAsyncOperation(); + EXPECT_EQ(GCMClient::SUCCESS, unregistration_result()); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_driver_unittest.cc b/chromium/components/gcm_driver/gcm_driver_unittest.cc new file mode 100644 index 00000000000..1bba7d56a23 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_driver_unittest.cc @@ -0,0 +1,269 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/gcm_driver_desktop.h" + +#include + +#include "base/base64.h" +#include "base/bind.h" +#include "base/files/scoped_temp_dir.h" +#include "base/run_loop.h" +#include "base/task/current_thread.h" +#include "base/test/task_environment.h" +#include "base/test/test_simple_task_runner.h" +#include "base/threading/thread.h" +#include "base/threading/thread_task_runner_handle.h" +#include "components/gcm_driver/crypto/gcm_decryption_result.h" +#include "components/gcm_driver/crypto/gcm_encryption_provider.h" +#include "components/gcm_driver/crypto/gcm_encryption_result.h" +#include "components/gcm_driver/fake_gcm_client_factory.h" +#include "components/gcm_driver/gcm_client_factory.h" +#include "components/prefs/pref_registry_simple.h" +#include "components/prefs/testing_pref_service.h" +#include "crypto/ec_private_key.h" +#include "net/url_request/url_request_context_getter.h" +#include "net/url_request/url_request_test_util.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" +#include "services/network/test/test_network_connection_tracker.h" +#include "services/network/test/test_url_loader_factory.h" +#include "services/network/test/test_utils.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/abseil-cpp/absl/types/optional.h" + +namespace gcm { + +namespace { + +const char kTestAppID1[] = "TestApp1"; + +void PumpCurrentLoop() { + base::RunLoop(base::RunLoop::Type::kNestableTasksAllowed).RunUntilIdle(); +} + +} // namespace + +class GCMDriverBaseTest : public testing::Test { + public: + enum WaitToFinish { DO_NOT_WAIT, WAIT }; + + GCMDriverBaseTest(); + + GCMDriverBaseTest(const GCMDriverBaseTest&) = delete; + GCMDriverBaseTest& operator=(const GCMDriverBaseTest&) = delete; + + ~GCMDriverBaseTest() override; + + // testing::Test: + void SetUp() override; + void TearDown() override; + + GCMDriverDesktop* driver() { return driver_.get(); } + + const std::string& p256dh() const { return p256dh_; } + const std::string& auth_secret() const { return auth_secret_; } + network::TestURLLoaderFactory& loader() { return test_url_loader_factory_; } + GCMEncryptionResult encryption_result() { return encryption_result_; } + const std::string& encrypted_message() { return encrypted_message_; } + GCMDecryptionResult decryption_result() { return decryption_result_; } + const std::string& decrypted_message() { return decrypted_message_; } + + void PumpIOLoop(); + + void CreateDriver(); + void ShutdownDriver(); + + void GetEncryptionInfo(const std::string& app_id, + WaitToFinish wait_to_finish); + void EncryptMessage(const std::string& app_id, + const std::string& authorized_entity, + const std::string& p256dh, + const std::string& auth_secret, + const std::string& message, + WaitToFinish wait_to_finish); + void DecryptMessage(const std::string& app_id, + const std::string& authorized_entity, + const std::string& message, + WaitToFinish wait_to_finish); + + void GetEncryptionInfoCompleted(std::string p256dh, std::string auth_secret); + void EncryptMessageCompleted(GCMEncryptionResult result, std::string message); + void DecryptMessageCompleted(GCMDecryptionResult result, std::string message); + void UnregisterCompleted(GCMClient::Result result); + + private: + base::ScopedTempDir temp_dir_; + TestingPrefServiceSimple prefs_; + base::test::TaskEnvironment task_environment_{ + base::test::TaskEnvironment::MainThreadType::UI}; + base::Thread io_thread_; + network::TestURLLoaderFactory test_url_loader_factory_; + + std::unique_ptr driver_; + + base::OnceClosure async_operation_completed_callback_; + std::string p256dh_; + std::string auth_secret_; + + GCMEncryptionResult encryption_result_ = + GCMEncryptionResult::ENCRYPTION_FAILED; + std::string encrypted_message_; + GCMDecryptionResult decryption_result_ = GCMDecryptionResult::UNENCRYPTED; + std::string decrypted_message_; +}; + +GCMDriverBaseTest::GCMDriverBaseTest() : io_thread_("IOThread") {} + +GCMDriverBaseTest::~GCMDriverBaseTest() = default; + +void GCMDriverBaseTest::SetUp() { + io_thread_.Start(); + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + + CreateDriver(); + PumpIOLoop(); + PumpCurrentLoop(); +} + +void GCMDriverBaseTest::TearDown() { + if (!driver_) + return; + + ShutdownDriver(); + driver_.reset(); + PumpIOLoop(); + + io_thread_.Stop(); + task_environment_.RunUntilIdle(); + ASSERT_TRUE(temp_dir_.Delete()); +} + +void GCMDriverBaseTest::PumpIOLoop() { + base::RunLoop run_loop; + io_thread_.task_runner()->PostTaskAndReply( + FROM_HERE, base::BindOnce(&PumpCurrentLoop), run_loop.QuitClosure()); + run_loop.Run(); +} + +void GCMDriverBaseTest::CreateDriver() { + scoped_refptr request_context = + new net::TestURLRequestContextGetter(io_thread_.task_runner()); + GCMClient::ChromeBuildInfo chrome_build_info; + chrome_build_info.product_category_for_subtypes = "com.chrome.macosx"; + driver_ = std::make_unique( + std::make_unique( + base::ThreadTaskRunnerHandle::Get(), io_thread_.task_runner()), + chrome_build_info, &prefs_, temp_dir_.GetPath(), + /*remove_account_mappings_with_email_key=*/true, base::DoNothing(), + base::MakeRefCounted( + &test_url_loader_factory_), + network::TestNetworkConnectionTracker::GetInstance(), + base::ThreadTaskRunnerHandle::Get(), io_thread_.task_runner(), + task_environment_.GetMainThreadTaskRunner()); +} + +void GCMDriverBaseTest::ShutdownDriver() { + driver()->Shutdown(); +} + +void GCMDriverBaseTest::GetEncryptionInfo(const std::string& app_id, + WaitToFinish wait_to_finish) { + base::RunLoop run_loop; + async_operation_completed_callback_ = run_loop.QuitClosure(); + driver_->GetEncryptionInfo( + app_id, base::BindOnce(&GCMDriverBaseTest::GetEncryptionInfoCompleted, + base::Unretained(this))); + if (wait_to_finish == WAIT) + run_loop.Run(); +} + +void GCMDriverBaseTest::EncryptMessage(const std::string& app_id, + const std::string& authorized_entity, + const std::string& p256dh, + const std::string& auth_secret, + const std::string& message, + WaitToFinish wait_to_finish) { + base::RunLoop run_loop; + async_operation_completed_callback_ = run_loop.QuitClosure(); + + driver()->EncryptMessage( + app_id, authorized_entity, p256dh, auth_secret, message, + base::BindOnce(&GCMDriverBaseTest::EncryptMessageCompleted, + base::Unretained(this))); + + if (wait_to_finish == WAIT) + run_loop.Run(); +} + +void GCMDriverBaseTest::DecryptMessage(const std::string& app_id, + const std::string& authorized_entity, + const std::string& message, + WaitToFinish wait_to_finish) { + base::RunLoop run_loop; + async_operation_completed_callback_ = run_loop.QuitClosure(); + + driver()->DecryptMessage( + app_id, authorized_entity, message, + base::BindOnce(&GCMDriverBaseTest::DecryptMessageCompleted, + base::Unretained(this))); + + if (wait_to_finish == WAIT) + run_loop.Run(); +} + +void GCMDriverBaseTest::GetEncryptionInfoCompleted(std::string p256dh, + std::string auth_secret) { + p256dh_ = std::move(p256dh); + auth_secret_ = std::move(auth_secret); + if (!async_operation_completed_callback_.is_null()) + std::move(async_operation_completed_callback_).Run(); +} + +void GCMDriverBaseTest::EncryptMessageCompleted(GCMEncryptionResult result, + std::string message) { + encryption_result_ = result; + encrypted_message_ = std::move(message); + if (!async_operation_completed_callback_.is_null()) + std::move(async_operation_completed_callback_).Run(); +} + +void GCMDriverBaseTest::DecryptMessageCompleted(GCMDecryptionResult result, + std::string message) { + decryption_result_ = result; + decrypted_message_ = std::move(message); + if (!async_operation_completed_callback_.is_null()) + std::move(async_operation_completed_callback_).Run(); +} + +TEST_F(GCMDriverBaseTest, EncryptionDecryptionRoundTrip) { + GetEncryptionInfo(kTestAppID1, GCMDriverBaseTest::WAIT); + + std::string message = "payload"; + ASSERT_NO_FATAL_FAILURE( + EncryptMessage(kTestAppID1, /* authorized_entity= */ "", p256dh(), + auth_secret(), message, GCMDriverBaseTest::WAIT)); + + EXPECT_EQ(GCMEncryptionResult::ENCRYPTED_DRAFT_08, encryption_result()); + + ASSERT_NO_FATAL_FAILURE( + DecryptMessage(kTestAppID1, /* authorized_entity= */ "", + encrypted_message(), GCMDriverBaseTest::WAIT)); + + EXPECT_EQ(GCMDecryptionResult::DECRYPTED_DRAFT_08, decryption_result()); + EXPECT_EQ(message, decrypted_message()); +} + +TEST_F(GCMDriverBaseTest, EncryptionError) { + // Intentionally not creating encryption info. + + std::string message = "payload"; + ASSERT_NO_FATAL_FAILURE( + EncryptMessage(kTestAppID1, /* authorized_entity= */ "", p256dh(), + auth_secret(), message, GCMDriverBaseTest::WAIT)); + + EXPECT_EQ(GCMEncryptionResult::NO_KEYS, encryption_result()); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_internals_constants.cc b/chromium/components/gcm_driver/gcm_internals_constants.cc new file mode 100644 index 00000000000..94fc32ba0b0 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_internals_constants.cc @@ -0,0 +1,41 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/gcm_internals_constants.h" + +namespace gcm_driver { + +// Resource paths. +const char kGcmInternalsCSS[] = "gcm_internals.css"; +const char kGcmInternalsJS[] = "gcm_internals.js"; + +// Message handlers. +const char kGetGcmInternalsInfo[] = "getGcmInternalsInfo"; +const char kSetGcmInternalsInfo[] = "set-gcm-internals-info"; +const char kSetGcmInternalsRecording[] = "setGcmInternalsRecording"; + +// GCM internal info. +const char kAndroidId[] = "androidId"; +const char kAndroidSecret[] = "androidSecret"; +const char kCheckinInfo[] = "checkinInfo"; +const char kConnectionClientCreated[] = "connectionClientCreated"; +const char kConnectionInfo[] = "connectionInfo"; +const char kConnectionState[] = "connectionState"; +const char kDeviceInfo[] = "deviceInfo"; +const char kGcmClientCreated[] = "gcmClientCreated"; +const char kGcmClientState[] = "gcmClientState"; +const char kGcmEnabled[] = "gcmEnabled"; +const char kIsRecording[] = "isRecording"; +const char kLastCheckin[] = "lastCheckin"; +const char kNextCheckin[] = "nextCheckin"; +const char kProfileServiceCreated[] = "profileServiceCreated"; +const char kReceiveInfo[] = "receiveInfo"; +const char kRegisteredAppIds[] = "registeredAppIds"; +const char kRegistrationInfo[] = "registrationInfo"; +const char kResendQueueSize[] = "resendQueueSize"; +const char kSendInfo[] = "sendInfo"; +const char kSendQueueSize[] = "sendQueueSize"; +const char kDecryptionFailureInfo[] = "decryptionFailureInfo"; + +} // namespace gcm_driver diff --git a/chromium/components/gcm_driver/gcm_internals_constants.h b/chromium/components/gcm_driver/gcm_internals_constants.h new file mode 100644 index 00000000000..54623bc4303 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_internals_constants.h @@ -0,0 +1,47 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_GCM_DRIVER_GCM_INTERNALS_CONSTANTS_H_ +#define COMPONENTS_GCM_DRIVER_GCM_INTERNALS_CONSTANTS_H_ + +namespace gcm_driver { + +// Resource paths. +// Must match the resource file names. +extern const char kGcmInternalsCSS[]; +extern const char kGcmInternalsJS[]; + +// Message handlers. +// Must match the constants used in the resource files. +extern const char kGetGcmInternalsInfo[]; +extern const char kSetGcmInternalsInfo[]; +extern const char kSetGcmInternalsRecording[]; + +// GCM internal info. +// Must match the constants used in the resource files. +extern const char kAndroidId[]; +extern const char kAndroidSecret[]; +extern const char kCheckinInfo[]; +extern const char kConnectionClientCreated[]; +extern const char kConnectionInfo[]; +extern const char kConnectionState[]; +extern const char kDeviceInfo[]; +extern const char kGcmClientCreated[]; +extern const char kGcmClientState[]; +extern const char kGcmEnabled[]; +extern const char kIsRecording[]; +extern const char kLastCheckin[]; +extern const char kNextCheckin[]; +extern const char kProfileServiceCreated[]; +extern const char kReceiveInfo[]; +extern const char kRegisteredAppIds[]; +extern const char kRegistrationInfo[]; +extern const char kResendQueueSize[]; +extern const char kSendInfo[]; +extern const char kSendQueueSize[]; +extern const char kDecryptionFailureInfo[]; + +} // namespace gcm_driver + +#endif // COMPONENTS_GCM_DRIVER_GCM_INTERNALS_CONSTANTS_H_ diff --git a/chromium/components/gcm_driver/gcm_internals_helper.cc b/chromium/components/gcm_driver/gcm_internals_helper.cc new file mode 100644 index 00000000000..6b360d6a526 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_internals_helper.cc @@ -0,0 +1,190 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/gcm_internals_helper.h" + +#include +#include + +#include "base/format_macros.h" +#include "base/i18n/time_formatting.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "base/values.h" +#include "components/gcm_driver/gcm_activity.h" +#include "components/gcm_driver/gcm_internals_constants.h" +#include "components/gcm_driver/gcm_profile_service.h" + +namespace gcm_driver { + +namespace { + +void SetCheckinInfo(const std::vector& checkins, + std::vector* checkin_info) { + for (const gcm::CheckinActivity& checkin : checkins) { + base::Value row(base::Value::Type::LIST); + row.Append(checkin.time.ToJsTime()); + row.Append(checkin.event); + row.Append(checkin.details); + checkin_info->push_back(std::move(row)); + } +} + +void SetConnectionInfo(const std::vector& connections, + std::vector* connection_info) { + for (const gcm::ConnectionActivity& connection : connections) { + base::Value row(base::Value::Type::LIST); + row.Append(connection.time.ToJsTime()); + row.Append(connection.event); + row.Append(connection.details); + connection_info->push_back(std::move(row)); + } +} + +void SetRegistrationInfo( + const std::vector& registrations, + std::vector* registration_info) { + for (const gcm::RegistrationActivity& registration : registrations) { + base::Value row(base::Value::Type::LIST); + row.Append(registration.time.ToJsTime()); + row.Append(registration.app_id); + row.Append(registration.source); + row.Append(registration.event); + row.Append(registration.details); + registration_info->push_back(std::move(row)); + } +} + +void SetReceivingInfo(const std::vector& receives, + std::vector* receive_info) { + for (const gcm::ReceivingActivity& receive : receives) { + base::Value row(base::Value::Type::LIST); + row.Append(receive.time.ToJsTime()); + row.Append(receive.app_id); + row.Append(receive.from); + row.Append(base::NumberToString(receive.message_byte_size)); + row.Append(receive.event); + row.Append(receive.details); + receive_info->push_back(std::move(row)); + } +} + +void SetSendingInfo(const std::vector& sends, + std::vector* send_info) { + for (const gcm::SendingActivity& send : sends) { + base::Value row(base::Value::Type::LIST); + row.Append(send.time.ToJsTime()); + row.Append(send.app_id); + row.Append(send.receiver_id); + row.Append(send.message_id); + row.Append(send.event); + row.Append(send.details); + send_info->push_back(std::move(row)); + } +} + +void SetDecryptionFailureInfo( + const std::vector& failures, + std::vector* failure_info) { + for (const gcm::DecryptionFailureActivity& failure : failures) { + base::Value row(base::Value::Type::LIST); + row.Append(failure.time.ToJsTime()); + row.Append(failure.app_id); + row.Append(failure.details); + failure_info->push_back(std::move(row)); + } +} + +} // namespace + +void SetGCMInternalsInfo(const gcm::GCMClient::GCMStatistics* stats, + gcm::GCMProfileService* profile_service, + PrefService* prefs, + base::DictionaryValue* results) { + base::Value device_info(base::Value::Type::DICTIONARY); + + device_info.SetBoolKey(kProfileServiceCreated, profile_service != nullptr); + device_info.SetBoolKey(kGcmEnabled, true); + if (stats) { + results->SetBoolKey(kIsRecording, stats->is_recording); + device_info.SetBoolKey(kGcmClientCreated, stats->gcm_client_created); + device_info.SetStringKey(kGcmClientState, stats->gcm_client_state); + device_info.SetBoolKey(kConnectionClientCreated, + stats->connection_client_created); + + base::Value registered_app_ids(base::Value::Type::LIST); + for (const std::string& app_id : stats->registered_app_ids) + registered_app_ids.Append(app_id); + + device_info.SetKey(kRegisteredAppIds, std::move(registered_app_ids)); + + if (stats->connection_client_created) + device_info.SetStringKey(kConnectionState, stats->connection_state); + if (!stats->last_checkin.is_null()) { + device_info.SetStringKey( + kLastCheckin, base::UTF16ToUTF8(base::TimeFormatFriendlyDateAndTime( + stats->last_checkin))); + } + if (!stats->next_checkin.is_null()) { + device_info.SetStringKey( + kNextCheckin, base::UTF16ToUTF8(base::TimeFormatFriendlyDateAndTime( + stats->next_checkin))); + } + if (stats->android_id > 0) { + device_info.SetStringKey( + kAndroidId, base::StringPrintf("0x%" PRIx64, stats->android_id)); + } + if (stats->android_secret > 0) { + device_info.SetStringKey(kAndroidSecret, + base::NumberToString(stats->android_secret)); + } + device_info.SetIntKey(kSendQueueSize, stats->send_queue_size); + device_info.SetIntKey(kResendQueueSize, stats->resend_queue_size); + results->SetKey(kDeviceInfo, std::move(device_info)); + + if (stats->recorded_activities.checkin_activities.size() > 0) { + std::vector checkin_info; + SetCheckinInfo(stats->recorded_activities.checkin_activities, + &checkin_info); + results->SetKey(kCheckinInfo, base::Value(std::move(checkin_info))); + } + if (stats->recorded_activities.connection_activities.size() > 0) { + std::vector connection_info; + SetConnectionInfo(stats->recorded_activities.connection_activities, + &connection_info); + results->SetKey(kConnectionInfo, base::Value(std::move(connection_info))); + } + if (stats->recorded_activities.registration_activities.size() > 0) { + std::vector registration_info; + SetRegistrationInfo(stats->recorded_activities.registration_activities, + ®istration_info); + results->SetKey(kRegistrationInfo, + base::Value(std::move(registration_info))); + } + if (stats->recorded_activities.receiving_activities.size() > 0) { + std::vector receive_info; + SetReceivingInfo(stats->recorded_activities.receiving_activities, + &receive_info); + results->SetKey(kReceiveInfo, base::Value(std::move(receive_info))); + } + if (stats->recorded_activities.sending_activities.size() > 0) { + std::vector send_info; + SetSendingInfo(stats->recorded_activities.sending_activities, &send_info); + results->SetKey(kSendInfo, base::Value(std::move(send_info))); + } + + if (stats->recorded_activities.decryption_failure_activities.size() > 0) { + std::vector failure_info; + SetDecryptionFailureInfo( + stats->recorded_activities.decryption_failure_activities, + &failure_info); + results->SetKey(kDecryptionFailureInfo, + base::Value(std::move(failure_info))); + } + } +} + +} // namespace gcm_driver diff --git a/chromium/components/gcm_driver/gcm_internals_helper.h b/chromium/components/gcm_driver/gcm_internals_helper.h new file mode 100644 index 00000000000..1ea48b23b8a --- /dev/null +++ b/chromium/components/gcm_driver/gcm_internals_helper.h @@ -0,0 +1,30 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_GCM_DRIVER_GCM_INTERNALS_HELPER_H_ +#define COMPONENTS_GCM_DRIVER_GCM_INTERNALS_HELPER_H_ + +#include "components/gcm_driver/gcm_client.h" + +class PrefService; + +namespace base { +class DictionaryValue; +} + +namespace gcm { +class GCMProfileService; +} + +namespace gcm_driver { + +// Sets the GCM infos for the gcm-internals WebUI in |results|. +void SetGCMInternalsInfo(const gcm::GCMClient::GCMStatistics* stats, + gcm::GCMProfileService* profile_service, + PrefService* prefs, + base::DictionaryValue* results); + +} // namespace gcm_driver + +#endif // COMPONENTS_GCM_DRIVER_GCM_INTERNALS_HELPER_H_ diff --git a/chromium/components/gcm_driver/gcm_profile_service.cc b/chromium/components/gcm_driver/gcm_profile_service.cc new file mode 100644 index 00000000000..6b6bed3d2ff --- /dev/null +++ b/chromium/components/gcm_driver/gcm_profile_service.cc @@ -0,0 +1,205 @@ +// Copyright (c) 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 "components/gcm_driver/gcm_profile_service.h" + +#include +#include + +#include "base/memory/raw_ptr.h" +#include "build/build_config.h" +#include "components/gcm_driver/gcm_driver.h" +#include "components/gcm_driver/gcm_driver_constants.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/prefs/pref_service.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" + +#if BUILDFLAG(USE_GCM_FROM_PLATFORM) +#include "base/task/sequenced_task_runner.h" +#include "components/gcm_driver/gcm_driver_android.h" +#else +#include "base/bind.h" +#include "base/files/file_path.h" +#include "base/memory/weak_ptr.h" +#include "components/gcm_driver/account_tracker.h" +#include "components/gcm_driver/gcm_account_tracker.h" +#include "components/gcm_driver/gcm_client_factory.h" +#include "components/gcm_driver/gcm_desktop_utils.h" +#include "components/gcm_driver/gcm_driver_desktop.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#endif + +namespace gcm { + +#if !BUILDFLAG(USE_GCM_FROM_PLATFORM) +// Identity observer only has actual work to do when the user is actually signed +// in. It ensures that account tracker is taking +class GCMProfileService::IdentityObserver + : public signin::IdentityManager::Observer { + public: + IdentityObserver( + signin::IdentityManager* identity_manager, + scoped_refptr url_loader_factory, + GCMDriver* driver); + + IdentityObserver(const IdentityObserver&) = delete; + IdentityObserver& operator=(const IdentityObserver&) = delete; + + ~IdentityObserver() override; + + // signin::IdentityManager::Observer: + void OnPrimaryAccountChanged( + const signin::PrimaryAccountChangeEvent& event) override; + + private: + void OnSyncPrimaryAccountSet(const CoreAccountInfo& primary_account_info); + void StartAccountTracker( + scoped_refptr url_loader_factory); + + raw_ptr driver_; + raw_ptr identity_manager_; + std::unique_ptr gcm_account_tracker_; + + // The account ID that this service is responsible for. Empty when the service + // is not running. + CoreAccountId account_id_; + + base::WeakPtrFactory weak_ptr_factory_{ + this}; +}; + +GCMProfileService::IdentityObserver::IdentityObserver( + signin::IdentityManager* identity_manager, + scoped_refptr url_loader_factory, + GCMDriver* driver) + : driver_(driver), identity_manager_(identity_manager) { + identity_manager_->AddObserver(this); + + OnSyncPrimaryAccountSet( + identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSync)); + StartAccountTracker(std::move(url_loader_factory)); +} + +GCMProfileService::IdentityObserver::~IdentityObserver() { + if (gcm_account_tracker_) + gcm_account_tracker_->Shutdown(); + identity_manager_->RemoveObserver(this); +} + +void GCMProfileService::IdentityObserver::OnPrimaryAccountChanged( + const signin::PrimaryAccountChangeEvent& event) { + switch (event.GetEventTypeFor(signin::ConsentLevel::kSync)) { + case signin::PrimaryAccountChangeEvent::Type::kSet: + OnSyncPrimaryAccountSet(event.GetCurrentState().primary_account); + break; + case signin::PrimaryAccountChangeEvent::Type::kCleared: + account_id_ = CoreAccountId(); + + // Still need to notify GCMDriver for UMA purpose. + driver_->OnSignedOut(); + break; + case signin::PrimaryAccountChangeEvent::Type::kNone: + break; + } +} + +void GCMProfileService::IdentityObserver::OnSyncPrimaryAccountSet( + const CoreAccountInfo& primary_account_info) { + // This might be called multiple times when the password changes. + if (primary_account_info.account_id == account_id_) + return; + account_id_ = primary_account_info.account_id; + + // Still need to notify GCMDriver for UMA purpose. + driver_->OnSignedIn(); +} + +void GCMProfileService::IdentityObserver::StartAccountTracker( + scoped_refptr url_loader_factory) { + if (gcm_account_tracker_) + return; + + std::unique_ptr gaia_account_tracker( + new AccountTracker(identity_manager_)); + + gcm_account_tracker_ = std::make_unique( + std::move(gaia_account_tracker), identity_manager_, driver_); + + gcm_account_tracker_->Start(); +} + +#endif // !BUILDFLAG(USE_GCM_FROM_PLATFORM) + +#if BUILDFLAG(USE_GCM_FROM_PLATFORM) +GCMProfileService::GCMProfileService( + base::FilePath path, + scoped_refptr& blocking_task_runner) { + driver_ = std::make_unique( + path.Append(gcm_driver::kGCMStoreDirname), blocking_task_runner); +} +#else +GCMProfileService::GCMProfileService( + PrefService* prefs, + base::FilePath path, + base::RepeatingCallback, + mojo::PendingReceiver)> + get_socket_factory_callback, + scoped_refptr url_loader_factory, + network::NetworkConnectionTracker* network_connection_tracker, + version_info::Channel channel, + const std::string& product_category_for_subtypes, + signin::IdentityManager* identity_manager, + std::unique_ptr gcm_client_factory, + const scoped_refptr& ui_task_runner, + const scoped_refptr& io_task_runner, + scoped_refptr& blocking_task_runner) + : identity_manager_(identity_manager), + url_loader_factory_(std::move(url_loader_factory)) { + signin::IdentityManager::AccountIdMigrationState id_migration = + identity_manager_->GetAccountIdMigrationState(); + bool remove_account_mappings_with_email_key = + (id_migration == signin::IdentityManager::MIGRATION_IN_PROGRESS) || + (id_migration == signin::IdentityManager::MIGRATION_DONE); + driver_ = CreateGCMDriverDesktop( + std::move(gcm_client_factory), prefs, + path.Append(gcm_driver::kGCMStoreDirname), + remove_account_mappings_with_email_key, + base::BindRepeating(get_socket_factory_callback, + weak_ptr_factory_.GetWeakPtr()), + url_loader_factory_, network_connection_tracker, channel, + product_category_for_subtypes, ui_task_runner, io_task_runner, + blocking_task_runner); + + identity_observer_ = std::make_unique( + identity_manager_, url_loader_factory_, driver_.get()); +} +#endif // BUILDFLAG(USE_GCM_FROM_PLATFORM) + +GCMProfileService::GCMProfileService() {} + +GCMProfileService::~GCMProfileService() {} + +void GCMProfileService::Shutdown() { +#if !BUILDFLAG(USE_GCM_FROM_PLATFORM) + identity_observer_.reset(); +#endif // !BUILDFLAG(USE_GCM_FROM_PLATFORM) + if (driver_) { + driver_->Shutdown(); + driver_.reset(); + } +} + +void GCMProfileService::SetDriverForTesting(std::unique_ptr driver) { + driver_ = std::move(driver); + +#if !BUILDFLAG(USE_GCM_FROM_PLATFORM) + if (identity_observer_) { + identity_observer_ = std::make_unique( + identity_manager_, url_loader_factory_, driver.get()); + } +#endif // !BUILDFLAG(USE_GCM_FROM_PLATFORM) +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_profile_service.h b/chromium/components/gcm_driver/gcm_profile_service.h new file mode 100644 index 00000000000..2f06a17f832 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_profile_service.h @@ -0,0 +1,111 @@ +// Copyright (c) 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 COMPONENTS_GCM_DRIVER_GCM_PROFILE_SERVICE_H_ +#define COMPONENTS_GCM_DRIVER_GCM_PROFILE_SERVICE_H_ + +#include +#include + +#include "base/callback_forward.h" +#include "base/compiler_specific.h" +#include "base/files/file_path.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/ref_counted.h" +#include "base/memory/weak_ptr.h" +#include "build/build_config.h" +#include "components/gcm_driver/gcm_buildflags.h" +#include "components/keyed_service/core/keyed_service.h" +#include "components/version_info/version_info.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "services/network/public/mojom/proxy_resolving_socket.mojom.h" + +class PrefService; + +namespace base { +class SequencedTaskRunner; +} + +namespace signin { +class IdentityManager; +} + +namespace network { +class NetworkConnectionTracker; +class SharedURLLoaderFactory; +} + +namespace gcm { + +class GCMClientFactory; +class GCMDriver; + +// Providing GCM service, via GCMDriver. +class GCMProfileService : public KeyedService { + public: + using GetProxyResolvingFactoryCallback = base::RepeatingCallback)>; + +#if BUILDFLAG(USE_GCM_FROM_PLATFORM) + GCMProfileService( + base::FilePath path, + scoped_refptr& blocking_task_runner); +#else + GCMProfileService( + PrefService* prefs, + base::FilePath path, + base::RepeatingCallback, + mojo::PendingReceiver)> + get_socket_factory_callback, + scoped_refptr url_loader_factory, + network::NetworkConnectionTracker* network_connection_tracker, + version_info::Channel channel, + const std::string& product_category_for_subtypes, + signin::IdentityManager* identity_manager, + std::unique_ptr gcm_client_factory, + const scoped_refptr& ui_task_runner, + const scoped_refptr& io_task_runner, + scoped_refptr& blocking_task_runner); +#endif + + GCMProfileService(const GCMProfileService&) = delete; + GCMProfileService& operator=(const GCMProfileService&) = delete; + + ~GCMProfileService() override; + + // KeyedService: + void Shutdown() override; + + // For testing purposes. + void SetDriverForTesting(std::unique_ptr driver); + + GCMDriver* driver() const { return driver_.get(); } + + protected: + // Used for constructing fake GCMProfileService for testing purpose. + GCMProfileService(); + + private: + std::unique_ptr driver_; + +#if !BUILDFLAG(USE_GCM_FROM_PLATFORM) + raw_ptr identity_manager_; + + scoped_refptr url_loader_factory_; + + // Used for both account tracker and GCM.UserSignedIn UMA. + class IdentityObserver; + std::unique_ptr identity_observer_; +#endif + + GetProxyResolvingFactoryCallback get_socket_factory_callback_; + + // WeakPtr generated by the factory must be dereferenced on the UI thread. + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_GCM_PROFILE_SERVICE_H_ diff --git a/chromium/components/gcm_driver/gcm_stats_recorder_android.cc b/chromium/components/gcm_driver/gcm_stats_recorder_android.cc new file mode 100644 index 00000000000..dc3137be535 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_stats_recorder_android.cc @@ -0,0 +1,148 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include + +#include "components/gcm_driver/crypto/gcm_decryption_result.h" +#include "components/gcm_driver/gcm_stats_recorder_android.h" + +namespace gcm { + +namespace { + +const size_t MAX_LOGGED_ACTIVITY_COUNT = 100; + +const char kSuccess[] = "SUCCESS"; +const char kUnknownError[] = "UNKNOWN_ERROR"; + +} // namespace + +GCMStatsRecorderAndroid::GCMStatsRecorderAndroid(Delegate* delegate) + : delegate_(delegate) {} + +GCMStatsRecorderAndroid::~GCMStatsRecorderAndroid() {} + +void GCMStatsRecorderAndroid::Clear() { + registration_activities_.clear(); + receiving_activities_.clear(); + decryption_failure_activities_.clear(); +} + +void GCMStatsRecorderAndroid::CollectActivities( + RecordedActivities* recorded_activities) const { + DCHECK(recorded_activities); + + recorded_activities->registration_activities.insert( + recorded_activities->registration_activities.begin(), + registration_activities_.begin(), + registration_activities_.end()); + recorded_activities->receiving_activities.insert( + recorded_activities->receiving_activities.begin(), + receiving_activities_.begin(), + receiving_activities_.end()); + recorded_activities->decryption_failure_activities.insert( + recorded_activities->decryption_failure_activities.begin(), + decryption_failure_activities_.begin(), + decryption_failure_activities_.end()); +} + +void GCMStatsRecorderAndroid::RecordRegistrationSent( + const std::string& app_id) { + if (!is_recording_) + return; + + RecordRegistration(app_id, "Registration request sent", "" /* details */); +} + +void GCMStatsRecorderAndroid::RecordRegistrationResponse( + const std::string& app_id, + bool success) { + if (!is_recording_) + return; + + RecordRegistration( + app_id, "Registration response received", success ? kSuccess + : kUnknownError); +} + +void GCMStatsRecorderAndroid::RecordUnregistrationSent( + const std::string& app_id) { + if (!is_recording_) + return; + + RecordRegistration(app_id, "Unregistration request sent", "" /* details */); +} + +void GCMStatsRecorderAndroid::RecordUnregistrationResponse( + const std::string& app_id, + bool success) { + if (!is_recording_) + return; + + RecordRegistration( + app_id, "Unregistration response received", success ? kSuccess + : kUnknownError); +} + +void GCMStatsRecorderAndroid::RecordRegistration(const std::string& app_id, + const std::string& event, + const std::string& details) { + RegistrationActivity activity; + activity.app_id = app_id; + activity.event = event; + activity.details = details; + + // TODO(peter): Include the |source| (sender id(s)) of the registrations. + + registration_activities_.push_front(activity); + if (registration_activities_.size() > MAX_LOGGED_ACTIVITY_COUNT) + registration_activities_.pop_back(); + + if (delegate_) + delegate_->OnActivityRecorded(); +} + +void GCMStatsRecorderAndroid::RecordDataMessageReceived( + const std::string& app_id, + const std::string& from, + int message_byte_size) { + if (!is_recording_) + return; + + ReceivingActivity activity; + activity.app_id = app_id; + activity.from = from; + activity.message_byte_size = message_byte_size; + activity.event = "Data msg received"; + + receiving_activities_.push_front(activity); + if (receiving_activities_.size() > MAX_LOGGED_ACTIVITY_COUNT) + receiving_activities_.pop_back(); + + if (delegate_) + delegate_->OnActivityRecorded(); +} + +void GCMStatsRecorderAndroid::RecordDecryptionFailure( + const std::string& app_id, + GCMDecryptionResult result) { + DCHECK_NE(result, GCMDecryptionResult::UNENCRYPTED); + DCHECK_NE(result, GCMDecryptionResult::DECRYPTED_DRAFT_03); + DCHECK_NE(result, GCMDecryptionResult::DECRYPTED_DRAFT_08); + if (!is_recording_) + return; + + DecryptionFailureActivity activity; + activity.app_id = app_id; + activity.details = ToGCMDecryptionResultDetailsString(result); + + decryption_failure_activities_.push_front(activity); + if (decryption_failure_activities_.size() > MAX_LOGGED_ACTIVITY_COUNT) + decryption_failure_activities_.pop_back(); + + if (delegate_) + delegate_->OnActivityRecorded(); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_stats_recorder_android.h b/chromium/components/gcm_driver/gcm_stats_recorder_android.h new file mode 100644 index 00000000000..3bbe4c11302 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_stats_recorder_android.h @@ -0,0 +1,99 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_GCM_DRIVER_GCM_STATS_RECORDER_ANDROID_H_ +#define COMPONENTS_GCM_DRIVER_GCM_STATS_RECORDER_ANDROID_H_ + +#include + +#include "base/containers/circular_deque.h" +#include "base/memory/raw_ptr.h" +#include "components/gcm_driver/gcm_activity.h" + +namespace gcm { + +enum class GCMDecryptionResult; + +// Stats recorder for Android, used for recording stats and activities on the +// GCM Driver level for debugging purposes. Based on the GCMStatsRecorder, as +// defined in the GCM Engine, which does not exist on Android. +// +// Note that this class, different from the GCMStatsRecorder(Impl), is expected +// to be used on the UI thread. +class GCMStatsRecorderAndroid { + public: + // A delegate interface that allows the GCMStatsRecorderAndroid instance to + // interact with its container. + class Delegate { + public: + // Called when the GCMStatsRecorderAndroid is recording activities and a new + // activity has just been recorded. + virtual void OnActivityRecorded() = 0; + }; + + // A weak reference to |delegate| is stored, so it must outlive the recorder. + explicit GCMStatsRecorderAndroid(Delegate* delegate); + + GCMStatsRecorderAndroid(const GCMStatsRecorderAndroid&) = delete; + GCMStatsRecorderAndroid& operator=(const GCMStatsRecorderAndroid&) = delete; + + ~GCMStatsRecorderAndroid(); + + // Clears the recorded activities. + void Clear(); + + // Collects all recorded activities into |*recorded_activities|. + void CollectActivities(RecordedActivities* recorded_activities) const; + + // Records that a registration for |app_id| has been sent. + void RecordRegistrationSent(const std::string& app_id); + + // Records that the registration sent for |app_id| has received a response. + // |success| indicates whether the registration was successful. + void RecordRegistrationResponse(const std::string& app_id, bool success); + + // Records that an unregistration for |app_id| has been sent. + void RecordUnregistrationSent(const std::string& app_id); + + // Records that the unregistration sent for |app_id| has received a response. + // |success| indicates whether the unregistration was successful. + void RecordUnregistrationResponse(const std::string& app_id, bool success); + + // Records that a data message has been received for |app_id|. + void RecordDataMessageReceived(const std::string& app_id, + const std::string& from, + int message_byte_size); + + // Records a message decryption failure caused by |result| for |app_id|. + void RecordDecryptionFailure(const std::string& app_id, + GCMDecryptionResult result); + + bool is_recording() const { return is_recording_; } + void set_is_recording(bool recording) { is_recording_ = recording; } + + private: + void RecordRegistration(const std::string& app_id, + const std::string& event, + const std::string& details); + + // Delegate made available by the container. May be a nullptr. + raw_ptr delegate_; + + // Toggle determining whether the recorder is recording. + bool is_recording_ = false; + + // Recorded registration activities (which includes unregistrations). + base::circular_deque registration_activities_; + + // Recorded received message activities. + base::circular_deque receiving_activities_; + + // Recorded message decryption failure activities. + base::circular_deque + decryption_failure_activities_; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_GCM_STATS_RECORDER_ANDROID_H_ diff --git a/chromium/components/gcm_driver/gcm_stats_recorder_android_unittest.cc b/chromium/components/gcm_driver/gcm_stats_recorder_android_unittest.cc new file mode 100644 index 00000000000..d789e76907a --- /dev/null +++ b/chromium/components/gcm_driver/gcm_stats_recorder_android_unittest.cc @@ -0,0 +1,116 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/gcm_stats_recorder_android.h" + +#include + +#include "components/gcm_driver/crypto/gcm_decryption_result.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { + +namespace { + +const char kTestAppId[] = "test_app_id"; +const char kTestSenderId[] = "test_sender_id"; + +class GCMStatsRecorderAndroidTest : public ::testing::Test, + public GCMStatsRecorderAndroid::Delegate { + public: + GCMStatsRecorderAndroidTest() + : activity_recorded_calls_(0) {} + ~GCMStatsRecorderAndroidTest() override {} + + // GCMStatsRecorderAndroid::Delegate implementation. + void OnActivityRecorded() override { + ++activity_recorded_calls_; + } + + size_t activity_recorded_calls() const { return activity_recorded_calls_; } + + private: + size_t activity_recorded_calls_; +}; + +TEST_F(GCMStatsRecorderAndroidTest, RecordsAndCallsDelegate) { + GCMStatsRecorderAndroid recorder(this /* delegate */); + recorder.set_is_recording(true); + + ASSERT_TRUE(recorder.is_recording()); + + EXPECT_EQ(0u, activity_recorded_calls()); + + recorder.RecordRegistrationSent(kTestAppId); + EXPECT_EQ(1u, activity_recorded_calls()); + + recorder.RecordRegistrationResponse(kTestAppId, true /* success */); + EXPECT_EQ(2u, activity_recorded_calls()); + + recorder.RecordUnregistrationSent(kTestAppId); + EXPECT_EQ(3u, activity_recorded_calls()); + + recorder.RecordUnregistrationResponse(kTestAppId, true /* success */); + EXPECT_EQ(4u, activity_recorded_calls()); + + recorder.RecordDataMessageReceived(kTestAppId, kTestSenderId, + 42 /* message_byte_size */); + EXPECT_EQ(5u, activity_recorded_calls()); + + recorder.RecordDecryptionFailure(kTestAppId, + GCMDecryptionResult::INVALID_PAYLOAD); + EXPECT_EQ(6u, activity_recorded_calls()); + + RecordedActivities activities; + recorder.CollectActivities(&activities); + + EXPECT_EQ(4u, activities.registration_activities.size()); + EXPECT_EQ(1u, activities.receiving_activities.size()); + EXPECT_EQ(1u, activities.decryption_failure_activities.size()); + + recorder.CollectActivities(&activities); + + EXPECT_EQ(8u, activities.registration_activities.size()); + EXPECT_EQ(2u, activities.receiving_activities.size()); + EXPECT_EQ(2u, activities.decryption_failure_activities.size()); + + recorder.Clear(); + + RecordedActivities empty_activities; + recorder.CollectActivities(&empty_activities); + + EXPECT_EQ(0u, empty_activities.registration_activities.size()); + EXPECT_EQ(0u, empty_activities.receiving_activities.size()); + EXPECT_EQ(0u, empty_activities.decryption_failure_activities.size()); +} + +TEST_F(GCMStatsRecorderAndroidTest, NullDelegate) { + GCMStatsRecorderAndroid recorder(nullptr /* delegate */); + recorder.set_is_recording(true); + + ASSERT_TRUE(recorder.is_recording()); + + recorder.RecordRegistrationSent(kTestAppId); + + RecordedActivities activities; + recorder.CollectActivities(&activities); + + EXPECT_EQ(1u, activities.registration_activities.size()); +} + +TEST_F(GCMStatsRecorderAndroidTest, NotRecording) { + GCMStatsRecorderAndroid recorder(this /* delegate */); + ASSERT_FALSE(recorder.is_recording()); + + recorder.RecordRegistrationSent(kTestAppId); + + RecordedActivities activities; + recorder.CollectActivities(&activities); + + EXPECT_EQ(0u, activities.registration_activities.size()); +} + +} // namespace + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_stats_recorder_impl.cc b/chromium/components/gcm_driver/gcm_stats_recorder_impl.cc new file mode 100644 index 00000000000..d7af543792e --- /dev/null +++ b/chromium/components/gcm_driver/gcm_stats_recorder_impl.cc @@ -0,0 +1,514 @@ +// 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 "components/gcm_driver/gcm_stats_recorder_impl.h" + + +#include "base/containers/circular_deque.h" +#include "base/format_macros.h" +#include "base/logging.h" +#include "base/metrics/histogram_macros.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "components/gcm_driver/crypto/gcm_decryption_result.h" +#include "components/gcm_driver/crypto/gcm_encryption_provider.h" + +namespace gcm { + +const uint32_t MAX_LOGGED_ACTIVITY_COUNT = 100; + +namespace { + +// Insert an item to the front of deque while maintaining the size of the deque. +// Overflow item is discarded. +// +// DANGER: the returned pointer will not be valind if the queue is modified. +template +T* InsertCircularBuffer(base::circular_deque* q, const T& item) { + DCHECK(q); + if (q->size() > MAX_LOGGED_ACTIVITY_COUNT - 1) { + q->pop_back(); + } + q->push_front(item); + return &q->front(); +} + +// Helper for getting string representation of the MessageSendStatus enum. +std::string GetMessageSendStatusString( + gcm::MCSClient::MessageSendStatus status) { + switch (status) { + case gcm::MCSClient::QUEUED: + return "QUEUED"; + case gcm::MCSClient::SENT: + return "SENT"; + case gcm::MCSClient::QUEUE_SIZE_LIMIT_REACHED: + return "QUEUE_SIZE_LIMIT_REACHED"; + case gcm::MCSClient::APP_QUEUE_SIZE_LIMIT_REACHED: + return "APP_QUEUE_SIZE_LIMIT_REACHED"; + case gcm::MCSClient::MESSAGE_TOO_LARGE: + return "MESSAGE_TOO_LARGE"; + case gcm::MCSClient::NO_CONNECTION_ON_ZERO_TTL: + return "NO_CONNECTION_ON_ZERO_TTL"; + case gcm::MCSClient::TTL_EXCEEDED: + return "TTL_EXCEEDED"; + case gcm::MCSClient::SEND_STATUS_COUNT: + NOTREACHED(); + break; + } + return "UNKNOWN"; +} + +// Helper for getting string representation of the +// ConnectionFactory::ConnectionResetReason enum. +std::string GetConnectionResetReasonString( + gcm::ConnectionFactory::ConnectionResetReason reason) { + switch (reason) { + case gcm::ConnectionFactory::LOGIN_FAILURE: + return "LOGIN_FAILURE"; + case gcm::ConnectionFactory::CLOSE_COMMAND: + return "CLOSE_COMMAND"; + case gcm::ConnectionFactory::HEARTBEAT_FAILURE: + return "HEARTBEAT_FAILURE"; + case gcm::ConnectionFactory::SOCKET_FAILURE: + return "SOCKET_FAILURE"; + case gcm::ConnectionFactory::NETWORK_CHANGE: + return "NETWORK_CHANGE"; + case gcm::ConnectionFactory::NEW_HEARTBEAT_INTERVAL: + return "NEW_HEARTBEAT_INTERVAL"; + case gcm::ConnectionFactory::CONNECTION_RESET_COUNT: + NOTREACHED(); + break; + } + return "UNKNOWN_REASON"; +} + +// Helper for getting string representation of the RegistrationRequest::Status +// enum. +std::string GetRegistrationStatusString( + gcm::RegistrationRequest::Status status) { + switch (status) { + case gcm::RegistrationRequest::SUCCESS: + return "SUCCESS"; + case gcm::RegistrationRequest::INVALID_PARAMETERS: + return "INVALID_PARAMETERS"; + case gcm::RegistrationRequest::INVALID_SENDER: + return "INVALID_SENDER"; + case gcm::RegistrationRequest::AUTHENTICATION_FAILED: + return "AUTHENTICATION_FAILED"; + case gcm::RegistrationRequest::DEVICE_REGISTRATION_ERROR: + return "DEVICE_REGISTRATION_ERROR"; + case gcm::RegistrationRequest::UNKNOWN_ERROR: + return "UNKNOWN_ERROR"; + case gcm::RegistrationRequest::URL_FETCHING_FAILED: + return "URL_FETCHING_FAILED"; + case gcm::RegistrationRequest::HTTP_NOT_OK: + return "HTTP_NOT_OK"; + case gcm::RegistrationRequest::NO_RESPONSE_BODY: + return "NO_RESPONSE_BODY"; + case gcm::RegistrationRequest::REACHED_MAX_RETRIES: + return "REACHED_MAX_RETRIES"; + case gcm::RegistrationRequest::RESPONSE_PARSING_FAILED: + return "RESPONSE_PARSING_FAILED"; + case gcm::RegistrationRequest::INTERNAL_SERVER_ERROR: + return "INTERNAL_SERVER_ERROR"; + case gcm::RegistrationRequest::QUOTA_EXCEEDED: + return "QUOTA_EXCEEDED"; + case gcm::RegistrationRequest::TOO_MANY_REGISTRATIONS: + return "TOO_MANY_REGISTRATIONS"; + case gcm::RegistrationRequest::STATUS_COUNT: + NOTREACHED(); + break; + } + return "UNKNOWN_STATUS"; +} + +// Helper for getting string representation of the RegistrationRequest::Status +// enum. +std::string GetUnregistrationStatusString( + gcm::UnregistrationRequest::Status status) { + switch (status) { + case gcm::UnregistrationRequest::SUCCESS: + return "SUCCESS"; + case gcm::UnregistrationRequest::URL_FETCHING_FAILED: + return "URL_FETCHING_FAILED"; + case gcm::UnregistrationRequest::NO_RESPONSE_BODY: + return "NO_RESPONSE_BODY"; + case gcm::UnregistrationRequest::RESPONSE_PARSING_FAILED: + return "RESPONSE_PARSING_FAILED"; + case gcm::UnregistrationRequest::INCORRECT_APP_ID: + return "INCORRECT_APP_ID"; + case gcm::UnregistrationRequest::INVALID_PARAMETERS: + return "INVALID_PARAMETERS"; + case gcm::UnregistrationRequest::SERVICE_UNAVAILABLE: + return "SERVICE_UNAVAILABLE"; + case gcm::UnregistrationRequest::INTERNAL_SERVER_ERROR: + return "INTERNAL_SERVER_ERROR"; + case gcm::UnregistrationRequest::HTTP_NOT_OK: + return "HTTP_NOT_OK"; + case gcm::UnregistrationRequest::UNKNOWN_ERROR: + return "UNKNOWN_ERROR"; + case gcm::UnregistrationRequest::REACHED_MAX_RETRIES: + return "REACHED_MAX_RETRIES"; + case gcm::UnregistrationRequest::DEVICE_REGISTRATION_ERROR: + return "DEVICE_REGISTRATION_ERROR"; + case gcm::UnregistrationRequest::UNREGISTRATION_STATUS_COUNT: + NOTREACHED(); + break; + } + return "UNKNOWN_STATUS"; +} + +} // namespace + +GCMStatsRecorderImpl::GCMStatsRecorderImpl() + : is_recording_(false), delegate_(nullptr) {} + +GCMStatsRecorderImpl::~GCMStatsRecorderImpl() = default; + +void GCMStatsRecorderImpl::SetDelegate(Delegate* delegate) { + delegate_ = delegate; +} + +void GCMStatsRecorderImpl::Clear() { + checkin_activities_.clear(); + connection_activities_.clear(); + registration_activities_.clear(); + receiving_activities_.clear(); + sending_activities_.clear(); + decryption_failure_activities_.clear(); +} + +void GCMStatsRecorderImpl::NotifyActivityRecorded() { + if (delegate_) + delegate_->OnActivityRecorded(); +} + +void GCMStatsRecorderImpl::RecordDecryptionFailure(const std::string& app_id, + GCMDecryptionResult result) { + DCHECK_NE(result, GCMDecryptionResult::UNENCRYPTED); + DCHECK_NE(result, GCMDecryptionResult::DECRYPTED_DRAFT_03); + DCHECK_NE(result, GCMDecryptionResult::DECRYPTED_DRAFT_08); + if (!is_recording_) + return; + + DecryptionFailureActivity data; + DecryptionFailureActivity* inserted_data = InsertCircularBuffer( + &decryption_failure_activities_, data); + inserted_data->app_id = app_id; + inserted_data->details = ToGCMDecryptionResultDetailsString(result); + + NotifyActivityRecorded(); +} + +void GCMStatsRecorderImpl::RecordCheckin( + const std::string& event, + const std::string& details) { + CheckinActivity data; + CheckinActivity* inserted_data = InsertCircularBuffer( + &checkin_activities_, data); + inserted_data->event = event; + inserted_data->details = details; + NotifyActivityRecorded(); +} + +void GCMStatsRecorderImpl::RecordCheckinInitiated(uint64_t android_id) { + if (!is_recording_) + return; + RecordCheckin("Checkin initiated", + base::StringPrintf("Android Id: %" PRIu64, android_id)); +} + +void GCMStatsRecorderImpl::RecordCheckinDelayedDueToBackoff( + int64_t delay_msec) { + if (!is_recording_) + return; + RecordCheckin("Checkin backoff", + base::StringPrintf("Delayed for %" PRId64 " msec", + delay_msec)); +} + +void GCMStatsRecorderImpl::RecordCheckinSuccess() { + if (!is_recording_) + return; + RecordCheckin("Checkin succeeded", std::string()); +} + +void GCMStatsRecorderImpl::RecordCheckinFailure(const std::string& status, + bool will_retry) { + if (!is_recording_) + return; + RecordCheckin("Checkin failed", base::StringPrintf( + "%s.%s", + status.c_str(), + will_retry ? " Will retry." : "Will not retry.")); +} + +void GCMStatsRecorderImpl::RecordConnection( + const std::string& event, + const std::string& details) { + ConnectionActivity data; + ConnectionActivity* inserted_data = InsertCircularBuffer( + &connection_activities_, data); + inserted_data->event = event; + inserted_data->details = details; + NotifyActivityRecorded(); +} + +void GCMStatsRecorderImpl::RecordConnectionInitiated(const std::string& host) { + last_connection_initiation_time_ = base::TimeTicks::Now(); + if (!is_recording_) + return; + + RecordConnection("Connection initiated", host); +} + +void GCMStatsRecorderImpl::RecordConnectionDelayedDueToBackoff( + int64_t delay_msec) { + if (!is_recording_) + return; + + RecordConnection("Connection backoff", + base::StringPrintf("Delayed for %" PRId64 " msec", + delay_msec)); +} + +void GCMStatsRecorderImpl::RecordConnectionSuccess() { + DCHECK(!last_connection_initiation_time_.is_null()); + UMA_HISTOGRAM_MEDIUM_TIMES( + "GCM.ConnectionLatency", + (base::TimeTicks::Now() - last_connection_initiation_time_)); + last_connection_initiation_time_ = base::TimeTicks(); + if (!is_recording_) + return; + RecordConnection("Connection succeeded", std::string()); +} + +void GCMStatsRecorderImpl::RecordConnectionFailure(int network_error) { + if (!is_recording_) + return; + RecordConnection("Connection failed", + base::StringPrintf("With network error %d", network_error)); +} + +void GCMStatsRecorderImpl::RecordConnectionResetSignaled( + ConnectionFactory::ConnectionResetReason reason) { + if (!is_recording_) + return; + RecordConnection("Connection reset", + GetConnectionResetReasonString(reason)); +} + +void GCMStatsRecorderImpl::RecordRegistration( + const std::string& app_id, + const std::string& source, + const std::string& event, + const std::string& details) { + RegistrationActivity data; + RegistrationActivity* inserted_data = InsertCircularBuffer( + ®istration_activities_, data); + inserted_data->app_id = app_id; + inserted_data->source = source; + inserted_data->event = event; + inserted_data->details = details; + NotifyActivityRecorded(); +} + +void GCMStatsRecorderImpl::RecordRegistrationSent( + const std::string& app_id, + const std::string& sender_ids) { + UMA_HISTOGRAM_COUNTS_1M("GCM.RegistrationRequest", 1); + if (!is_recording_) + return; + RecordRegistration(app_id, sender_ids, + "Registration request sent", std::string()); +} + +void GCMStatsRecorderImpl::RecordRegistrationResponse( + const std::string& app_id, + const std::string& source, + RegistrationRequest::Status status) { + if (!is_recording_) + return; + RecordRegistration(app_id, source, + "Registration response received", + GetRegistrationStatusString(status)); +} + +void GCMStatsRecorderImpl::RecordRegistrationRetryDelayed( + const std::string& app_id, + const std::string& source, + int64_t delay_msec, + int retries_left) { + if (!is_recording_) + return; + RecordRegistration( + app_id, + source, + "Registration retry delayed", + base::StringPrintf("Delayed for %" PRId64 " msec, retries left: %d", + delay_msec, + retries_left)); +} + +void GCMStatsRecorderImpl::RecordUnregistrationSent( + const std::string& app_id, const std::string& source) { + UMA_HISTOGRAM_COUNTS_1M("GCM.UnregistrationRequest", 1); + if (!is_recording_) + return; + RecordRegistration(app_id, source, "Unregistration request sent", + std::string()); +} + +void GCMStatsRecorderImpl::RecordUnregistrationResponse( + const std::string& app_id, + const std::string& source, + UnregistrationRequest::Status status) { + if (!is_recording_) + return; + RecordRegistration(app_id, + source, + "Unregistration response received", + GetUnregistrationStatusString(status)); +} + +void GCMStatsRecorderImpl::RecordUnregistrationRetryDelayed( + const std::string& app_id, + const std::string& source, + int64_t delay_msec, + int retries_left) { + if (!is_recording_) + return; + RecordRegistration( + app_id, + source, + "Unregistration retry delayed", + base::StringPrintf("Delayed for %" PRId64 " msec, retries left: %d", + delay_msec, + retries_left)); +} + +void GCMStatsRecorderImpl::RecordReceiving( + const std::string& app_id, + const std::string& from, + int message_byte_size, + const std::string& event, + const std::string& details) { + ReceivingActivity data; + ReceivingActivity* inserted_data = InsertCircularBuffer( + &receiving_activities_, data); + inserted_data->app_id = app_id; + inserted_data->from = from; + inserted_data->message_byte_size = message_byte_size; + inserted_data->event = event; + inserted_data->details = details; + NotifyActivityRecorded(); +} + +void GCMStatsRecorderImpl::RecordDataMessageReceived( + const std::string& app_id, + const std::string& from, + int message_byte_size, + ReceivedMessageType message_type) { + if (!is_recording_) + return; + + switch (message_type) { + case GCMStatsRecorderImpl::DATA_MESSAGE: + RecordReceiving(app_id, from, message_byte_size, "Data msg received", + std::string()); + break; + case GCMStatsRecorderImpl::DELETED_MESSAGES: + RecordReceiving(app_id, from, message_byte_size, "Data msg received", + "Message has been deleted on server"); + break; + } +} + +void GCMStatsRecorderImpl::CollectActivities( + RecordedActivities* recorded_activities) const { + recorded_activities->checkin_activities.insert( + recorded_activities->checkin_activities.begin(), + checkin_activities_.begin(), + checkin_activities_.end()); + recorded_activities->connection_activities.insert( + recorded_activities->connection_activities.begin(), + connection_activities_.begin(), + connection_activities_.end()); + recorded_activities->registration_activities.insert( + recorded_activities->registration_activities.begin(), + registration_activities_.begin(), + registration_activities_.end()); + recorded_activities->receiving_activities.insert( + recorded_activities->receiving_activities.begin(), + receiving_activities_.begin(), + receiving_activities_.end()); + recorded_activities->sending_activities.insert( + recorded_activities->sending_activities.begin(), + sending_activities_.begin(), + sending_activities_.end()); + recorded_activities->decryption_failure_activities.insert( + recorded_activities->decryption_failure_activities.begin(), + decryption_failure_activities_.begin(), + decryption_failure_activities_.end()); +} + +void GCMStatsRecorderImpl::RecordSending(const std::string& app_id, + const std::string& receiver_id, + const std::string& message_id, + const std::string& event, + const std::string& details) { + SendingActivity data; + SendingActivity* inserted_data = InsertCircularBuffer( + &sending_activities_, data); + inserted_data->app_id = app_id; + inserted_data->receiver_id = receiver_id; + inserted_data->message_id = message_id; + inserted_data->event = event; + inserted_data->details = details; + NotifyActivityRecorded(); +} + +void GCMStatsRecorderImpl::RecordDataSentToWire( + const std::string& app_id, + const std::string& receiver_id, + const std::string& message_id, + int queued) { + if (!is_recording_) + return; + RecordSending(app_id, receiver_id, message_id, "Data msg sent to wire", + base::StringPrintf("Msg queued for %d seconds", queued)); +} + +void GCMStatsRecorderImpl::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) { + UMA_HISTOGRAM_ENUMERATION("GCM.SendMessageStatus", status, + gcm::MCSClient::SEND_STATUS_COUNT); + if (!is_recording_) + return; + RecordSending( + app_id, + receiver_id, + message_id, + base::StringPrintf("SEND status: %s", + GetMessageSendStatusString(status).c_str()), + base::StringPrintf("Msg size: %d bytes, TTL: %d", byte_size, ttl)); +} + +void GCMStatsRecorderImpl::RecordIncomingSendError( + const std::string& app_id, + const std::string& receiver_id, + const std::string& message_id) { + UMA_HISTOGRAM_COUNTS_1M("GCM.IncomingSendErrors", 1); + if (!is_recording_) + return; + RecordSending(app_id, receiver_id, message_id, "Received 'send error' msg", + std::string()); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/gcm_stats_recorder_impl.h b/chromium/components/gcm_driver/gcm_stats_recorder_impl.h new file mode 100644 index 00000000000..fca73d2ed0f --- /dev/null +++ b/chromium/components/gcm_driver/gcm_stats_recorder_impl.h @@ -0,0 +1,171 @@ +// 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 COMPONENTS_GCM_DRIVER_GCM_STATS_RECORDER_IMPL_H_ +#define COMPONENTS_GCM_DRIVER_GCM_STATS_RECORDER_IMPL_H_ + +#include + +#include + +#include "base/containers/circular_deque.h" +#include "base/memory/raw_ptr.h" +#include "base/time/time.h" +#include "components/gcm_driver/gcm_activity.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" +#include "google_apis/gcm/monitoring/gcm_stats_recorder.h" + +namespace gcm { + +enum class GCMDecryptionResult; + +// Records GCM internal stats and activities for debugging purpose. Recording +// can be turned on/off by calling set_is_recording(...) function. It is turned +// off by default. +// This class is not thread safe. It is meant to be owned by a gcm client +// instance. +class GCMStatsRecorderImpl : public GCMStatsRecorder { + public: + GCMStatsRecorderImpl(); + + GCMStatsRecorderImpl(const GCMStatsRecorderImpl&) = delete; + GCMStatsRecorderImpl& operator=(const GCMStatsRecorderImpl&) = delete; + + ~GCMStatsRecorderImpl() override; + + // Set a delegate to receive callback from the recorder. + void SetDelegate(Delegate* delegate); + + // Clear all recorded activities. + void Clear(); + + // Records a message decryption failure caused by |result| for |app_id|. + void RecordDecryptionFailure(const std::string& app_id, + GCMDecryptionResult result); + + // GCMStatsRecorder implementation: + void RecordCheckinInitiated(uint64_t android_id) override; + void RecordCheckinDelayedDueToBackoff(int64_t delay_msec) override; + void RecordCheckinSuccess() override; + void RecordCheckinFailure(const std::string& status, + bool will_retry) override; + void RecordConnectionInitiated(const std::string& host) override; + void RecordConnectionDelayedDueToBackoff(int64_t delay_msec) override; + void RecordConnectionSuccess() override; + void RecordConnectionFailure(int network_error) override; + void RecordConnectionResetSignaled( + ConnectionFactory::ConnectionResetReason reason) override; + void RecordRegistrationSent(const std::string& app_id, + const std::string& source) override; + void RecordRegistrationResponse(const std::string& app_id, + const std::string& source, + RegistrationRequest::Status status) override; + void RecordRegistrationRetryDelayed(const std::string& app_id, + const std::string& source, + int64_t delay_msec, + int retries_left) override; + void RecordUnregistrationSent(const std::string& app_id, + const std::string& source) override; + void RecordUnregistrationResponse( + const std::string& app_id, + const std::string& source, + UnregistrationRequest::Status status) override; + void RecordUnregistrationRetryDelayed(const std::string& app_id, + const std::string& source, + int64_t delay_msec, + int retries_left) override; + void RecordDataMessageReceived(const std::string& app_id, + const std::string& from, + int message_byte_size, + ReceivedMessageType message_type) override; + void RecordDataSentToWire(const std::string& app_id, + const std::string& receiver_id, + const std::string& message_id, + int queued) override; + 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; + void RecordIncomingSendError(const std::string& app_id, + const std::string& receiver_id, + const std::string& message_id) override; + + // Collect all recorded activities into |*recorded_activities|. + void CollectActivities(RecordedActivities* recorded_activities) const; + + bool is_recording() const { return is_recording_; } + void set_is_recording(bool recording) { is_recording_ = recording; } + + const base::circular_deque& checkin_activities() const { + return checkin_activities_; + } + const base::circular_deque& connection_activities() + const { + return connection_activities_; + } + const base::circular_deque& registration_activities() + const { + return registration_activities_; + } + const base::circular_deque& receiving_activities() const { + return receiving_activities_; + } + const base::circular_deque& sending_activities() const { + return sending_activities_; + } + const base::circular_deque& + decryption_failure_activities() const { + return decryption_failure_activities_; + } + + protected: + // Notify the recorder delegate, if it exists, that an activity has been + // recorded. + void NotifyActivityRecorded(); + + void RecordCheckin(const std::string& event, + const std::string& details); + + void RecordConnection(const std::string& event, + const std::string& details); + + void RecordRegistration(const std::string& app_id, + const std::string& source, + const std::string& event, + const std::string& details); + + void RecordReceiving(const std::string& app_id, + const std::string& from, + int message_byte_size, + const std::string& event, + const std::string& details); + + void RecordSending(const std::string& app_id, + const std::string& receiver_id, + const std::string& message_id, + const std::string& event, + const std::string& details); + + bool is_recording_; + raw_ptr delegate_; + + base::circular_deque checkin_activities_; + base::circular_deque connection_activities_; + base::circular_deque registration_activities_; + base::circular_deque receiving_activities_; + base::circular_deque sending_activities_; + base::circular_deque + decryption_failure_activities_; + + base::TimeTicks last_connection_initiation_time_; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_GCM_STATS_RECORDER_IMPL_H_ diff --git a/chromium/components/gcm_driver/gcm_stats_recorder_impl_unittest.cc b/chromium/components/gcm_driver/gcm_stats_recorder_impl_unittest.cc new file mode 100644 index 00000000000..f9076b99ad3 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_stats_recorder_impl_unittest.cc @@ -0,0 +1,536 @@ +// 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 "components/gcm_driver/gcm_stats_recorder_impl.h" + +#include + +#include + +#include "base/containers/circular_deque.h" +#include "components/gcm_driver/crypto/gcm_decryption_result.h" +#include "components/gcm_driver/crypto/gcm_encryption_provider.h" +#include "google_apis/gcm/engine/mcs_client.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { + +namespace { + +static uint64_t kAndroidId = 4U; +static const char kCheckinStatus[] = "URL_FETCHING_FAILED"; +static const char kHost[] = "www.example.com"; +static const char kAppId[] = "app id 1"; +static const char kFrom[] = "from"; +static const char kSenderIds[] = "s1,s2"; +static const char kReceiverId[] = "receiver 1"; +static const char kMessageId[] = "message id 1"; +static const int kQueuedSec = 5; +static const gcm::MCSClient::MessageSendStatus kMessageSendStatus = + gcm::MCSClient::QUEUED; +static const int kByteSize = 99; +static const int kTTL = 7; +static const int kRetries = 3; +static const int64_t kDelay = 15000; +static const ConnectionFactory::ConnectionResetReason kReason = + ConnectionFactory::NETWORK_CHANGE; +static const int kNetworkError = 1; + +static const RegistrationRequest::Status kRegistrationStatus = + RegistrationRequest::SUCCESS; +static const UnregistrationRequest::Status kUnregistrationStatus = + UnregistrationRequest::SUCCESS; + +static const char kCheckinInitiatedEvent[] = "Checkin initiated"; +static const char kCheckinInitiatedDetails[] = "Android Id: 4"; +static const char kCheckinDelayedDueToBackoffEvent[] = "Checkin backoff"; +static const char kCheckinDelayedDueToBackoffDetails[] = + "Delayed for 15000 msec"; +static const char kCheckinSuccessEvent[] = "Checkin succeeded"; +static const char kCheckinSuccessDetails[] = ""; +static const char kCheckinFailureEvent[] = "Checkin failed"; +static const char kCheckinFailureDetails[] = "URL_FETCHING_FAILED. Will retry."; + +static const char kConnectionInitiatedEvent[] = "Connection initiated"; +static const char kConnectionInitiatedDetails[] = "www.example.com"; +static const char kConnectionDelayedDueToBackoffEvent[] = "Connection backoff"; +static const char kConnectionDelayedDueToBackoffDetails[] = + "Delayed for 15000 msec"; +static const char kConnectionSuccessEvent[] = "Connection succeeded"; +static const char kConnectionSuccessDetails[] = ""; +static const char kConnectionFailureEvent[] = "Connection failed"; +static const char kConnectionFailureDetails[] = "With network error 1"; +static const char kConnectionResetSignaledEvent[] = "Connection reset"; +static const char kConnectionResetSignaledDetails[] = "NETWORK_CHANGE"; + +static const char kRegistrationSentEvent[] = "Registration request sent"; +static const char kRegistrationSentDetails[] = ""; +static const char kRegistrationResponseEvent[] = + "Registration response received"; +static const char kRegistrationResponseDetails[] = "SUCCESS"; +static const char kRegistrationRetryDelayedEvent[] = + "Registration retry delayed"; +static const char kRegistrationRetryDelayedDetails[] = + "Delayed for 15000 msec, retries left: 3"; +static const char kUnregistrationSentEvent[] = "Unregistration request sent"; +static const char kUnregistrationSentDetails[] = ""; +static const char kUnregistrationResponseEvent[] = + "Unregistration response received"; +static const char kUnregistrationResponseDetails[] = "SUCCESS"; +static const char kUnregistrationRetryDelayedEvent[] = + "Unregistration retry delayed"; +static const char kUnregistrationRetryDelayedDetails[] = + "Delayed for 15000 msec, retries left: 3"; + +static const char kDataReceivedEvent[] = "Data msg received"; +static const char kDataReceivedDetails[] = ""; +static const char kDataDeletedMessageEvent[] = "Data msg received"; +static const char kDataDeletedMessageDetails[] = + "Message has been deleted on server"; + +static const char kDataSentToWireEvent[] = "Data msg sent to wire"; +static const char kSentToWireDetails[] = "Msg queued for 5 seconds"; +static const char kNotifySendStatusEvent[] = "SEND status: QUEUED"; +static const char kNotifySendStatusDetails[] = "Msg size: 99 bytes, TTL: 7"; +static const char kIncomingSendErrorEvent[] = "Received 'send error' msg"; +static const char kIncomingSendErrorDetails[] = ""; + +static const GCMDecryptionResult kDecryptionResultFailure = + GCMDecryptionResult::INVALID_PAYLOAD; + +} // namespace + +class GCMStatsRecorderImplTest : public testing::Test { + public: + GCMStatsRecorderImplTest(); + ~GCMStatsRecorderImplTest() override; + void SetUp() override; + + void VerifyRecordedCheckinCount(int expected_count) { + EXPECT_EQ(expected_count, + static_cast(recorder_.checkin_activities().size())); + } + void VerifyRecordedConnectionCount(int expected_count) { + EXPECT_EQ(expected_count, + static_cast(recorder_.connection_activities().size())); + } + void VerifyRecordedRegistrationCount(int expected_count) { + EXPECT_EQ(expected_count, + static_cast(recorder_.registration_activities().size())); + } + void VerifyRecordedReceivingCount(int expected_count) { + EXPECT_EQ(expected_count, + static_cast(recorder_.receiving_activities().size())); + } + void VerifyRecordedSendingCount(int expected_count) { + EXPECT_EQ(expected_count, + static_cast(recorder_.sending_activities().size())); + } + void VerifyRecordedDecryptionFailureCount(int expected_count) { + EXPECT_EQ( + expected_count, + static_cast(recorder_.decryption_failure_activities().size())); + } + void VerifyAllActivityQueueEmpty(const std::string& remark) { + EXPECT_TRUE(recorder_.checkin_activities().empty()) << remark; + EXPECT_TRUE(recorder_.connection_activities().empty()) << remark; + EXPECT_TRUE(recorder_.registration_activities().empty()) << remark; + EXPECT_TRUE(recorder_.receiving_activities().empty()) << remark; + EXPECT_TRUE(recorder_.sending_activities().empty()) << remark; + EXPECT_TRUE(recorder_.decryption_failure_activities().empty()) << remark; + } + + void VerifyCheckinInitiated(const std::string& remark) { + VerifyCheckin(recorder_.checkin_activities(), + kCheckinInitiatedEvent, + kCheckinInitiatedDetails, + remark); + } + + void VerifyCheckinDelayedDueToBackoff(const std::string& remark) { + VerifyCheckin(recorder_.checkin_activities(), + kCheckinDelayedDueToBackoffEvent, + kCheckinDelayedDueToBackoffDetails, + remark); + } + + void VerifyCheckinSuccess(const std::string& remark) { + VerifyCheckin(recorder_.checkin_activities(), + kCheckinSuccessEvent, + kCheckinSuccessDetails, + remark); + } + + void VerifyCheckinFailure(const std::string& remark) { + VerifyCheckin(recorder_.checkin_activities(), + kCheckinFailureEvent, + kCheckinFailureDetails, + remark); + } + + void VerifyConnectionInitiated(const std::string& remark) { + VerifyConnection(recorder_.connection_activities(), + kConnectionInitiatedEvent, + kConnectionInitiatedDetails, + remark); + } + + void VerifyConnectionDelayedDueToBackoff(const std::string& remark) { + VerifyConnection(recorder_.connection_activities(), + kConnectionDelayedDueToBackoffEvent, + kConnectionDelayedDueToBackoffDetails, + remark); + } + + void VerifyConnectionSuccess(const std::string& remark) { + VerifyConnection(recorder_.connection_activities(), + kConnectionSuccessEvent, + kConnectionSuccessDetails, + remark); + } + + void VerifyConnectionFailure(const std::string& remark) { + VerifyConnection(recorder_.connection_activities(), + kConnectionFailureEvent, + kConnectionFailureDetails, + remark); + } + + void VerifyConnectionResetSignaled(const std::string& remark) { + VerifyConnection(recorder_.connection_activities(), + kConnectionResetSignaledEvent, + kConnectionResetSignaledDetails, + remark); + } + + void VerifyRegistrationSent(const std::string& remark) { + VerifyRegistration(recorder_.registration_activities(), + kSenderIds, + kRegistrationSentEvent, + kRegistrationSentDetails, + remark); + } + + void VerifyRegistrationResponse(const std::string& remark) { + VerifyRegistration(recorder_.registration_activities(), + kSenderIds, + kRegistrationResponseEvent, + kRegistrationResponseDetails, + remark); + } + + void VerifyRegistrationRetryRequested(const std::string& remark) { + VerifyRegistration(recorder_.registration_activities(), + kSenderIds, + kRegistrationRetryDelayedEvent, + kRegistrationRetryDelayedDetails, + remark); + } + + void VerifyUnregistrationSent(const std::string& remark) { + VerifyRegistration(recorder_.registration_activities(), + kSenderIds, + kUnregistrationSentEvent, + kUnregistrationSentDetails, + remark); + } + + void VerifyUnregistrationResponse(const std::string& remark) { + VerifyRegistration(recorder_.registration_activities(), + kSenderIds, + kUnregistrationResponseEvent, + kUnregistrationResponseDetails, + remark); + } + + void VerifyUnregistrationRetryDelayed(const std::string& remark) { + VerifyRegistration(recorder_.registration_activities(), + kSenderIds, + kUnregistrationRetryDelayedEvent, + kUnregistrationRetryDelayedDetails, + remark); + } + + void VerifyDataMessageReceived(const std::string& remark) { + VerifyReceivingData(recorder_.receiving_activities(), + kDataReceivedEvent, + kDataReceivedDetails, + remark); + } + + void VerifyDataDeletedMessage(const std::string& remark) { + VerifyReceivingData(recorder_.receiving_activities(), + kDataDeletedMessageEvent, + kDataDeletedMessageDetails, + remark); + } + + void VerifyDataSentToWire(const std::string& remark) { + VerifySendingData(recorder_.sending_activities(), + kDataSentToWireEvent, + kSentToWireDetails, + remark); + } + + void VerifyNotifySendStatus(const std::string& remark) { + VerifySendingData(recorder_.sending_activities(), + kNotifySendStatusEvent, + kNotifySendStatusDetails, + remark); + } + + void VerifyIncomingSendError(const std::string& remark) { + VerifySendingData(recorder_.sending_activities(), + kIncomingSendErrorEvent, + kIncomingSendErrorDetails, + remark); + } + + void VerifyRecordedDecryptionFailure(const std::string& remark) { + const auto& queue = recorder_.decryption_failure_activities(); + + EXPECT_EQ(kAppId, queue.front().app_id) << remark; + EXPECT_EQ(ToGCMDecryptionResultDetailsString(kDecryptionResultFailure), + queue.front().details) + << remark; + } + + protected: + void VerifyCheckin(const base::circular_deque& queue, + const std::string& event, + const std::string& details, + const std::string& remark) { + EXPECT_EQ(event, queue.front().event) << remark; + EXPECT_EQ(details, queue.front().details) << remark; + } + + void VerifyConnection(const base::circular_deque& queue, + const std::string& event, + const std::string& details, + const std::string& remark) { + EXPECT_EQ(event, queue.front().event) << remark; + EXPECT_EQ(details, queue.front().details) << remark; + } + + void VerifyRegistration( + const base::circular_deque& queue, + const std::string& source, + const std::string& event, + const std::string& details, + const std::string& remark) { + EXPECT_EQ(kAppId, queue.front().app_id) << remark; + EXPECT_EQ(source, queue.front().source) << remark; + EXPECT_EQ(event, queue.front().event) << remark; + EXPECT_EQ(details, queue.front().details) << remark; + } + + void VerifyReceivingData(const base::circular_deque& queue, + const std::string& event, + const std::string& details, + const std::string& remark) { + EXPECT_EQ(kAppId, queue.front().app_id) << remark; + EXPECT_EQ(kFrom, queue.front().from) << remark; + EXPECT_EQ(kByteSize, queue.front().message_byte_size) << remark; + EXPECT_EQ(event, queue.front().event) << remark; + EXPECT_EQ(details, queue.front().details) << remark; + } + + void VerifySendingData(const base::circular_deque& queue, + const std::string& event, + const std::string& details, + const std::string& remark) { + EXPECT_EQ(kAppId, queue.front().app_id) << remark; + EXPECT_EQ(kReceiverId, queue.front().receiver_id) << remark; + EXPECT_EQ(kMessageId, queue.front().message_id) << remark; + EXPECT_EQ(event, queue.front().event) << remark; + EXPECT_EQ(details, queue.front().details) << remark; + } + + std::string source_; + GCMStatsRecorderImpl recorder_; +}; + +GCMStatsRecorderImplTest::GCMStatsRecorderImplTest(){ +} + +GCMStatsRecorderImplTest::~GCMStatsRecorderImplTest() {} + +void GCMStatsRecorderImplTest::SetUp(){ + source_ = "s1,s2"; + recorder_.set_is_recording(true); +} + +TEST_F(GCMStatsRecorderImplTest, StartStopRecordingTest) { + EXPECT_TRUE(recorder_.is_recording()); + recorder_.RecordDataSentToWire(kAppId, kReceiverId, kMessageId, kQueuedSec); + VerifyRecordedSendingCount(1); + VerifyDataSentToWire("1st call"); + + recorder_.set_is_recording(false); + EXPECT_FALSE(recorder_.is_recording()); + recorder_.Clear(); + VerifyAllActivityQueueEmpty("all cleared"); + + // Exercise every recording method below and verify that nothing is recorded. + recorder_.RecordCheckinInitiated(kAndroidId); + recorder_.RecordCheckinDelayedDueToBackoff(kDelay); + recorder_.RecordCheckinSuccess(); + recorder_.RecordCheckinFailure(kCheckinStatus, true); + VerifyAllActivityQueueEmpty("no checkin"); + + recorder_.RecordConnectionInitiated(kHost); + recorder_.RecordConnectionDelayedDueToBackoff(kDelay); + recorder_.RecordConnectionSuccess(); + recorder_.RecordConnectionFailure(kNetworkError); + recorder_.RecordConnectionResetSignaled(kReason); + VerifyAllActivityQueueEmpty("no registration"); + + recorder_.RecordRegistrationSent(kAppId, kSenderIds); + recorder_.RecordRegistrationResponse(kAppId, source_, + kRegistrationStatus); + recorder_.RecordRegistrationRetryDelayed(kAppId, source_, kDelay, kRetries); + recorder_.RecordUnregistrationSent(kAppId, source_); + recorder_.RecordUnregistrationResponse( + kAppId, source_, kUnregistrationStatus); + recorder_.RecordUnregistrationRetryDelayed(kAppId, source_, kDelay, kRetries); + VerifyAllActivityQueueEmpty("no unregistration"); + + recorder_.RecordDataMessageReceived(kAppId, kFrom, kByteSize, + GCMStatsRecorder::DATA_MESSAGE); + recorder_.RecordDataMessageReceived(kAppId, kFrom, kByteSize, + GCMStatsRecorder::DELETED_MESSAGES); + VerifyAllActivityQueueEmpty("no receiving"); + + recorder_.RecordDataSentToWire(kAppId, kReceiverId, kMessageId, kQueuedSec); + recorder_.RecordNotifySendStatus(kAppId, kReceiverId, kMessageId, + kMessageSendStatus, kByteSize, kTTL); + recorder_.RecordIncomingSendError(kAppId, kReceiverId, kMessageId); + recorder_.RecordDataSentToWire(kAppId, kReceiverId, kMessageId, kQueuedSec); + VerifyAllActivityQueueEmpty("no sending"); +} + +TEST_F(GCMStatsRecorderImplTest, ClearLogTest) { + recorder_.RecordDataSentToWire(kAppId, kReceiverId, kMessageId, kQueuedSec); + VerifyRecordedSendingCount(1); + VerifyDataSentToWire("1st call"); + + recorder_.RecordNotifySendStatus(kAppId, kReceiverId, kMessageId, + kMessageSendStatus, kByteSize, kTTL); + VerifyRecordedSendingCount(2); + VerifyNotifySendStatus("2nd call"); + + recorder_.Clear(); + VerifyRecordedSendingCount(0); +} + +// This test is flaky, see https://crbug.com/1010462 +TEST_F(GCMStatsRecorderImplTest, DISABLED_CheckinTest) { + recorder_.RecordCheckinInitiated(kAndroidId); + VerifyRecordedCheckinCount(1); + VerifyCheckinInitiated("1st call"); + + recorder_.RecordCheckinDelayedDueToBackoff(kDelay); + VerifyRecordedCheckinCount(2); + VerifyCheckinDelayedDueToBackoff("2nd call"); + + recorder_.RecordCheckinSuccess(); + VerifyRecordedCheckinCount(3); + VerifyCheckinSuccess("3rd call"); + + recorder_.RecordCheckinFailure(kCheckinStatus, true); + VerifyRecordedCheckinCount(4); + VerifyCheckinFailure("4th call"); +} + +TEST_F(GCMStatsRecorderImplTest, ConnectionTest) { + recorder_.RecordConnectionInitiated(kHost); + VerifyRecordedConnectionCount(1); + VerifyConnectionInitiated("1st call"); + + recorder_.RecordConnectionDelayedDueToBackoff(kDelay); + VerifyRecordedConnectionCount(2); + VerifyConnectionDelayedDueToBackoff("2nd call"); + + recorder_.RecordConnectionSuccess(); + VerifyRecordedConnectionCount(3); + VerifyConnectionSuccess("3rd call"); + + recorder_.RecordConnectionFailure(kNetworkError); + VerifyRecordedConnectionCount(4); + VerifyConnectionFailure("4th call"); + + recorder_.RecordConnectionResetSignaled(kReason); + VerifyRecordedConnectionCount(5); + VerifyConnectionResetSignaled("5th call"); +} + +TEST_F(GCMStatsRecorderImplTest, RegistrationTest) { + recorder_.RecordRegistrationSent(kAppId, kSenderIds); + VerifyRecordedRegistrationCount(1); + VerifyRegistrationSent("1st call"); + + recorder_.RecordRegistrationResponse(kAppId, source_, + kRegistrationStatus); + VerifyRecordedRegistrationCount(2); + VerifyRegistrationResponse("2nd call"); + + recorder_.RecordRegistrationRetryDelayed(kAppId, source_, kDelay, kRetries); + VerifyRecordedRegistrationCount(3); + VerifyRegistrationRetryRequested("3rd call"); + + recorder_.RecordUnregistrationSent(kAppId, source_); + VerifyRecordedRegistrationCount(4); + VerifyUnregistrationSent("4th call"); + + recorder_.RecordUnregistrationResponse( + kAppId, source_, kUnregistrationStatus); + VerifyRecordedRegistrationCount(5); + VerifyUnregistrationResponse("5th call"); + + recorder_.RecordUnregistrationRetryDelayed(kAppId, source_, kDelay, kRetries); + VerifyRecordedRegistrationCount(6); + VerifyUnregistrationRetryDelayed("6th call"); +} + +TEST_F(GCMStatsRecorderImplTest, RecordReceivingTest) { + recorder_.RecordConnectionInitiated(std::string()); + recorder_.RecordConnectionSuccess(); + recorder_.RecordDataMessageReceived(kAppId, kFrom, kByteSize, + GCMStatsRecorder::DATA_MESSAGE); + VerifyRecordedReceivingCount(1); + VerifyDataMessageReceived("1st call"); + + recorder_.RecordDataMessageReceived(kAppId, kFrom, kByteSize, + GCMStatsRecorder::DELETED_MESSAGES); + VerifyRecordedReceivingCount(2); + VerifyDataDeletedMessage("2nd call"); +} + +TEST_F(GCMStatsRecorderImplTest, RecordSendingTest) { + recorder_.RecordDataSentToWire(kAppId, kReceiverId, kMessageId, kQueuedSec); + VerifyRecordedSendingCount(1); + VerifyDataSentToWire("1st call"); + + recorder_.RecordNotifySendStatus(kAppId, kReceiverId, kMessageId, + kMessageSendStatus, kByteSize, kTTL); + VerifyRecordedSendingCount(2); + VerifyNotifySendStatus("2nd call"); + + recorder_.RecordIncomingSendError(kAppId, kReceiverId, kMessageId); + VerifyRecordedSendingCount(3); + VerifyIncomingSendError("3rd call"); + + recorder_.RecordDataSentToWire(kAppId, kReceiverId, kMessageId, kQueuedSec); + VerifyRecordedSendingCount(4); + VerifyDataSentToWire("4th call"); +} + +TEST_F(GCMStatsRecorderImplTest, RecordDecryptionFailureTest) { + recorder_.RecordDecryptionFailure(kAppId, kDecryptionResultFailure); + VerifyRecordedDecryptionFailureCount(1); + + VerifyRecordedDecryptionFailure("1st call"); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/instance_id/DEPS b/chromium/components/gcm_driver/instance_id/DEPS new file mode 100644 index 00000000000..2d411161aa4 --- /dev/null +++ b/chromium/components/gcm_driver/instance_id/DEPS @@ -0,0 +1,4 @@ +include_rules = [ + "+components/gcm_driver", + "+crypto", +] diff --git a/chromium/components/gcm_driver/instance_id/android/javatests/src/org/chromium/components/gcm_driver/instance_id/FakeInstanceIDWithSubtype.java b/chromium/components/gcm_driver/instance_id/android/javatests/src/org/chromium/components/gcm_driver/instance_id/FakeInstanceIDWithSubtype.java new file mode 100644 index 00000000000..0c467469145 --- /dev/null +++ b/chromium/components/gcm_driver/instance_id/android/javatests/src/org/chromium/components/gcm_driver/instance_id/FakeInstanceIDWithSubtype.java @@ -0,0 +1,198 @@ +// Copyright 2016 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. + +package org.chromium.components.gcm_driver.instance_id; + +import android.os.Looper; +import android.util.Pair; + +import com.google.android.gms.iid.InstanceID; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +/** + * Fake for InstanceIDWithSubtype. Doesn't hit the network or filesystem (so instance IDs don't + * survive restarts, and sending messages to tokens via the GCM server won't work). + */ +@JNINamespace("instance_id") +public class FakeInstanceIDWithSubtype extends InstanceIDWithSubtype { + private String mSubtype; + private String mId; + private long mCreationTime; + + /** Map from (subtype + ',' + authorizedEntity + ',' + scope) to token. */ + private Map mTokens = new HashMap<>(); + + /** + * Enable this in all InstanceID tests to use this fake instead of hitting the network/disk. + * @return The previous value. + */ + @CalledByNative + public static boolean clearDataAndSetEnabled(boolean enable) { + synchronized (sSubtypeInstancesLock) { + sSubtypeInstances.clear(); + boolean wasEnabled = sFakeFactoryForTesting != null; + if (enable) { + sFakeFactoryForTesting = new FakeFactory() { + @Override + public InstanceIDWithSubtype create(String subtype) { + return new FakeInstanceIDWithSubtype(subtype); + } + }; + } else { + sFakeFactoryForTesting = null; + } + return wasEnabled; + } + } + + /** + * If exactly one instance of InstanceID exists, and it has exactly one token, this returns + * the subtype of the InstanceID and the authorizedEntity of the token. Otherwise it throws. + * If a test fails with no InstanceID or no tokens, it probably means subscribing failed, or + * that the test subscribed in the wrong way (e.g. a GCM registration rather than an InstanceID + * token). If a test fails with too many InstanceIDs/tokens, the test subscribed too many times. + */ + public static Pair getSubtypeAndAuthorizedEntityOfOnlyToken() { + synchronized (sSubtypeInstancesLock) { + if (sSubtypeInstances.size() != 1) { + throw new IllegalStateException("Expected exactly one InstanceID, but there are " + + sSubtypeInstances.size()); + } + final String subType = sSubtypeInstances.values().iterator().next().getSubtype(); + return Pair.create(subType, getAuthorizedEntityForSubtype(subType)); + } + } + + /** + * If an instanceID exists for subtype, and it has exactly one token, this returns + * the authorizedEntity of the token. Otherwise it throws. + */ + public static String getAuthorizedEntityForSubtype(String subtype) { + synchronized (sSubtypeInstancesLock) { + FakeInstanceIDWithSubtype iid = + (FakeInstanceIDWithSubtype) sSubtypeInstances.get(subtype); + if (iid == null) { + throw new IllegalStateException("No subtype instance found for " + subtype); + } + if (iid.mTokens.size() != 1) { + throw new IllegalStateException( + "Expected exactly one token, but there are " + iid.mTokens.size()); + } + return iid.mTokens.keySet().iterator().next().split(",", 3)[1]; + } + } + + private FakeInstanceIDWithSubtype(String subtype) { + super(null); + mSubtype = subtype; + + // The first call to InstanceIDWithSubtype.getInstance calls InstanceID.getInstance which + // triggers a strict mode violation if it's called on the main thread, by reading from + // SharedPreferences. Since we can't override those static methods to simulate the strict + // mode violation in tests, check the thread here (which is only called from getInstance). + if (Looper.getMainLooper() == Looper.myLooper()) { + throw new AssertionError(InstanceID.ERROR_MAIN_THREAD); + } + } + + @Override + public String getSubtype() { + return mSubtype; + } + + @Override + public String getId() { + // InstanceID.getId sometimes triggers a strict mode violation if it's called on the main + // thread, by reading from SharedPreferences. + if (Looper.getMainLooper() == Looper.myLooper()) { + throw new AssertionError(InstanceID.ERROR_MAIN_THREAD); + } + + if (mId == null) { + mCreationTime = System.currentTimeMillis(); + mId = randomBase64(11 /* length */); + } + return mId; + } + + @Override + public long getCreationTime() { + // InstanceID.getCreationTime sometimes triggers a strict mode violation if it's called on + // the main thread, by reading from SharedPreferences. + if (Looper.getMainLooper() == Looper.myLooper()) { + throw new AssertionError(InstanceID.ERROR_MAIN_THREAD); + } + + return mCreationTime; + } + + @Override + public String getToken(String authorizedEntity, String scope) throws IOException { + // InstanceID.getToken enforces this. + if (Looper.getMainLooper() == Looper.myLooper()) { + throw new IOException(InstanceID.ERROR_MAIN_THREAD); + } + + String key = getSubtype() + ',' + authorizedEntity + ',' + scope; + String token = mTokens.get(key); + if (token == null) { + getId(); + token = mId + ':' + randomBase64(140 /* length */); + mTokens.put(key, token); + } + return token; + } + + @Override + public void deleteToken(String authorizedEntity, String scope) throws IOException { + // InstanceID.deleteToken enforces this. + if (Looper.getMainLooper() == Looper.myLooper()) { + throw new IOException(InstanceID.ERROR_MAIN_THREAD); + } + + String key = getSubtype() + ',' + authorizedEntity + ',' + scope; + mTokens.remove(key); + // Calling deleteToken causes ID to be generated; can be observed though getCreationTime. + getId(); + } + + @Override + public void deleteInstanceID() throws IOException { + synchronized (sSubtypeInstancesLock) { + sSubtypeInstances.remove(getSubtype()); + + // InstanceID.deleteInstanceID calls InstanceID.deleteToken which enforces this. + if (Looper.getMainLooper() == Looper.myLooper()) { + throw new IOException(InstanceID.ERROR_MAIN_THREAD); + } + + mTokens.clear(); + mCreationTime = 0; + mId = null; + } + } + + /** Returns a random base64url encoded string. */ + private static String randomBase64(int encodedLength) { + // It would probably make more sense for this method to produce fixed-length plaintext, + // rather than fixed-length encodings that correspond to variable-length plaintext. + // But the added randomness helps avoid us depending on the length of tokens GCM gives us. + final String base64urlAlphabet = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_"; + Random random = new Random(); + StringBuilder sb = new StringBuilder(encodedLength); + for (int i = 0; i < encodedLength; i++) { + int index = random.nextInt(base64urlAlphabet.length()); + sb.append(base64urlAlphabet.charAt(index)); + } + return sb.toString(); + } +} diff --git a/chromium/components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.cc b/chromium/components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.cc new file mode 100644 index 00000000000..a33c49aeb2f --- /dev/null +++ b/chromium/components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.cc @@ -0,0 +1,121 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.h" + +#include "base/bind.h" +#include "base/location.h" +#include "base/rand_util.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/task/single_thread_task_runner.h" +#include "base/threading/thread_task_runner_handle.h" +#include "components/gcm_driver/gcm_client.h" + +namespace instance_id { + +FakeGCMDriverForInstanceID::FakeGCMDriverForInstanceID() + : gcm::FakeGCMDriver(base::ThreadTaskRunnerHandle::Get()) {} + +FakeGCMDriverForInstanceID::FakeGCMDriverForInstanceID( + const scoped_refptr& blocking_task_runner) + : FakeGCMDriver(blocking_task_runner) {} + +FakeGCMDriverForInstanceID::~FakeGCMDriverForInstanceID() { +} + +gcm::InstanceIDHandler* +FakeGCMDriverForInstanceID::GetInstanceIDHandlerInternal() { + return this; +} + +void FakeGCMDriverForInstanceID::AddInstanceIDData( + const std::string& app_id, + const std::string& instance_id, + const std::string& extra_data) { + instance_id_data_[app_id] = std::make_pair(instance_id, extra_data); +} + +void FakeGCMDriverForInstanceID::RemoveInstanceIDData( + const std::string& app_id) { + instance_id_data_.erase(app_id); +} + +void FakeGCMDriverForInstanceID::GetInstanceIDData( + const std::string& app_id, + GetInstanceIDDataCallback callback) { + auto iter = instance_id_data_.find(app_id); + std::string instance_id; + std::string extra_data; + if (iter != instance_id_data_.end()) { + instance_id = iter->second.first; + extra_data = iter->second.second; + } + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), instance_id, extra_data)); +} + +void FakeGCMDriverForInstanceID::GetToken( + const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + base::TimeDelta time_to_live, + GetTokenCallback callback) { + std::string key = app_id + authorized_entity + scope; + auto iter = tokens_.find(key); + std::string token; + if (iter != tokens_.end()) { + token = iter->second; + } else { + token = base::NumberToString(base::RandUint64()); + tokens_[key] = token; + } + + last_gettoken_app_id_ = app_id; + last_gettoken_authorized_entity_ = authorized_entity; + + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(std::move(callback), token, gcm::GCMClient::SUCCESS)); +} + +void FakeGCMDriverForInstanceID::ValidateToken( + const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + const std::string& token, + ValidateTokenCallback callback) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), true /* is_valid */)); +} + +void FakeGCMDriverForInstanceID::DeleteToken( + const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + DeleteTokenCallback callback) { + std::string key_prefix = app_id; + + // Calls to InstanceID::DeleteID() will end up deleting the token for a given + // |app_id| with both |authorized_entity| and |scope| set to "*", meaning that + // all data has to be deleted. Do a prefix search to emulate this behaviour. + if (authorized_entity != "*") + key_prefix += authorized_entity; + if (scope != "*") + key_prefix += scope; + + for (auto iter = tokens_.begin(); iter != tokens_.end();) { + if (base::StartsWith(iter->first, key_prefix, base::CompareCase::SENSITIVE)) + iter = tokens_.erase(iter); + else + iter++; + } + + last_deletetoken_app_id_ = app_id; + + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), gcm::GCMClient::SUCCESS)); +} + +} // namespace instance_id diff --git a/chromium/components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.h b/chromium/components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.h new file mode 100644 index 00000000000..2f133802825 --- /dev/null +++ b/chromium/components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.h @@ -0,0 +1,80 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_GCM_DRIVER_INSTANCE_ID_FAKE_GCM_DRIVER_FOR_INSTANCE_ID_H_ +#define COMPONENTS_GCM_DRIVER_INSTANCE_ID_FAKE_GCM_DRIVER_FOR_INSTANCE_ID_H_ + +#include +#include +#include + +#include "base/compiler_specific.h" +#include "components/gcm_driver/fake_gcm_driver.h" + +namespace base { +class SequencedTaskRunner; +} + +namespace instance_id { + +class FakeGCMDriverForInstanceID : public gcm::FakeGCMDriver, + protected gcm::InstanceIDHandler { + public: + FakeGCMDriverForInstanceID(); + explicit FakeGCMDriverForInstanceID( + const scoped_refptr& blocking_task_runner); + + FakeGCMDriverForInstanceID(const FakeGCMDriverForInstanceID&) = delete; + FakeGCMDriverForInstanceID& operator=(const FakeGCMDriverForInstanceID&) = + delete; + + ~FakeGCMDriverForInstanceID() override; + + // FakeGCMDriver overrides: + gcm::InstanceIDHandler* GetInstanceIDHandlerInternal() override; + + const std::string& last_gettoken_app_id() const { + return last_gettoken_app_id_; + } + const std::string& last_gettoken_authorized_entity() const { + return last_gettoken_authorized_entity_; + } + const std::string& last_deletetoken_app_id() const { + return last_deletetoken_app_id_; + } + + protected: + // InstanceIDHandler overrides: + void GetToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + base::TimeDelta time_to_live, + GetTokenCallback callback) override; + void ValidateToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + const std::string& token, + ValidateTokenCallback callback) override; + void DeleteToken(const std::string& app_id, + const std::string& authorized_entity, + const std::string& scope, + DeleteTokenCallback callback) override; + void AddInstanceIDData(const std::string& app_id, + const std::string& instance_id, + const std::string& extra_data) override; + void RemoveInstanceIDData(const std::string& app_id) override; + void GetInstanceIDData(const std::string& app_id, + GetInstanceIDDataCallback callback) override; + + private: + std::map> instance_id_data_; + std::map tokens_; + std::string last_gettoken_app_id_; + std::string last_gettoken_authorized_entity_; + std::string last_deletetoken_app_id_; +}; + +} // namespace instance_id + +#endif // COMPONENTS_GCM_DRIVER_INSTANCE_ID_FAKE_GCM_DRIVER_FOR_INSTANCE_ID_H_ diff --git a/chromium/components/gcm_driver/instance_id/instance_id.cc b/chromium/components/gcm_driver/instance_id/instance_id.cc new file mode 100644 index 00000000000..6aca1265f76 --- /dev/null +++ b/chromium/components/gcm_driver/instance_id/instance_id.cc @@ -0,0 +1,58 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/instance_id/instance_id.h" + +#include "base/bind.h" +#include "components/gcm_driver/gcm_driver.h" + +namespace instance_id { + +// A common use case for InstanceID tokens is to authorize and route push +// messages sent via Google Cloud Messaging (replacing the earlier registration +// IDs). To get such a GCM-enabled token, pass this scope to getToken. +// Must match Java GoogleCloudMessaging.INSTANCE_ID_SCOPE. +const char kGCMScope[] = "GCM"; + +InstanceID::InstanceID(const std::string& app_id, gcm::GCMDriver* gcm_driver) + : gcm_driver_(gcm_driver), app_id_(app_id) {} + +InstanceID::~InstanceID() {} + +void InstanceID::GetEncryptionInfo(const std::string& authorized_entity, + GetEncryptionInfoCallback callback) { + gcm_driver_->GetEncryptionProviderInternal()->GetEncryptionInfo( + app_id_, authorized_entity, std::move(callback)); +} + +void InstanceID::DeleteToken(const std::string& authorized_entity, + const std::string& scope, + DeleteTokenCallback callback) { + // Tokens with GCM scope act as Google Cloud Messaging registrations, so may + // have associated encryption information in the GCMKeyStore. This needs to be + // cleared when the token is deleted. + DeleteTokenCallback wrapped_callback = + scope == kGCMScope + ? base::BindOnce(&InstanceID::DidDelete, + weak_ptr_factory_.GetWeakPtr(), authorized_entity, + std::move(callback)) + : std::move(callback); + DeleteTokenImpl(authorized_entity, scope, std::move(wrapped_callback)); +} + +void InstanceID::DeleteID(DeleteIDCallback callback) { + // Use "*" as authorized_entity to remove any encryption info for all tokens. + DeleteIDImpl( + base::BindOnce(&InstanceID::DidDelete, weak_ptr_factory_.GetWeakPtr(), + "*" /* authorized_entity */, std::move(callback))); +} + +void InstanceID::DidDelete(const std::string& authorized_entity, + base::OnceCallback callback, + Result result) { + gcm_driver_->GetEncryptionProviderInternal()->RemoveEncryptionInfo( + app_id_, authorized_entity, base::BindOnce(std::move(callback), result)); +} + +} // namespace instance_id diff --git a/chromium/components/gcm_driver/instance_id/instance_id.h b/chromium/components/gcm_driver/instance_id/instance_id.h new file mode 100644 index 00000000000..01c19c46883 --- /dev/null +++ b/chromium/components/gcm_driver/instance_id/instance_id.h @@ -0,0 +1,182 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_H_ +#define COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_H_ + +#include +#include +#include + +#include "base/callback.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/time/time.h" + +namespace gcm { +class GCMDriver; +} // namespace gcm + +namespace instance_id { + +extern const char kGCMScope[]; + +// Encapsulates Instance ID functionalities that need to be implemented for +// different platforms. One instance is created per application. Life of +// Instance ID is managed by the InstanceIDDriver. +// +// Create instances of this class by calling |InstanceIDDriver::GetInstanceID|. +class InstanceID { + public: + // Used in UMA. Can add enum values, but never renumber or delete and reuse. + enum Result { + // Successful operation. + SUCCESS = 0, + // Invalid parameter. + INVALID_PARAMETER = 1, + // Instance ID is disabled. + DISABLED = 2, + // Previous asynchronous operation is still pending to finish. + ASYNC_OPERATION_PENDING = 3, + // Network socket error. + NETWORK_ERROR = 4, + // Problem at the server. + SERVER_ERROR = 5, + // 6 is omitted, in case we ever merge this enum with GCMClient::Result. + // Other errors. + UNKNOWN_ERROR = 7, + + // Used for UMA. Keep kMaxValue up to date and sync with histograms.xml. + kMaxValue = UNKNOWN_ERROR + }; + + // Flags to be used to create a token. These might be platform specific. + // GENERATED_JAVA_ENUM_PACKAGE: org.chromium.components.gcm_driver + // GENERATED_JAVA_CLASS_NAME_OVERRIDE: InstanceIDFlags + enum class Flags { + // Whether delivery of received messages should be deferred until there is a + // visible activity. Only applicable for Android. + kIsLazy = 1 << 0, + // Whether delivery of received messages should bypass the background task + // scheduler. Only applicable for high priority messages on Android. + kBypassScheduler = 1 << 1, + }; + + // Asynchronous callbacks. Must not synchronously delete |this| (using + // InstanceIDDriver::RemoveInstanceID). + using GetIDCallback = base::OnceCallback; + using GetCreationTimeCallback = + base::OnceCallback; + using GetTokenCallback = + base::OnceCallback; + using ValidateTokenCallback = base::OnceCallback; + using GetEncryptionInfoCallback = + base::OnceCallback; + using DeleteTokenCallback = base::OnceCallback; + using DeleteIDCallback = base::OnceCallback; + + static const int kInstanceIDByteLength = 8; + + // Creator. Should only be used by InstanceIDDriver::GetInstanceID. + // |app_id|: identifies the application that uses the Instance ID. + // |handler|: provides the GCM functionality needed to support Instance ID. + // Must outlive this class. On Android, this can be null instead. + static std::unique_ptr CreateInternal(const std::string& app_id, + gcm::GCMDriver* gcm_driver); + + InstanceID(const InstanceID&) = delete; + InstanceID& operator=(const InstanceID&) = delete; + + virtual ~InstanceID(); + + // Returns the Instance ID. + virtual void GetID(GetIDCallback callback) = 0; + + // Returns the time when the Instance ID has been generated. + virtual void GetCreationTime(GetCreationTimeCallback callback) = 0; + + // Retrieves a token that allows the authorized entity to access the service + // defined as "scope". This may cause network requests but the result is + // cached on disk for up to a week. Token validity will be checked + // automatically. Thus you should not store tokens for long periods yourself, + // instead call this function each time it's needed. + // + // To receive messages, register an |AppIdHandler| on |gcm_driver()|. + // + // |authorized_entity|: identifies the entity that is authorized to access + // resources associated with this Instance ID. It can be + // another Instance ID or a numeric project ID. + // |scope|: identifies authorized actions that the authorized entity can take. + // E.g. for sending GCM messages, "GCM" scope should be used. + // |time_to_live|: TTL of retrieved token, unlimited if zero value passed. + // |flags|: Flags used to create this token. + // |callback|: to be called once the asynchronous operation is done. + virtual void GetToken(const std::string& authorized_entity, + const std::string& scope, + base::TimeDelta time_to_live, + std::set flags, + GetTokenCallback callback) = 0; + + // Checks that the provided |token| matches the stored token for (|app_id()|, + // |authorized_entity|, |scope|). If you follow the guidance for |GetToken|, + // and call that function each time you need the token, then you will not + // need to use this function. + virtual void ValidateToken(const std::string& authorized_entity, + const std::string& scope, + const std::string& token, + ValidateTokenCallback callback) = 0; + + // Get the public encryption key and authentication secret associated with a + // GCM-scoped token. If encryption info is not yet associated, it will be + // created. + // |authorized_entity|: the authorized entity passed when obtaining the token. + // |callback|: to be called once the asynchronous operation is done. + virtual void GetEncryptionInfo(const std::string& authorized_entity, + GetEncryptionInfoCallback callback); + + // Revokes a granted token. + // |authorized_entity|: the authorized entity passed when obtaining the token. + // |scope|: the scope that was passed when obtaining the token. + // |callback|: to be called once the asynchronous operation is done. + virtual void DeleteToken(const std::string& authorized_entity, + const std::string& scope, + DeleteTokenCallback callback); + + // Resets the app instance identifier and revokes all tokens associated with + // it. + // |callback|: to be called once the asynchronous operation is done. + void DeleteID(DeleteIDCallback callback); + + std::string app_id() const { return app_id_; } + + gcm::GCMDriver* gcm_driver() { return gcm_driver_; } + + protected: + InstanceID(const std::string& app_id, gcm::GCMDriver* gcm_driver); + + // Platform-specific implementations. + virtual void DeleteTokenImpl(const std::string& authorized_entity, + const std::string& scope, + DeleteTokenCallback callback) = 0; + virtual void DeleteIDImpl(DeleteIDCallback callback) = 0; + + void NotifyTokenRefresh(bool update_id); + + private: + void DidDelete(const std::string& authorized_entity, + base::OnceCallback callback, + Result result); + + // Owned by GCMProfileServiceFactory, which is a dependency of + // InstanceIDProfileServiceFactory, which owns this. + raw_ptr gcm_driver_; + + std::string app_id_; + + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +} // namespace instance_id + +#endif // COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_H_ diff --git a/chromium/components/gcm_driver/instance_id/instance_id_android.cc b/chromium/components/gcm_driver/instance_id/instance_id_android.cc new file mode 100644 index 00000000000..ace47d9eeef --- /dev/null +++ b/chromium/components/gcm_driver/instance_id/instance_id_android.cc @@ -0,0 +1,229 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/instance_id/instance_id_android.h" + +#include +#include +#include + +#include "base/android/jni_android.h" +#include "base/android/jni_string.h" +#include "base/bind.h" +#include "base/location.h" +#include "base/logging.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "components/gcm_driver/instance_id/android/jni_headers/InstanceIDBridge_jni.h" + +using base::android::AttachCurrentThread; +using base::android::ConvertJavaStringToUTF8; +using base::android::ConvertUTF8ToJavaString; + +namespace instance_id { + +InstanceIDAndroid::ScopedBlockOnAsyncTasksForTesting:: + ScopedBlockOnAsyncTasksForTesting() { + JNIEnv* env = AttachCurrentThread(); + previous_value_ = + Java_InstanceIDBridge_setBlockOnAsyncTasksForTesting(env, true); +} + +InstanceIDAndroid::ScopedBlockOnAsyncTasksForTesting:: + ~ScopedBlockOnAsyncTasksForTesting() { + JNIEnv* env = AttachCurrentThread(); + Java_InstanceIDBridge_setBlockOnAsyncTasksForTesting(env, previous_value_); +} + +// static +std::unique_ptr InstanceID::CreateInternal( + const std::string& app_id, + gcm::GCMDriver* gcm_driver) { + return std::make_unique(app_id, gcm_driver); +} + +InstanceIDAndroid::InstanceIDAndroid(const std::string& app_id, + gcm::GCMDriver* gcm_driver) + : InstanceID(app_id, gcm_driver) { + DCHECK(thread_checker_.CalledOnValidThread()); + + DCHECK(!app_id.empty()) << "Empty app_id is not supported"; + // The |app_id| is stored in GCM's category field by the desktop InstanceID + // implementation, but because the category is reserved for the app's package + // name on Android the subtype field is used instead. + std::string subtype = app_id; + + JNIEnv* env = AttachCurrentThread(); + java_ref_.Reset( + Java_InstanceIDBridge_create(env, reinterpret_cast(this), + ConvertUTF8ToJavaString(env, subtype))); +} + +InstanceIDAndroid::~InstanceIDAndroid() { + DCHECK(thread_checker_.CalledOnValidThread()); + + JNIEnv* env = AttachCurrentThread(); + Java_InstanceIDBridge_destroy(env, java_ref_); +} + +void InstanceIDAndroid::GetID(GetIDCallback callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + + int32_t request_id = get_id_callbacks_.Add( + std::make_unique(std::move(callback))); + + JNIEnv* env = AttachCurrentThread(); + Java_InstanceIDBridge_getId(env, java_ref_, request_id); +} + +void InstanceIDAndroid::GetCreationTime(GetCreationTimeCallback callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + + int32_t request_id = get_creation_time_callbacks_.Add( + std::make_unique(std::move(callback))); + + JNIEnv* env = AttachCurrentThread(); + Java_InstanceIDBridge_getCreationTime(env, java_ref_, request_id); +} + +void InstanceIDAndroid::GetToken( + const std::string& authorized_entity, + const std::string& scope, + base::TimeDelta time_to_live, + std::set flags, + GetTokenCallback callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + + if (!time_to_live.is_zero()) { + LOG(WARNING) << "Non-zero TTL requested for InstanceID token, while TTLs" + " are not supported by Android Firebase IID API."; + } + + int32_t request_id = get_token_callbacks_.Add( + std::make_unique(std::move(callback))); + + int java_flags = std::accumulate( + flags.begin(), flags.end(), 0, + [](int sum, Flags flag) { return sum + static_cast(flag); }); + + JNIEnv* env = AttachCurrentThread(); + Java_InstanceIDBridge_getToken( + env, java_ref_, request_id, + ConvertUTF8ToJavaString(env, authorized_entity), + ConvertUTF8ToJavaString(env, scope), java_flags); +} + +void InstanceIDAndroid::ValidateToken(const std::string& authorized_entity, + const std::string& scope, + const std::string& token, + ValidateTokenCallback callback) { + // gcm_driver doesn't store tokens on Android, so assume it's valid. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), true /* is_valid */)); +} + +void InstanceIDAndroid::DeleteTokenImpl(const std::string& authorized_entity, + const std::string& scope, + DeleteTokenCallback callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + + int32_t request_id = delete_token_callbacks_.Add( + std::make_unique(std::move(callback))); + + JNIEnv* env = AttachCurrentThread(); + Java_InstanceIDBridge_deleteToken( + env, java_ref_, request_id, + ConvertUTF8ToJavaString(env, authorized_entity), + ConvertUTF8ToJavaString(env, scope)); +} + +void InstanceIDAndroid::DeleteIDImpl(DeleteIDCallback callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + + int32_t request_id = delete_id_callbacks_.Add( + std::make_unique(std::move(callback))); + + JNIEnv* env = AttachCurrentThread(); + Java_InstanceIDBridge_deleteInstanceID(env, java_ref_, request_id); +} + +void InstanceIDAndroid::DidGetID( + JNIEnv* env, + const base::android::JavaParamRef& obj, + jint request_id, + const base::android::JavaParamRef& jid) { + DCHECK(thread_checker_.CalledOnValidThread()); + + GetIDCallback* callback = get_id_callbacks_.Lookup(request_id); + DCHECK(callback); + std::move(*callback).Run(ConvertJavaStringToUTF8(jid)); + get_id_callbacks_.Remove(request_id); +} + +void InstanceIDAndroid::DidGetCreationTime( + JNIEnv* env, + const base::android::JavaParamRef& obj, + jint request_id, + jlong creation_time_unix_ms) { + DCHECK(thread_checker_.CalledOnValidThread()); + + base::Time creation_time; + // If the InstanceID's getId, getToken and deleteToken methods have never been + // called, or deleteInstanceID has cleared it since, creation time will be 0. + if (creation_time_unix_ms) { + creation_time = + base::Time::UnixEpoch() + base::Milliseconds(creation_time_unix_ms); + } + + GetCreationTimeCallback* callback = + get_creation_time_callbacks_.Lookup(request_id); + DCHECK(callback); + std::move(*callback).Run(creation_time); + get_creation_time_callbacks_.Remove(request_id); +} + +void InstanceIDAndroid::DidGetToken( + JNIEnv* env, + const base::android::JavaParamRef& obj, + jint request_id, + const base::android::JavaParamRef& jtoken) { + DCHECK(thread_checker_.CalledOnValidThread()); + + GetTokenCallback* callback = get_token_callbacks_.Lookup(request_id); + DCHECK(callback); + std::string token = ConvertJavaStringToUTF8(jtoken); + std::move(*callback).Run( + token, token.empty() ? InstanceID::UNKNOWN_ERROR : InstanceID::SUCCESS); + get_token_callbacks_.Remove(request_id); +} + +void InstanceIDAndroid::DidDeleteToken( + JNIEnv* env, + const base::android::JavaParamRef& obj, + jint request_id, + jboolean success) { + DCHECK(thread_checker_.CalledOnValidThread()); + + DeleteTokenCallback* callback = delete_token_callbacks_.Lookup(request_id); + DCHECK(callback); + std::move(*callback).Run(success ? InstanceID::SUCCESS + : InstanceID::UNKNOWN_ERROR); + delete_token_callbacks_.Remove(request_id); +} + +void InstanceIDAndroid::DidDeleteID( + JNIEnv* env, + const base::android::JavaParamRef& obj, + jint request_id, + jboolean success) { + DCHECK(thread_checker_.CalledOnValidThread()); + + DeleteIDCallback* callback = delete_id_callbacks_.Lookup(request_id); + DCHECK(callback); + std::move(*callback).Run(success ? InstanceID::SUCCESS + : InstanceID::UNKNOWN_ERROR); + delete_id_callbacks_.Remove(request_id); +} + +} // namespace instance_id diff --git a/chromium/components/gcm_driver/instance_id/instance_id_android.h b/chromium/components/gcm_driver/instance_id/instance_id_android.h new file mode 100644 index 00000000000..40b77244059 --- /dev/null +++ b/chromium/components/gcm_driver/instance_id/instance_id_android.h @@ -0,0 +1,104 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_ANDROID_H_ +#define COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_ANDROID_H_ + +#include + +#include + +#include "base/android/scoped_java_ref.h" +#include "base/callback.h" +#include "base/compiler_specific.h" +#include "base/containers/id_map.h" +#include "base/threading/thread_checker.h" +#include "base/time/time.h" +#include "components/gcm_driver/instance_id/instance_id.h" + +namespace instance_id { + +// InstanceID implementation for Android. +class InstanceIDAndroid : public InstanceID { + public: + // Tests depending on InstanceID that run without a nested Java message loop + // must use this. Operations that would normally be asynchronous will instead + // block the UI thread. + class ScopedBlockOnAsyncTasksForTesting { + public: + ScopedBlockOnAsyncTasksForTesting(); + + ScopedBlockOnAsyncTasksForTesting( + const ScopedBlockOnAsyncTasksForTesting&) = delete; + ScopedBlockOnAsyncTasksForTesting& operator=( + const ScopedBlockOnAsyncTasksForTesting&) = delete; + + ~ScopedBlockOnAsyncTasksForTesting(); + + private: + bool previous_value_; + }; + + InstanceIDAndroid(const std::string& app_id, gcm::GCMDriver* gcm_driver); + + InstanceIDAndroid(const InstanceIDAndroid&) = delete; + InstanceIDAndroid& operator=(const InstanceIDAndroid&) = delete; + + ~InstanceIDAndroid() override; + + // InstanceID implementation: + void GetID(GetIDCallback callback) override; + void GetCreationTime(GetCreationTimeCallback callback) override; + void GetToken(const std::string& audience, + const std::string& scope, + base::TimeDelta time_to_live, + std::set flags, + GetTokenCallback callback) override; + void ValidateToken(const std::string& authorized_entity, + const std::string& scope, + const std::string& token, + ValidateTokenCallback callback) override; + void DeleteTokenImpl(const std::string& audience, + const std::string& scope, + DeleteTokenCallback callback) override; + void DeleteIDImpl(DeleteIDCallback callback) override; + + // Methods called from Java via JNI: + void DidGetID(JNIEnv* env, + const base::android::JavaParamRef& obj, + jint request_id, + const base::android::JavaParamRef& jid); + void DidGetCreationTime(JNIEnv* env, + const base::android::JavaParamRef& obj, + jint request_id, + jlong creation_time_unix_ms); + void DidGetToken(JNIEnv* env, + const base::android::JavaParamRef& obj, + jint request_id, + const base::android::JavaParamRef& jtoken); + void DidDeleteToken(JNIEnv* env, + const base::android::JavaParamRef& obj, + jint request_id, + jboolean success); + void DidDeleteID(JNIEnv* env, + const base::android::JavaParamRef& obj, + jint request_id, + jboolean success); + + private: + base::android::ScopedJavaGlobalRef java_ref_; + + base::IDMap> get_id_callbacks_; + base::IDMap> + get_creation_time_callbacks_; + base::IDMap> get_token_callbacks_; + base::IDMap> delete_token_callbacks_; + base::IDMap> delete_id_callbacks_; + + base::ThreadChecker thread_checker_; +}; + +} // namespace instance_id + +#endif // COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_ANDROID_H_ diff --git a/chromium/components/gcm_driver/instance_id/instance_id_driver.cc b/chromium/components/gcm_driver/instance_id/instance_id_driver.cc new file mode 100644 index 00000000000..52756898a77 --- /dev/null +++ b/chromium/components/gcm_driver/instance_id/instance_id_driver.cc @@ -0,0 +1,40 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/instance_id/instance_id_driver.h" + +#include "build/build_config.h" +#include "components/gcm_driver/gcm_driver.h" +#include "components/gcm_driver/instance_id/instance_id.h" + +namespace instance_id { + +InstanceIDDriver::InstanceIDDriver(gcm::GCMDriver* gcm_driver) + : gcm_driver_(gcm_driver) { +} + +InstanceIDDriver::~InstanceIDDriver() { +} + +InstanceID* InstanceIDDriver::GetInstanceID(const std::string& app_id) { + auto iter = instance_id_map_.find(app_id); + if (iter != instance_id_map_.end()) + return iter->second.get(); + + std::unique_ptr instance_id = + InstanceID::CreateInternal(app_id, gcm_driver_); + InstanceID* instance_id_ptr = instance_id.get(); + instance_id_map_.insert(std::make_pair(app_id, std::move(instance_id))); + return instance_id_ptr; +} + +void InstanceIDDriver::RemoveInstanceID(const std::string& app_id) { + instance_id_map_.erase(app_id); +} + +bool InstanceIDDriver::ExistsInstanceID(const std::string& app_id) const { + return instance_id_map_.find(app_id) != instance_id_map_.end(); +} + +} // namespace instance_id diff --git a/chromium/components/gcm_driver/instance_id/instance_id_driver.h b/chromium/components/gcm_driver/instance_id/instance_id_driver.h new file mode 100644 index 00000000000..8fe328fa22f --- /dev/null +++ b/chromium/components/gcm_driver/instance_id/instance_id_driver.h @@ -0,0 +1,58 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_DRIVER_H_ +#define COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_DRIVER_H_ + +#include +#include +#include + +#include "base/memory/raw_ptr.h" + +namespace gcm { +class GCMDriver; +} // namespace gcm + +namespace instance_id { + +class InstanceID; + +// Bridge between Instance ID users in Chrome and the platform-specific +// implementation. +// +// Create instances of this class with |InstanceIDProfileServiceFactory|. +class InstanceIDDriver { + public: + explicit InstanceIDDriver(gcm::GCMDriver* gcm_driver); + + InstanceIDDriver(const InstanceIDDriver&) = delete; + InstanceIDDriver& operator=(const InstanceIDDriver&) = delete; + + virtual ~InstanceIDDriver(); + + // Returns the InstanceID that provides the Instance ID service for the given + // application. The lifetime of the InstanceID will be managed by this class. + // App IDs are arbitrary strings that typically look like "chrome.foo.bar". + virtual InstanceID* GetInstanceID(const std::string& app_id); + + // Removes the InstanceID when it is not longer needed, i.e. the app is being + // uninstalled. + virtual void RemoveInstanceID(const std::string& app_id); + + // Returns true if the InstanceID for the given application has been created. + // This is currently only used for testing purpose. + virtual bool ExistsInstanceID(const std::string& app_id) const; + + private: + // Owned by GCMProfileServiceFactory, which is a dependency of + // InstanceIDProfileServiceFactory, which owns this. + raw_ptr gcm_driver_; + + std::map> instance_id_map_; +}; + +} // namespace instance_id + +#endif // COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_DRIVER_H_ diff --git a/chromium/components/gcm_driver/instance_id/instance_id_driver_unittest.cc b/chromium/components/gcm_driver/instance_id/instance_id_driver_unittest.cc new file mode 100644 index 00000000000..0170f499206 --- /dev/null +++ b/chromium/components/gcm_driver/instance_id/instance_id_driver_unittest.cc @@ -0,0 +1,366 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/instance_id/instance_id_driver.h" + +#include + +#include +#include + +#include "base/bind.h" +#include "base/run_loop.h" +#include "base/strings/string_util.h" +#include "base/test/task_environment.h" +#include "components/gcm_driver/gcm_buildflags.h" +#include "components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.h" +#include "components/gcm_driver/instance_id/instance_id.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if BUILDFLAG(USE_GCM_FROM_PLATFORM) +#include "components/gcm_driver/instance_id/instance_id_android.h" +#include "components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.h" +#endif // BUILDFLAG(USE_GCM_FROM_PLATFORM) + +namespace instance_id { + +namespace { + +const char kTestAppID1[] = "TestApp1"; +const char kTestAppID2[] = "TestApp2"; +const char kAuthorizedEntity1[] = "Sender 1"; +const char kAuthorizedEntity2[] = "Sender 2"; +const char kScope1[] = "GCM1"; +const char kScope2[] = "FooBar"; + +bool VerifyInstanceID(const std::string& str) { + // Checks the length. + if (str.length() != static_cast( + std::ceil(InstanceID::kInstanceIDByteLength * 8 / 6.0))) + return false; + + // Checks if it is URL-safe base64 encoded. + for (auto ch : str) { + if (!base::IsAsciiAlpha(ch) && !base::IsAsciiDigit(ch) && + ch != '_' && ch != '-') + return false; + } + return true; +} + +} // namespace + +class InstanceIDDriverTest : public testing::Test { + public: + InstanceIDDriverTest(); + + InstanceIDDriverTest(const InstanceIDDriverTest&) = delete; + InstanceIDDriverTest& operator=(const InstanceIDDriverTest&) = delete; + + ~InstanceIDDriverTest() override; + + // testing::Test: + void SetUp() override; + void TearDown() override; + + void WaitForAsyncOperation(); + + // Recreates InstanceIDDriver to simulate restart. + void RecreateInstanceIDDriver(); + + // Sync wrappers for async version. + std::string GetID(InstanceID* instance_id); + base::Time GetCreationTime(InstanceID* instance_id); + InstanceID::Result DeleteID(InstanceID* instance_id); + std::string GetToken(InstanceID* instance_id, + const std::string& authorized_entity, + const std::string& scope); + InstanceID::Result DeleteToken( + InstanceID* instance_id, + const std::string& authorized_entity, + const std::string& scope); + + InstanceIDDriver* driver() const { return driver_.get(); } + + private: + void GetIDCompleted(const std::string& id); + void GetCreationTimeCompleted(const base::Time& creation_time); + void DeleteIDCompleted(InstanceID::Result result); + void GetTokenCompleted(const std::string& token, InstanceID::Result result); + void DeleteTokenCompleted(InstanceID::Result result); + + base::test::SingleThreadTaskEnvironment task_environment_; + std::unique_ptr gcm_driver_; + std::unique_ptr driver_; + +#if BUILDFLAG(USE_GCM_FROM_PLATFORM) + InstanceIDAndroid::ScopedBlockOnAsyncTasksForTesting block_async_; + ScopedUseFakeInstanceIDAndroid use_fake_; +#endif // BUILDFLAG(USE_GCM_FROM_PLATFORM) + + std::string id_; + base::Time creation_time_; + std::string token_; + InstanceID::Result result_; + + bool async_operation_completed_; + base::OnceClosure async_operation_completed_callback_; +}; + +InstanceIDDriverTest::InstanceIDDriverTest() + : task_environment_( + base::test::SingleThreadTaskEnvironment::MainThreadType::UI), + result_(InstanceID::UNKNOWN_ERROR), + async_operation_completed_(false) {} + +InstanceIDDriverTest::~InstanceIDDriverTest() { +} + +void InstanceIDDriverTest::SetUp() { + gcm_driver_ = std::make_unique(); + RecreateInstanceIDDriver(); +} + +void InstanceIDDriverTest::TearDown() { + driver_.reset(); + gcm_driver_.reset(); + // |gcm_driver_| owns a GCMKeyStore that owns a ProtoDatabase whose + // destructor deletes the underlying LevelDB on the task runner. + base::RunLoop().RunUntilIdle(); +} + +void InstanceIDDriverTest::RecreateInstanceIDDriver() { + driver_ = std::make_unique(gcm_driver_.get()); +} + +void InstanceIDDriverTest::WaitForAsyncOperation() { + // No need to wait if async operation is not needed. + if (async_operation_completed_) + return; + base::RunLoop run_loop; + async_operation_completed_callback_ = run_loop.QuitClosure(); + run_loop.Run(); +} + +std::string InstanceIDDriverTest::GetID(InstanceID* instance_id) { + async_operation_completed_ = false; + id_.clear(); + instance_id->GetID(base::BindOnce(&InstanceIDDriverTest::GetIDCompleted, + base::Unretained(this))); + WaitForAsyncOperation(); + return id_; +} + +base::Time InstanceIDDriverTest::GetCreationTime(InstanceID* instance_id) { + async_operation_completed_ = false; + creation_time_ = base::Time(); + instance_id->GetCreationTime(base::BindOnce( + &InstanceIDDriverTest::GetCreationTimeCompleted, base::Unretained(this))); + WaitForAsyncOperation(); + return creation_time_; +} + +InstanceID::Result InstanceIDDriverTest::DeleteID(InstanceID* instance_id) { + async_operation_completed_ = false; + result_ = InstanceID::UNKNOWN_ERROR; + instance_id->DeleteID(base::BindOnce(&InstanceIDDriverTest::DeleteIDCompleted, + base::Unretained(this))); + WaitForAsyncOperation(); + return result_; +} + +std::string InstanceIDDriverTest::GetToken(InstanceID* instance_id, + const std::string& authorized_entity, + const std::string& scope) { + async_operation_completed_ = false; + token_.clear(); + result_ = InstanceID::UNKNOWN_ERROR; + instance_id->GetToken( + authorized_entity, scope, /*time_to_live=*/base::TimeDelta(), + /*flags=*/{}, + base::BindRepeating(&InstanceIDDriverTest::GetTokenCompleted, + base::Unretained(this))); + WaitForAsyncOperation(); + return token_; +} + +InstanceID::Result InstanceIDDriverTest::DeleteToken( + InstanceID* instance_id, + const std::string& authorized_entity, + const std::string& scope) { + async_operation_completed_ = false; + result_ = InstanceID::UNKNOWN_ERROR; + instance_id->DeleteToken( + authorized_entity, scope, + base::BindOnce(&InstanceIDDriverTest::DeleteTokenCompleted, + base::Unretained(this))); + WaitForAsyncOperation(); + return result_; +} + +void InstanceIDDriverTest::GetIDCompleted(const std::string& id) { + DCHECK(!async_operation_completed_); + async_operation_completed_ = true; + id_ = id; + if (async_operation_completed_callback_) + std::move(async_operation_completed_callback_).Run(); +} + +void InstanceIDDriverTest::GetCreationTimeCompleted( + const base::Time& creation_time) { + DCHECK(!async_operation_completed_); + async_operation_completed_ = true; + creation_time_ = creation_time; + if (async_operation_completed_callback_) + std::move(async_operation_completed_callback_).Run(); +} + +void InstanceIDDriverTest::DeleteIDCompleted(InstanceID::Result result) { + DCHECK(!async_operation_completed_); + async_operation_completed_ = true; + result_ = result; + if (async_operation_completed_callback_) + std::move(async_operation_completed_callback_).Run(); +} + +void InstanceIDDriverTest::GetTokenCompleted( + const std::string& token, InstanceID::Result result) { + DCHECK(!async_operation_completed_); + async_operation_completed_ = true; + token_ = token; + result_ = result; + if (async_operation_completed_callback_) + std::move(async_operation_completed_callback_).Run(); +} + +void InstanceIDDriverTest::DeleteTokenCompleted(InstanceID::Result result) { + DCHECK(!async_operation_completed_); + async_operation_completed_ = true; + result_ = result; + if (async_operation_completed_callback_) + std::move(async_operation_completed_callback_).Run(); +} + +TEST_F(InstanceIDDriverTest, GetAndRemoveInstanceID) { + EXPECT_FALSE(driver()->ExistsInstanceID(kTestAppID1)); + + InstanceID* instance_id = driver()->GetInstanceID(kTestAppID1); + EXPECT_TRUE(instance_id); + EXPECT_TRUE(driver()->ExistsInstanceID(kTestAppID1)); + + driver()->RemoveInstanceID(kTestAppID1); + EXPECT_FALSE(driver()->ExistsInstanceID(kTestAppID1)); +} + +TEST_F(InstanceIDDriverTest, NewID) { + // Creation time should not be set when the ID is not created. + InstanceID* instance_id1 = driver()->GetInstanceID(kTestAppID1); + EXPECT_TRUE(GetCreationTime(instance_id1).is_null()); + + // New ID is generated for the first time. + std::string id1 = GetID(instance_id1); + EXPECT_TRUE(VerifyInstanceID(id1)); + base::Time creation_time = GetCreationTime(instance_id1); + EXPECT_FALSE(creation_time.is_null()); + + // Same ID is returned for the same app. + EXPECT_EQ(id1, GetID(instance_id1)); + EXPECT_EQ(creation_time, GetCreationTime(instance_id1)); + + // New ID is generated for another app. + InstanceID* instance_id2 = driver()->GetInstanceID(kTestAppID2); + std::string id2 = GetID(instance_id2); + EXPECT_TRUE(VerifyInstanceID(id2)); + EXPECT_NE(id1, id2); + EXPECT_FALSE(GetCreationTime(instance_id2).is_null()); +} + +TEST_F(InstanceIDDriverTest, PersistID) { + InstanceID* instance_id = driver()->GetInstanceID(kTestAppID1); + + // Create the ID for the first time. The ID and creation time should be saved + // to the store. + std::string id = GetID(instance_id); + EXPECT_FALSE(id.empty()); + base::Time creation_time = GetCreationTime(instance_id); + EXPECT_FALSE(creation_time.is_null()); + + // Simulate restart by recreating InstanceIDDriver. Same ID and creation time + // should be expected. + RecreateInstanceIDDriver(); + instance_id = driver()->GetInstanceID(kTestAppID1); + EXPECT_EQ(creation_time, GetCreationTime(instance_id)); + EXPECT_EQ(id, GetID(instance_id)); + + // Delete the ID. The ID and creation time should be removed from the store. + EXPECT_EQ(InstanceID::SUCCESS, DeleteID(instance_id)); + EXPECT_TRUE(GetCreationTime(instance_id).is_null()); + + // Simulate restart by recreating InstanceIDDriver. Different ID should be + // expected. + // Note that we do not check for different creation time since the test might + // be run at a very fast server. + RecreateInstanceIDDriver(); + instance_id = driver()->GetInstanceID(kTestAppID1); + EXPECT_NE(id, GetID(instance_id)); +} + +TEST_F(InstanceIDDriverTest, DeleteID) { + InstanceID* instance_id = driver()->GetInstanceID(kTestAppID1); + std::string id1 = GetID(instance_id); + EXPECT_FALSE(id1.empty()); + EXPECT_FALSE(GetCreationTime(instance_id).is_null()); + + // New ID will be generated from GetID after calling DeleteID. + EXPECT_EQ(InstanceID::SUCCESS, DeleteID(instance_id)); + EXPECT_TRUE(GetCreationTime(instance_id).is_null()); + + std::string id2 = GetID(instance_id); + EXPECT_FALSE(id2.empty()); + EXPECT_NE(id1, id2); + EXPECT_FALSE(GetCreationTime(instance_id).is_null()); +} + +TEST_F(InstanceIDDriverTest, GetToken) { + InstanceID* instance_id = driver()->GetInstanceID(kTestAppID1); + std::string token1 = GetToken(instance_id, kAuthorizedEntity1, kScope1); + EXPECT_FALSE(token1.empty()); + + // Same token is returned for same authorized entity and scope. + EXPECT_EQ(token1, GetToken(instance_id, kAuthorizedEntity1, kScope1)); + + // Different token is returned for different authorized entity or scope. + std::string token2 = GetToken(instance_id, kAuthorizedEntity1, kScope2); + EXPECT_FALSE(token2.empty()); + EXPECT_NE(token1, token2); + + std::string token3 = GetToken(instance_id, kAuthorizedEntity2, kScope1); + EXPECT_FALSE(token3.empty()); + EXPECT_NE(token1, token3); + EXPECT_NE(token2, token3); +} + +TEST_F(InstanceIDDriverTest, DeleteToken) { + InstanceID* instance_id = driver()->GetInstanceID(kTestAppID1); + + // Gets 2 tokens. + std::string token1 = GetToken(instance_id, kAuthorizedEntity1, kScope1); + EXPECT_FALSE(token1.empty()); + std::string token2 = GetToken(instance_id, kAuthorizedEntity2, kScope1); + EXPECT_FALSE(token1.empty()); + EXPECT_NE(token1, token2); + + // Different token is returned for same authorized entity and scope after + // deletion. + EXPECT_EQ(InstanceID::SUCCESS, + DeleteToken(instance_id, kAuthorizedEntity1, kScope1)); + std::string new_token1 = GetToken(instance_id, kAuthorizedEntity1, kScope2); + EXPECT_FALSE(new_token1.empty()); + EXPECT_NE(token1, new_token1); + + // The other token is not affected by the deletion. + EXPECT_EQ(token2, GetToken(instance_id, kAuthorizedEntity2, kScope1)); +} + +} // namespace instance_id diff --git a/chromium/components/gcm_driver/instance_id/instance_id_impl.cc b/chromium/components/gcm_driver/instance_id/instance_id_impl.cc new file mode 100644 index 00000000000..3f21ba69842 --- /dev/null +++ b/chromium/components/gcm_driver/instance_id/instance_id_impl.cc @@ -0,0 +1,275 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/instance_id/instance_id_impl.h" + +#include + +#include +#include + +#include "base/base64.h" +#include "base/bind.h" +#include "base/containers/cxx20_erase.h" +#include "base/logging.h" +#include "base/strings/string_number_conversions.h" +#include "base/threading/thread_task_runner_handle.h" +#include "components/gcm_driver/gcm_driver.h" +#include "crypto/random.h" + +namespace instance_id { + +namespace { + +InstanceID::Result GCMClientResultToInstanceIDResult( + gcm::GCMClient::Result result) { + switch (result) { + case gcm::GCMClient::SUCCESS: + return InstanceID::SUCCESS; + case gcm::GCMClient::INVALID_PARAMETER: + return InstanceID::INVALID_PARAMETER; + case gcm::GCMClient::GCM_DISABLED: + return InstanceID::DISABLED; + case gcm::GCMClient::ASYNC_OPERATION_PENDING: + return InstanceID::ASYNC_OPERATION_PENDING; + case gcm::GCMClient::NETWORK_ERROR: + return InstanceID::NETWORK_ERROR; + case gcm::GCMClient::SERVER_ERROR: + return InstanceID::SERVER_ERROR; + case gcm::GCMClient::UNKNOWN_ERROR: + return InstanceID::UNKNOWN_ERROR; + case gcm::GCMClient::TTL_EXCEEDED: + NOTREACHED(); + break; + } + return InstanceID::UNKNOWN_ERROR; +} + +} // namespace + +// static +std::unique_ptr InstanceID::CreateInternal( + const std::string& app_id, + gcm::GCMDriver* gcm_driver) { + return std::make_unique(app_id, gcm_driver); +} + +InstanceIDImpl::InstanceIDImpl(const std::string& app_id, + gcm::GCMDriver* gcm_driver) + : InstanceID(app_id, gcm_driver) { + Handler()->GetInstanceIDData( + app_id, base::BindOnce(&InstanceIDImpl::GetInstanceIDDataCompleted, + weak_ptr_factory_.GetWeakPtr())); +} + +InstanceIDImpl::~InstanceIDImpl() { +} + +void InstanceIDImpl::GetID(GetIDCallback callback) { + RunWhenReady(base::BindOnce(&InstanceIDImpl::DoGetID, + weak_ptr_factory_.GetWeakPtr(), + std::move(callback))); +} + +void InstanceIDImpl::DoGetID(GetIDCallback callback) { + EnsureIDGenerated(); + std::move(callback).Run(id_); +} + +void InstanceIDImpl::GetCreationTime(GetCreationTimeCallback callback) { + RunWhenReady(base::BindOnce(&InstanceIDImpl::DoGetCreationTime, + weak_ptr_factory_.GetWeakPtr(), + std::move(callback))); +} + +void InstanceIDImpl::DoGetCreationTime(GetCreationTimeCallback callback) { + std::move(callback).Run(creation_time_); +} + +void InstanceIDImpl::GetToken(const std::string& authorized_entity, + const std::string& scope, + base::TimeDelta time_to_live, + std::set flags, + GetTokenCallback callback) { + DCHECK(!authorized_entity.empty()); + DCHECK(!scope.empty()); + + RunWhenReady(base::BindOnce(&InstanceIDImpl::DoGetToken, + weak_ptr_factory_.GetWeakPtr(), authorized_entity, + scope, time_to_live, std::move(callback))); +} + +void InstanceIDImpl::DoGetToken( + const std::string& authorized_entity, + const std::string& scope, + base::TimeDelta time_to_live, + GetTokenCallback callback) { + EnsureIDGenerated(); + + Handler()->GetToken( + app_id(), authorized_entity, scope, time_to_live, + base::BindOnce(&InstanceIDImpl::OnGetTokenCompleted, + weak_ptr_factory_.GetWeakPtr(), std::move(callback))); +} + +void InstanceIDImpl::ValidateToken(const std::string& authorized_entity, + const std::string& scope, + const std::string& token, + ValidateTokenCallback callback) { + DCHECK(!authorized_entity.empty()); + DCHECK(!scope.empty()); + DCHECK(!token.empty()); + + RunWhenReady(base::BindOnce(&InstanceIDImpl::DoValidateToken, + weak_ptr_factory_.GetWeakPtr(), authorized_entity, + scope, token, std::move(callback))); +} + +void InstanceIDImpl::DoValidateToken(const std::string& authorized_entity, + const std::string& scope, + const std::string& token, + ValidateTokenCallback callback) { + if (id_.empty()) { + std::move(callback).Run(false /* is_valid */); + return; + } + + Handler()->ValidateToken(app_id(), authorized_entity, scope, token, + std::move(callback)); +} + +void InstanceIDImpl::DeleteTokenImpl(const std::string& authorized_entity, + const std::string& scope, + DeleteTokenCallback callback) { + DCHECK(!authorized_entity.empty()); + DCHECK(!scope.empty()); + + RunWhenReady(base::BindOnce(&InstanceIDImpl::DoDeleteToken, + weak_ptr_factory_.GetWeakPtr(), authorized_entity, + scope, std::move(callback))); +} + +void InstanceIDImpl::DoDeleteToken(const std::string& authorized_entity, + const std::string& scope, + DeleteTokenCallback callback) { + // Nothing to delete if the ID has not been generated. + if (id_.empty()) { + std::move(callback).Run(InstanceID::INVALID_PARAMETER); + return; + } + + Handler()->DeleteToken( + app_id(), authorized_entity, scope, + base::BindOnce(&InstanceIDImpl::OnDeleteTokenCompleted, + weak_ptr_factory_.GetWeakPtr(), std::move(callback))); +} + +void InstanceIDImpl::DeleteIDImpl(DeleteIDCallback callback) { + RunWhenReady(base::BindOnce(&InstanceIDImpl::DoDeleteID, + weak_ptr_factory_.GetWeakPtr(), + std::move(callback))); +} + +void InstanceIDImpl::DoDeleteID(DeleteIDCallback callback) { + // Nothing to do if ID has not been generated. + if (id_.empty()) { + std::move(callback).Run(InstanceID::SUCCESS); + return; + } + + Handler()->DeleteAllTokensForApp( + app_id(), + base::BindOnce(&InstanceIDImpl::OnDeleteIDCompleted, + weak_ptr_factory_.GetWeakPtr(), std::move(callback))); + + Handler()->RemoveInstanceIDData(app_id()); + + id_.clear(); + creation_time_ = base::Time(); +} + +void InstanceIDImpl::OnGetTokenCompleted(GetTokenCallback callback, + const std::string& token, + gcm::GCMClient::Result result) { + std::move(callback).Run(token, GCMClientResultToInstanceIDResult(result)); +} + +void InstanceIDImpl::OnDeleteTokenCompleted(DeleteTokenCallback callback, + gcm::GCMClient::Result result) { + std::move(callback).Run(GCMClientResultToInstanceIDResult(result)); +} + +void InstanceIDImpl::OnDeleteIDCompleted(DeleteIDCallback callback, + gcm::GCMClient::Result result) { + std::move(callback).Run(GCMClientResultToInstanceIDResult(result)); +} + +void InstanceIDImpl::GetInstanceIDDataCompleted( + const std::string& instance_id, + const std::string& extra_data) { + id_ = instance_id; + + if (extra_data.empty()) { + creation_time_ = base::Time(); + } else { + int64_t time_internal = 0LL; + if (!base::StringToInt64(extra_data, &time_internal)) { + DVLOG(1) << "Failed to parse the time data: " + extra_data; + return; + } + creation_time_ = base::Time::FromInternalValue(time_internal); + } + + delayed_task_controller_.SetReady(); +} + +void InstanceIDImpl::EnsureIDGenerated() { + if (!id_.empty()) + return; + + // Now produce the ID in the following steps: + + // 1) Generates the random number in 8 bytes which is required by the server. + // We don't want to be strictly cryptographically secure. The server might + // reject the ID if there is a conflict or problem. + uint8_t bytes[kInstanceIDByteLength]; + crypto::RandBytes(bytes, sizeof(bytes)); + + // 2) Transforms the first 4 bits to 0x7. Note that this is required by the + // server. + bytes[0] &= 0x0f; + bytes[0] |= 0x70; + + // 3) Encode the value in Android-compatible base64 scheme: + // * URL safe: '/' replaced by '_' and '+' replaced by '-'. + // * No padding: any trailing '=' will be removed. + base::Base64Encode( + base::StringPiece(reinterpret_cast(bytes), sizeof(bytes)), + &id_); + std::replace(id_.begin(), id_.end(), '+', '-'); + std::replace(id_.begin(), id_.end(), '/', '_'); + base::Erase(id_, '='); + + creation_time_ = base::Time::Now(); + + // Save to the persistent store. + Handler()->AddInstanceIDData( + app_id(), id_, base::NumberToString(creation_time_.ToInternalValue())); +} + +gcm::InstanceIDHandler* InstanceIDImpl::Handler() { + gcm::InstanceIDHandler* handler = + gcm_driver()->GetInstanceIDHandlerInternal(); + DCHECK(handler); + return handler; +} + +void InstanceIDImpl::RunWhenReady(base::OnceClosure task) { + if (!delayed_task_controller_.CanRunTaskWithoutDelay()) + delayed_task_controller_.AddTask(std::move(task)); + else + base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, std::move(task)); +} + +} // namespace instance_id diff --git a/chromium/components/gcm_driver/instance_id/instance_id_impl.h b/chromium/components/gcm_driver/instance_id/instance_id_impl.h new file mode 100644 index 00000000000..e39fd1e242c --- /dev/null +++ b/chromium/components/gcm_driver/instance_id/instance_id_impl.h @@ -0,0 +1,98 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_IMPL_H_ +#define COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_IMPL_H_ + +#include +#include + +#include "base/callback.h" +#include "base/memory/weak_ptr.h" +#include "base/time/time.h" +#include "components/gcm_driver/gcm_client.h" +#include "components/gcm_driver/gcm_delayed_task_controller.h" +#include "components/gcm_driver/instance_id/instance_id.h" + +namespace gcm { +class GCMDriver; +class InstanceIDHandler; +} // namespace gcm + +namespace instance_id { + +// InstanceID implementation for desktop and iOS. +class InstanceIDImpl : public InstanceID { + public: + InstanceIDImpl(const std::string& app_id, gcm::GCMDriver* gcm_driver); + + InstanceIDImpl(const InstanceIDImpl&) = delete; + InstanceIDImpl& operator=(const InstanceIDImpl&) = delete; + + ~InstanceIDImpl() override; + + // InstanceID: + void GetID(GetIDCallback callback) override; + void GetCreationTime(GetCreationTimeCallback callback) override; + void GetToken(const std::string& authorized_entity, + const std::string& scope, + base::TimeDelta time_to_live, + std::set flags, + GetTokenCallback callback) override; + void ValidateToken(const std::string& authorized_entity, + const std::string& scope, + const std::string& token, + ValidateTokenCallback callback) override; + void DeleteTokenImpl(const std::string& authorized_entity, + const std::string& scope, + DeleteTokenCallback callback) override; + void DeleteIDImpl(DeleteIDCallback callback) override; + + private: + void EnsureIDGenerated(); + + void OnGetTokenCompleted(GetTokenCallback callback, + const std::string& token, + gcm::GCMClient::Result result); + void OnDeleteTokenCompleted(DeleteTokenCallback callback, + gcm::GCMClient::Result result); + void OnDeleteIDCompleted(DeleteIDCallback callback, + gcm::GCMClient::Result result); + void GetInstanceIDDataCompleted(const std::string& instance_id, + const std::string& extra_data); + + void DoGetID(GetIDCallback callback); + void DoGetCreationTime(GetCreationTimeCallback callback); + void DoGetToken(const std::string& authorized_entity, + const std::string& scope, + base::TimeDelta time_to_live, + GetTokenCallback callback); + void DoValidateToken(const std::string& authorized_entity, + const std::string& scope, + const std::string& token, + ValidateTokenCallback callback); + void DoDeleteToken(const std::string& authorized_entity, + const std::string& scope, + DeleteTokenCallback callback); + void DoDeleteID(DeleteIDCallback callback); + + gcm::InstanceIDHandler* Handler(); + + // Asynchronously runs task once delayed_task_controller_ is ready. + void RunWhenReady(base::OnceClosure task); + + gcm::GCMDelayedTaskController delayed_task_controller_; + + // The generated Instance ID. + std::string id_; + + // The time when the Instance ID has been generated. + base::Time creation_time_; + + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +} // namespace instance_id + +#endif // COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_IMPL_H_ diff --git a/chromium/components/gcm_driver/instance_id/instance_id_profile_service.cc b/chromium/components/gcm_driver/instance_id/instance_id_profile_service.cc new file mode 100644 index 00000000000..ecbafd3d4ad --- /dev/null +++ b/chromium/components/gcm_driver/instance_id/instance_id_profile_service.cc @@ -0,0 +1,23 @@ +// Copyright (c) 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/instance_id/instance_id_profile_service.h" + +#include "base/check.h" +#include "components/gcm_driver/gcm_driver.h" +#include "components/gcm_driver/gcm_profile_service.h" +#include "components/gcm_driver/instance_id/instance_id_driver.h" + +namespace instance_id { + +InstanceIDProfileService::InstanceIDProfileService(gcm::GCMDriver* driver, + bool is_off_the_record) { + DCHECK(!is_off_the_record); + + driver_ = std::make_unique(driver); +} + +InstanceIDProfileService::~InstanceIDProfileService() {} + +} // namespace instance_id diff --git a/chromium/components/gcm_driver/instance_id/instance_id_profile_service.h b/chromium/components/gcm_driver/instance_id/instance_id_profile_service.h new file mode 100644 index 00000000000..8757c086250 --- /dev/null +++ b/chromium/components/gcm_driver/instance_id/instance_id_profile_service.h @@ -0,0 +1,38 @@ +// Copyright (c) 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_PROFILE_SERVICE_H_ +#define COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_PROFILE_SERVICE_H_ + +#include + +#include "components/keyed_service/core/keyed_service.h" + +namespace gcm { +class GCMDriver; +} + +namespace instance_id { + +class InstanceIDDriver; + +// Providing Instance ID support, via InstanceIDDriver, to a profile. +class InstanceIDProfileService : public KeyedService { + public: + InstanceIDProfileService(gcm::GCMDriver* driver, bool is_off_the_record); + + InstanceIDProfileService(const InstanceIDProfileService&) = delete; + InstanceIDProfileService& operator=(const InstanceIDProfileService&) = delete; + + ~InstanceIDProfileService() override; + + InstanceIDDriver* driver() const { return driver_.get(); } + + private: + std::unique_ptr driver_; +}; + +} // namespace instance_id + +#endif // COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_PROFILE_SERVICE_H_ diff --git a/chromium/components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.cc b/chromium/components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.cc new file mode 100644 index 00000000000..585e83d6b6d --- /dev/null +++ b/chromium/components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.cc @@ -0,0 +1,25 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.h" + +#include "base/android/jni_android.h" +#include "components/gcm_driver/instance_id/android/test_support_jni_headers/FakeInstanceIDWithSubtype_jni.h" + +using base::android::AttachCurrentThread; + +namespace instance_id { + +ScopedUseFakeInstanceIDAndroid::ScopedUseFakeInstanceIDAndroid() { + JNIEnv* env = AttachCurrentThread(); + previous_value_ = + Java_FakeInstanceIDWithSubtype_clearDataAndSetEnabled(env, true); +} + +ScopedUseFakeInstanceIDAndroid::~ScopedUseFakeInstanceIDAndroid() { + JNIEnv* env = AttachCurrentThread(); + Java_FakeInstanceIDWithSubtype_clearDataAndSetEnabled(env, previous_value_); +} + +} // namespace instance_id diff --git a/chromium/components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.h b/chromium/components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.h new file mode 100644 index 00000000000..75dc5c175eb --- /dev/null +++ b/chromium/components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.h @@ -0,0 +1,31 @@ +// Copyright 2016 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 COMPONENTS_GCM_DRIVER_INSTANCE_ID_SCOPED_USE_FAKE_INSTANCE_ID_ANDROID_H_ +#define COMPONENTS_GCM_DRIVER_INSTANCE_ID_SCOPED_USE_FAKE_INSTANCE_ID_ANDROID_H_ + +#include + +namespace instance_id { + +// Tests depending on InstanceID must use this, to avoid hitting the +// network/disk. Also clears cached InstanceIDs when constructed/destructed. +class ScopedUseFakeInstanceIDAndroid { + public: + ScopedUseFakeInstanceIDAndroid(); + + ScopedUseFakeInstanceIDAndroid(const ScopedUseFakeInstanceIDAndroid&) = + delete; + ScopedUseFakeInstanceIDAndroid& operator=( + const ScopedUseFakeInstanceIDAndroid&) = delete; + + ~ScopedUseFakeInstanceIDAndroid(); + + private: + bool previous_value_; +}; + +} // namespace instance_id + +#endif // COMPONENTS_GCM_DRIVER_INSTANCE_ID_SCOPED_USE_FAKE_INSTANCE_ID_ANDROID_H_ diff --git a/chromium/components/gcm_driver/registration_info.cc b/chromium/components/gcm_driver/registration_info.cc new file mode 100644 index 00000000000..13689bbbe8c --- /dev/null +++ b/chromium/components/gcm_driver/registration_info.cc @@ -0,0 +1,286 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/gcm_driver/registration_info.h" + +#include + +#include "base/format_macros.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" + +namespace gcm { + +namespace { +constexpr char kInstanceIDSerializationPrefix[] = "iid-"; +constexpr char kSerializedValidationTimeSeparator = '#'; +constexpr char kSerializedKeySeparator = ','; +constexpr int kInstanceIDSerializationPrefixLength = + sizeof(kInstanceIDSerializationPrefix) / sizeof(char) - 1; +} // namespace + +// static +scoped_refptr RegistrationInfo::BuildFromString( + const std::string& serialized_key, + const std::string& serialized_value, + std::string* registration_id) { + scoped_refptr registration; + + if (base::StartsWith(serialized_key, kInstanceIDSerializationPrefix, + base::CompareCase::SENSITIVE)) { + registration = base::MakeRefCounted(); + } else { + registration = base::MakeRefCounted(); + } + + if (!registration->Deserialize(serialized_key, serialized_value, + registration_id)) { + registration.reset(); + } + return registration; +} + +RegistrationInfo::RegistrationInfo() = default; + +RegistrationInfo::~RegistrationInfo() = default; + +// static +const GCMRegistrationInfo* GCMRegistrationInfo::FromRegistrationInfo( + const RegistrationInfo* registration_info) { + if (!registration_info || registration_info->GetType() != GCM_REGISTRATION) + return nullptr; + return static_cast(registration_info); +} + +// static +GCMRegistrationInfo* GCMRegistrationInfo::FromRegistrationInfo( + RegistrationInfo* registration_info) { + if (!registration_info || registration_info->GetType() != GCM_REGISTRATION) + return nullptr; + return static_cast(registration_info); +} + +GCMRegistrationInfo::GCMRegistrationInfo() = default; + +GCMRegistrationInfo::~GCMRegistrationInfo() = default; + +RegistrationInfo::RegistrationType GCMRegistrationInfo::GetType() const { + return GCM_REGISTRATION; +} + +std::string GCMRegistrationInfo::GetSerializedKey() const { + // Multiple registrations are not supported for legacy GCM. So the key is + // purely based on the application id. + return app_id; +} + +std::string GCMRegistrationInfo::GetSerializedValue( + const std::string& registration_id) const { + if (sender_ids.empty() || registration_id.empty()) + return std::string(); + + // Serialize as: + // sender1,sender2,...=reg_id#time_of_last_validation + std::string value; + for (auto 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; + } + + return base::StringPrintf("%s=%s%c%" PRId64, value.c_str(), + registration_id.c_str(), + kSerializedValidationTimeSeparator, + last_validated.since_origin().InMicroseconds()); +} + +bool GCMRegistrationInfo::Deserialize(const std::string& serialized_key, + const std::string& serialized_value, + std::string* registration_id) { + if (serialized_key.empty() || serialized_value.empty()) + return false; + + // Application ID is same as the serialized key. + app_id = serialized_key; + + // Sender IDs and registration ID are constructed from the serialized value. + size_t pos_equals = serialized_value.find('='); + if (pos_equals == std::string::npos) + return false; + // Note that it's valid for pos_hash to be std::string::npos. + size_t pos_hash = serialized_value.find(kSerializedValidationTimeSeparator); + bool has_timestamp = pos_hash != std::string::npos; + + std::string senders = serialized_value.substr(0, pos_equals); + std::string registration_id_str, last_validated_str; + if (has_timestamp) { + registration_id_str = + serialized_value.substr(pos_equals + 1, pos_hash - pos_equals - 1); + last_validated_str = serialized_value.substr(pos_hash + 1); + } else { + registration_id_str = serialized_value.substr(pos_equals + 1); + } + + sender_ids = base::SplitString( + senders, ",", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY); + if (sender_ids.empty() || registration_id_str.empty()) { + sender_ids.clear(); + registration_id_str.clear(); + return false; + } + + if (registration_id) + *registration_id = registration_id_str; + int64_t last_validated_ms = 0; + if (base::StringToInt64(last_validated_str, &last_validated_ms)) { + // It's okay for |last_validated| to be the default base::Time() value + // when there is no serialized timestamp value available. + last_validated = base::Time() + base::Microseconds(last_validated_ms); + } + + return true; +} + +// static +const InstanceIDTokenInfo* InstanceIDTokenInfo::FromRegistrationInfo( + const RegistrationInfo* registration_info) { + if (!registration_info || registration_info->GetType() != INSTANCE_ID_TOKEN) + return nullptr; + return static_cast(registration_info); +} + +// static +InstanceIDTokenInfo* InstanceIDTokenInfo::FromRegistrationInfo( + RegistrationInfo* registration_info) { + if (!registration_info || registration_info->GetType() != INSTANCE_ID_TOKEN) + return nullptr; + return static_cast(registration_info); +} + +InstanceIDTokenInfo::InstanceIDTokenInfo() = default; + +InstanceIDTokenInfo::~InstanceIDTokenInfo() = default; + +RegistrationInfo::RegistrationType InstanceIDTokenInfo::GetType() const { + return INSTANCE_ID_TOKEN; +} + +std::string InstanceIDTokenInfo::GetSerializedKey() const { + DCHECK(app_id.find(',') == std::string::npos && + authorized_entity.find(',') == std::string::npos && + scope.find(',') == std::string::npos); + + // Multiple registrations are supported for Instance ID. So the key is based + // on the combination of (app_id, authorized_entity, scope). + + // Adds a prefix to differentiate easily with GCM registration key. + return base::StringPrintf("%s%s%c%s%c%s", kInstanceIDSerializationPrefix, + app_id.c_str(), kSerializedKeySeparator, + authorized_entity.c_str(), kSerializedKeySeparator, + scope.c_str()); +} + +std::string InstanceIDTokenInfo::GetSerializedValue( + const std::string& registration_id) const { + int64_t last_validated_ms = last_validated.since_origin().InMicroseconds(); + return registration_id + kSerializedValidationTimeSeparator + + base::NumberToString(last_validated_ms); +} + +bool InstanceIDTokenInfo::Deserialize(const std::string& serialized_key, + const std::string& serialized_value, + std::string* registration_id) { + if (serialized_key.empty() || serialized_value.empty()) + return false; + + if (!base::StartsWith(serialized_key, kInstanceIDSerializationPrefix, + base::CompareCase::SENSITIVE)) + return false; + + std::vector fields = base::SplitString( + serialized_key.substr(kInstanceIDSerializationPrefixLength), ",", + base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY); + if (fields.size() != 3 || fields[0].empty() || + fields[1].empty() || fields[2].empty()) { + return false; + } + app_id = fields[0]; + authorized_entity = fields[1]; + scope = fields[2]; + + // Get Registration ID and last_validated from serialized value + size_t pos_hash = serialized_value.find(kSerializedValidationTimeSeparator); + bool has_timestamp = (pos_hash != std::string::npos); + + std::string registration_id_str, last_validated_str; + if (has_timestamp) { + registration_id_str = serialized_value.substr(0, pos_hash); + last_validated_str = serialized_value.substr(pos_hash + 1); + } else { + registration_id_str = serialized_value; + } + + if (registration_id) + *registration_id = registration_id_str; + + int64_t last_validated_ms = 0; + if (base::StringToInt64(last_validated_str, &last_validated_ms)) { + // It's okay for last_validated to be the default base::Time() value + // when there is no serialized timestamp available. + last_validated += base::Microseconds(last_validated_ms); + } + + return true; +} + +bool RegistrationInfoComparer::operator()( + const scoped_refptr& a, + const scoped_refptr& b) const { + DCHECK(a.get() && b.get()); + + // For GCMRegistrationInfo, the comparison is based on app_id only. + // For InstanceIDTokenInfo, the comparison is based on + // . + if (a->app_id < b->app_id) + return true; + if (a->app_id > b->app_id) + return false; + + InstanceIDTokenInfo* iid_a = + InstanceIDTokenInfo::FromRegistrationInfo(a.get()); + InstanceIDTokenInfo* iid_b = + InstanceIDTokenInfo::FromRegistrationInfo(b.get()); + + // !iid_a && !iid_b => false. + // !iid_a && iid_b => true. + // This makes GCM record is sorted before InstanceID record. + if (!iid_a) + return iid_b != nullptr; + + // iid_a && !iid_b => false. + if (!iid_b) + return false; + + // Otherwise, compare with authorized_entity and scope. + if (iid_a->authorized_entity < iid_b->authorized_entity) + return true; + if (iid_a->authorized_entity > iid_b->authorized_entity) + return false; + return iid_a->scope < iid_b->scope; +} + +bool ExistsGCMRegistrationInMap(const RegistrationInfoMap& map, + const std::string& app_id) { + scoped_refptr gcm_registration = + base::MakeRefCounted(); + gcm_registration->app_id = app_id; + return map.find(gcm_registration) != map.end(); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/registration_info.h b/chromium/components/gcm_driver/registration_info.h new file mode 100644 index 00000000000..b5364c2cdf4 --- /dev/null +++ b/chromium/components/gcm_driver/registration_info.h @@ -0,0 +1,137 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_GCM_DRIVER_REGISTRATION_INFO_H_ +#define COMPONENTS_GCM_DRIVER_REGISTRATION_INFO_H_ + +#include +#include +#include +#include + +#include "base/memory/ref_counted.h" +#include "base/time/time.h" + +namespace gcm { + +// Encapsulates the information needed to register with the server. +struct RegistrationInfo : public base::RefCounted { + enum RegistrationType { + GCM_REGISTRATION, + INSTANCE_ID_TOKEN + }; + + // Returns the appropriate RegistrationInfo instance based on the serialized + // key and value. + // |registration_id| can be NULL if no interest to it. + static scoped_refptr BuildFromString( + const std::string& serialized_key, + const std::string& serialized_value, + std::string* registration_id); + + RegistrationInfo(); + + // Returns the type of the registration info. + virtual RegistrationType GetType() const = 0; + + // For persisting to the store. Depending on the type, part of the + // registration info is written as key. The remaining of the registration + // info plus the registration ID are written as value. + virtual std::string GetSerializedKey() const = 0; + virtual std::string GetSerializedValue( + const std::string& registration_id) const = 0; + // |registration_id| can be NULL if it is of no interest to the caller. + virtual bool Deserialize(const std::string& serialized_key, + const std::string& serialized_value, + std::string* registration_id) = 0; + + // Every registration is associated with an application. + std::string app_id; + base::Time last_validated; + + protected: + friend class base::RefCounted; + virtual ~RegistrationInfo(); +}; + +// For GCM registration. +struct GCMRegistrationInfo final : public RegistrationInfo { + GCMRegistrationInfo(); + + // Converts from the base type; + static const GCMRegistrationInfo* FromRegistrationInfo( + const RegistrationInfo* registration_info); + static GCMRegistrationInfo* FromRegistrationInfo( + RegistrationInfo* registration_info); + + // RegistrationInfo overrides: + RegistrationType GetType() const override; + std::string GetSerializedKey() const override; + std::string GetSerializedValue( + const std::string& registration_id) const override; + bool Deserialize(const std::string& serialized_key, + const std::string& serialized_value, + std::string* registration_id) override; + + // 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. + std::vector sender_ids; + + private: + ~GCMRegistrationInfo() override; +}; + +// For InstanceID token retrieval. +struct InstanceIDTokenInfo final : public RegistrationInfo { + InstanceIDTokenInfo(); + + // Converts from the base type; + static const InstanceIDTokenInfo* FromRegistrationInfo( + const RegistrationInfo* registration_info); + static InstanceIDTokenInfo* FromRegistrationInfo( + RegistrationInfo* registration_info); + + // RegistrationInfo overrides: + RegistrationType GetType() const override; + std::string GetSerializedKey() const override; + std::string GetSerializedValue( + const std::string& registration_id) const override; + bool Deserialize(const std::string& serialized_key, + const std::string& serialized_value, + std::string* registration_id) override; + + // Entity that is authorized to access resources associated with the Instance + // ID. It can be another Instance ID or a project ID assigned by the Google + // API Console. + std::string authorized_entity; + + // Authorized actions that the authorized entity can take. + // E.g. for sending GCM messages, 'GCM' scope should be used. + std::string scope; + + // Specifies TTL of retrievable token, zero value means unlimited TTL. + // Not serialized/deserialized. + base::TimeDelta time_to_live; + + private: + ~InstanceIDTokenInfo() override; +}; + +struct RegistrationInfoComparer { + bool operator()(const scoped_refptr& a, + const scoped_refptr& b) const; +}; + +// Collection of registration info. +// Map from RegistrationInfo instance to registration ID. +using RegistrationInfoMap = std:: + map, std::string, RegistrationInfoComparer>; + +// Returns true if a GCM registration for |app_id| exists in |map|. +bool ExistsGCMRegistrationInMap(const RegistrationInfoMap& map, + const std::string& app_id); + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_REGISTRATION_INFO_H_ diff --git a/chromium/components/gcm_driver/resources/OWNERS b/chromium/components/gcm_driver/resources/OWNERS new file mode 100644 index 00000000000..c05db2e16e1 --- /dev/null +++ b/chromium/components/gcm_driver/resources/OWNERS @@ -0,0 +1,2 @@ +# For trivial or mechanical horizontal JS/CSS/HTML changes. +file://ui/webui/PLATFORM_OWNERS diff --git a/chromium/components/gcm_driver/resources/gcm_internals.css b/chromium/components/gcm_driver/resources/gcm_internals.css new file mode 100644 index 00000000000..82ebe76a375 --- /dev/null +++ b/chromium/components/gcm_driver/resources/gcm_internals.css @@ -0,0 +1,47 @@ +/* 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. */ + +h1 { + color: rgb(74, 142, 230); + margin: 0; + padding: 0; +} + +td { + padding: 4px; +} + +tr:nth-child(odd) { + background-color: rgb(245, 245, 200); +} + +th { + background-color: rgb(160, 160, 125); + color: rgb(255, 255, 255); + font-weight: bold; +} + +.flexbar { + display: flex; + flex-direction: row; + margin: 5px 0px; +} + +.flexbar button { + padding: 0px 4px; +} + +#device-info tr > :first-child { + font-weight: bold; + padding-right: 10px; + text-align: end; +} + +.log-table { + padding: 4px; +} + +#android-secret-container.invisible { + display: none; +} diff --git a/chromium/components/gcm_driver/resources/gcm_internals.html b/chromium/components/gcm_driver/resources/gcm_internals.html new file mode 100644 index 00000000000..70c3856c5e7 --- /dev/null +++ b/chromium/components/gcm_driver/resources/gcm_internals.html @@ -0,0 +1,210 @@ + + + + + GCM Internals + + + + + + + + +

GCM Internals

+
+ + + +
+ +

Device Info

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Android Id + + + +
+ User Profile Service Created + +
+ GCM Enabled + +
+ GCM Client Created + +
+ GCM Client State + +
+ Connection Client Created + +
+ Connection State + +
+ Last Checkin + +
+ Next Checkin + +
+ Registered App Ids + +
+ Send Message Queue Size + +
+ Resend Message Queue Size + +
+ + +

Check-in Log

+ + + + + + + + + + +
TimeEventDetails
+ +

Connection Log

+ + + + + + + + + + +
TimeEventDetails
+
+ +

Registration Log

+ + + + + + + + + + + + +
TimeApp IdSourceEventDetails
+ +

Receive Message Log

+ + + + + + + + + + + + + +
TimeApp IdFromSize (bytes)EventDetails
+ +

Message Decryption Failure Log

+ + + + + + + + + + +
TimeApp IdDetails
+ + +

Send Message Log

+ + + + + + + + + + + + + +
TimeApp IdReceiver IdMsg IdEventDetails
+
+ + + diff --git a/chromium/components/gcm_driver/resources/gcm_internals.js b/chromium/components/gcm_driver/resources/gcm_internals.js new file mode 100644 index 00000000000..32fe6a0ef51 --- /dev/null +++ b/chromium/components/gcm_driver/resources/gcm_internals.js @@ -0,0 +1,222 @@ +// 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 'chrome://resources/js/ios/web_ui.js'; +// + +import './strings.m.js'; +import {addWebUIListener} from 'chrome://resources/js/cr.m.js'; +import {$} from 'chrome://resources/js/util.m.js'; + +let isRecording = false; +let keyPressState = 0; + +/** + * If the info dictionary has property prop, then set the text content of + * element to the value of this property. Otherwise clear the content. + * @param {!Object} info A dictionary of device infos to be displayed. + * @param {string} prop Name of the property. + * @param {string} elementId The id of a HTML element. + */ +function setIfExists(info, prop, elementId) { + const element = $(elementId); + if (!element) { + return; + } + + if (info[prop] !== undefined) { + element.textContent = info[prop]; + } else { + element.textContent = ''; + } +} + +/** + * Sets the registeredAppIds from |info| to the element identified by + * |elementId|. The list will have duplicates counted and visually shown. + * @param {!Object} info A dictionary of device infos to be displayed. + * @param {string} prop Name of the property. + * @param {string} elementId The id of a HTML element. + */ +function setRegisteredAppIdsIfExists(info, prop, elementId) { + const element = $(elementId); + if (!element) { + return; + } + + if (info[prop] === undefined || !Array.isArray(info[prop])) { + return; + } + + const registeredAppIds = new Map(); + info[prop].forEach(registeredAppId => { + registeredAppIds.set( + registeredAppId, (registeredAppIds.get(registeredAppId) || 0) + 1); + }); + + const list = []; + for (const [registeredAppId, count] of registeredAppIds.entries()) { + list.push(registeredAppId + (count > 1 ? ` (x${count})` : ``)); + } + + element.textContent = list.join(', '); +} + +/** + * Display device information. + * @param {!Object} info A dictionary of device infos to be displayed. + */ +function displayDeviceInfo(info) { + setIfExists(info, 'androidId', 'android-id'); + setIfExists(info, 'androidSecret', 'android-secret'); + setIfExists(info, 'profileServiceCreated', 'profile-service-created'); + setIfExists(info, 'gcmEnabled', 'gcm-enabled'); + setIfExists(info, 'gcmClientCreated', 'gcm-client-created'); + setIfExists(info, 'gcmClientState', 'gcm-client-state'); + setIfExists(info, 'connectionClientCreated', 'connection-client-created'); + setIfExists(info, 'connectionState', 'connection-state'); + setIfExists(info, 'lastCheckin', 'last-checkin'); + setIfExists(info, 'nextCheckin', 'next-checkin'); + setIfExists(info, 'sendQueueSize', 'send-queue-size'); + setIfExists(info, 'resendQueueSize', 'resend-queue-size'); + + setRegisteredAppIdsIfExists(info, 'registeredAppIds', 'registered-app-ids'); +} + +/** + * Remove all the child nodes of the element. + * @param {HTMLElement} element A HTML element. + */ +function removeAllChildNodes(element) { + element.textContent = ''; +} + +/** + * For each item in line, add a row to the table. Each item is actually a list + * of sub-items; each of which will have a corresponding cell created in that + * row, and the sub-item will be displayed in the cell. + * @param {HTMLElement} table A HTML tbody element. + * @param {!Object} list A list of list of item. + */ +function addRows(table, list) { + for (let i = 0; i < list.length; ++i) { + const row = document.createElement('tr'); + + // The first element is always a timestamp. + let cell = document.createElement('td'); + const d = new Date(list[i][0]); + cell.textContent = d; + row.appendChild(cell); + + for (let j = 1; j < list[i].length; ++j) { + cell = document.createElement('td'); + cell.textContent = list[i][j]; + row.appendChild(cell); + } + table.appendChild(row); + } +} + +/** + * Refresh all displayed information. + */ +function refreshAll() { + chrome.send('getGcmInternalsInfo', [false]); +} + +/** + * Toggle the isRecording variable and send it to browser. + */ +function setRecording() { + isRecording = !isRecording; + chrome.send('setGcmInternalsRecording', [isRecording]); +} + +/** + * Clear all the activity logs. + */ +function clearLogs() { + chrome.send('getGcmInternalsInfo', [true]); +} + +function initialize() { + addWebUIListener('set-gcm-internals-info', setGcmInternalsInfo); + $('recording').disabled = true; + $('refresh').onclick = refreshAll; + $('recording').onclick = setRecording; + $('clear-logs').onclick = clearLogs; + chrome.send('getGcmInternalsInfo', [false]); + + // Recording defaults to on. + chrome.send('setGcmInternalsRecording', [true]); +} + +/** + * Allows displaying the Android Secret by typing a secret phrase. + * + * There are good reasons for displaying the Android Secret associated with + * the local connection info, but we also need to be careful to make sure that + * users don't share this value by accident. Therefore we require a secret + * phrase to be typed into the page for making it visible. + * + * @param {!Event} event The keypress event handler. + */ +function handleKeyPress(event) { + const PHRASE = 'secret'; + if (PHRASE.charCodeAt(keyPressState) === event.keyCode) { + if (++keyPressState < PHRASE.length) { + return; + } + + $('android-secret-container').classList.remove('invisible'); + } + + keyPressState = 0; +} + +/** + * Refresh the log html table by clearing it first. If data is not empty, then + * it will be used to populate the table. + * @param {string} tableId ID of the log html table. + * @param {!Object} data A list of list of data items. + */ +function refreshLogTable(tableId, data) { + const element = $(tableId); + if (!element) { + return; + } + + removeAllChildNodes(element); + if (data !== undefined) { + addRows(element, data); + } +} + +/** + * Callback function accepting a dictionary of info items to be displayed. + * @param {!Object} infos A dictionary of info items to be displayed. + */ +function setGcmInternalsInfo(infos) { + isRecording = infos.isRecording; + if (isRecording) { + $('recording').textContent = 'Stop Recording'; + } else { + $('recording').textContent = 'Start Recording'; + } + $('recording').disabled = false; + if (infos.deviceInfo !== undefined) { + displayDeviceInfo(infos.deviceInfo); + } + + refreshLogTable('checkin-info', infos.checkinInfo); + refreshLogTable('connection-info', infos.connectionInfo); + refreshLogTable('registration-info', infos.registrationInfo); + refreshLogTable('receive-info', infos.receiveInfo); + refreshLogTable('decryption-failure-info', infos.decryptionFailureInfo); + refreshLogTable('send-info', infos.sendInfo); +} + +document.addEventListener('DOMContentLoaded', initialize); +document.addEventListener('keypress', handleKeyPress); diff --git a/chromium/components/gcm_driver/system_encryptor.cc b/chromium/components/gcm_driver/system_encryptor.cc new file mode 100644 index 00000000000..bc731a30872 --- /dev/null +++ b/chromium/components/gcm_driver/system_encryptor.cc @@ -0,0 +1,23 @@ +// 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 "components/gcm_driver/system_encryptor.h" + +#include "components/os_crypt/os_crypt.h" + +namespace gcm { + +SystemEncryptor::~SystemEncryptor() {} + +bool SystemEncryptor::EncryptString(const std::string& plaintext, + std::string* ciphertext) { + return ::OSCrypt::EncryptString(plaintext, ciphertext); +} + +bool SystemEncryptor::DecryptString(const std::string& ciphertext, + std::string* plaintext) { + return ::OSCrypt::DecryptString(ciphertext, plaintext); +} + +} // namespace gcm diff --git a/chromium/components/gcm_driver/system_encryptor.h b/chromium/components/gcm_driver/system_encryptor.h new file mode 100644 index 00000000000..02339ad5559 --- /dev/null +++ b/chromium/components/gcm_driver/system_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 COMPONENTS_GCM_DRIVER_SYSTEM_ENCRYPTOR_H_ +#define COMPONENTS_GCM_DRIVER_SYSTEM_ENCRYPTOR_H_ + +#include "base/compiler_specific.h" +#include "google_apis/gcm/base/encryptor.h" + +namespace gcm { + +// Encryptor that uses the Chrome password manager's encryptor. +class SystemEncryptor : public Encryptor { + public: + ~SystemEncryptor() override; + + bool EncryptString(const std::string& plaintext, + std::string* ciphertext) override; + + bool DecryptString(const std::string& ciphertext, + std::string* plaintext) override; +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_SYSTEM_ENCRYPTOR_H_ -- cgit v1.2.1