diff options
-rw-r--r-- | chrome/browser/sync/engine/auth_watcher.cc | 275 | ||||
-rw-r--r-- | chrome/browser/sync/engine/auth_watcher.h | 112 | ||||
-rw-r--r-- | chrome/browser/sync/engine/auth_watcher_unittest.cc | 214 | ||||
-rw-r--r-- | chrome/browser/sync/engine/net/gaia_authenticator.h | 9 | ||||
-rw-r--r-- | chrome/browser/sync/engine/net/server_connection_manager.h | 4 | ||||
-rw-r--r-- | chrome/browser/sync/engine/syncapi.cc | 23 | ||||
-rw-r--r-- | chrome/browser/sync/notifier/listener/talk_mediator_impl.h | 1 | ||||
-rwxr-xr-x | chrome/chrome.gyp | 1 | ||||
-rw-r--r-- | chrome/test/sync/engine/mock_server_connection.cc | 43 | ||||
-rw-r--r-- | chrome/test/sync/engine/mock_server_connection.h | 14 | ||||
-rw-r--r-- | chrome/test/sync/engine/test_directory_setter_upper.cc | 21 | ||||
-rw-r--r-- | chrome/test/sync/engine/test_directory_setter_upper.h | 14 |
12 files changed, 472 insertions, 259 deletions
diff --git a/chrome/browser/sync/engine/auth_watcher.cc b/chrome/browser/sync/engine/auth_watcher.cc index 87c899f..c722a8d 100644 --- a/chrome/browser/sync/engine/auth_watcher.cc +++ b/chrome/browser/sync/engine/auth_watcher.cc @@ -11,12 +11,10 @@ #include "chrome/browser/sync/engine/net/gaia_authenticator.h" #include "chrome/browser/sync/engine/net/server_connection_manager.h" #include "chrome/browser/sync/notifier/listener/talk_mediator.h" -#include "chrome/browser/sync/protocol/service_constants.h" #include "chrome/browser/sync/syncable/directory_manager.h" #include "chrome/browser/sync/syncable/syncable.h" #include "chrome/browser/sync/util/character_set_converters.h" #include "chrome/browser/sync/util/event_sys-inl.h" -#include "chrome/browser/sync/util/pthread_helpers.h" #include "chrome/browser/sync/util/user_settings.h" // How authentication happens: @@ -57,9 +55,14 @@ AuthWatcher::AuthWatcher(DirectoryManager* dirman, status_(NOT_AUTHENTICATED), user_settings_(user_settings), talk_mediator_(talk_mediator), - thread_handle_valid_(false), - authenticating_now_(false), + auth_backend_thread_("SyncEngine_AuthWatcherThread"), current_attempt_trigger_(AuthWatcherEvent::USER_INITIATED) { + + if (!auth_backend_thread_.Start()) + NOTREACHED() << "Couldn't start SyncEngine_AuthWatcherThread"; + + gaia_->set_message_loop(message_loop()); + connmgr_hookup_.reset( NewEventListenerHookup(scm->channel(), this, &AuthWatcher::HandleServerConnectionEvent)); @@ -67,12 +70,8 @@ AuthWatcher::AuthWatcher(DirectoryManager* dirman, channel_.reset(new Channel(done)); } -void* AuthWatcher::AuthenticationThreadStartRoutine(void* arg) { - ThreadParams* args = reinterpret_cast<ThreadParams*>(arg); - return args->self->AuthenticationThreadMain(args); -} - -bool AuthWatcher::ProcessGaiaAuthSuccess() { +void AuthWatcher::PersistCredentials() { + DCHECK_EQ(MessageLoop::current(), message_loop()); GaiaAuthenticator::AuthResults results = gaia_->results(); // We just successfully signed in again, let's clear out any residual cached @@ -92,48 +91,19 @@ bool AuthWatcher::ProcessGaiaAuthSuccess() { SYNC_SERVICE_NAME, gaia_->auth_token()); } - - return AuthenticateWithToken(results.email, gaia_->auth_token()); } -bool AuthWatcher::GetAuthTokenForService(const string& service_name, - string* service_token) { - string user_name; - - // We special case this one by trying to return it from memory first. We - // do this because the user may not have checked "Remember me" and so we - // may not have persisted the sync service token beyond the initial - // login. - if (SYNC_SERVICE_NAME == service_name && !sync_service_token_.empty()) { - *service_token = sync_service_token_; - return true; - } - - if (user_settings_->GetLastUserAndServiceToken(service_name, &user_name, - service_token)) { - // The casing gets preserved in some places and not in others it seems, - // at least I have observed different casings persisted to different DB - // tables. - if (!base::strcasecmp(user_name.c_str(), - user_settings_->email().c_str())) { - return true; - } else { - LOG(ERROR) << "ERROR: We seem to have saved credentials for someone " - << " other than the current user."; - return false; - } - } +const char kAuthWatcher[] = "AuthWatcher"; - return false; +void AuthWatcher::AuthenticateWithToken(const std::string& gaia_email, + const std::string& auth_token) { + message_loop()->PostTask(FROM_HERE, NewRunnableMethod(this, + &AuthWatcher::DoAuthenticateWithToken, gaia_email, auth_token)); } -const char kAuthWatcher[] = "AuthWatcher"; - -bool AuthWatcher::AuthenticateWithToken(const string& gaia_email, - const string& auth_token) { - // Store a copy of the sync service token in memory. - sync_service_token_ = auth_token; - scm_->set_auth_token(sync_service_token_); +void AuthWatcher::DoAuthenticateWithToken(const std::string& gaia_email, + const std::string& auth_token) { + DCHECK_EQ(MessageLoop::current(), message_loop()); Authenticator auth(scm_, user_settings_); Authenticator::AuthenticationResult result = @@ -158,11 +128,12 @@ bool AuthWatcher::AuthenticateWithToken(const string& gaia_email, // Set the authentication token for notifications talk_mediator_->SetAuthToken(email, auth_token); + scm_->set_auth_token(auth_token); if (!was_authenticated) LoadDirectoryListAndOpen(share_name); NotifyAuthSucceeded(email); - return true; + return; } case Authenticator::BAD_AUTH_TOKEN: event.what_happened = AuthWatcherEvent::SERVICE_AUTH_FAILED; @@ -176,19 +147,19 @@ bool AuthWatcher::AuthenticateWithToken(const string& gaia_email, break; default: LOG(FATAL) << "Illegal return from AuthenticateToken"; - return true; // keep the compiler happy + return; } // Always fall back to local authentication. if (was_authenticated || AuthenticateLocally(email)) { if (AuthWatcherEvent::SERVICE_CONNECTION_FAILED == event.what_happened) - return true; + return; } - CHECK(event.what_happened != AuthWatcherEvent::ILLEGAL_VALUE); + DCHECK_NE(event.what_happened, AuthWatcherEvent::ILLEGAL_VALUE); NotifyListeners(&event); - return true; } bool AuthWatcher::AuthenticateLocally(string email) { + DCHECK_EQ(MessageLoop::current(), message_loop()); user_settings_->GetEmailForSignin(&email); if (file_util::PathExists(FilePath(dirman_->GetSyncDataDatabasePath()))) { gaia_->SetUsername(email); @@ -205,12 +176,14 @@ bool AuthWatcher::AuthenticateLocally(string email) { } bool AuthWatcher::AuthenticateLocally(string email, const string& password) { + DCHECK_EQ(MessageLoop::current(), message_loop()); user_settings_->GetEmailForSignin(&email); return user_settings_->VerifyAgainstStoredHash(email, password) && AuthenticateLocally(email); } void AuthWatcher::ProcessGaiaAuthFailure() { + DCHECK_EQ(MessageLoop::current(), message_loop()); GaiaAuthenticator::AuthResults results = gaia_->results(); if (LOCALLY_AUTHENTICATED == status_) { return; // nothing todo @@ -234,77 +207,44 @@ void AuthWatcher::ProcessGaiaAuthFailure() { NotifyListeners(&myevent); } -void* AuthWatcher::AuthenticationThreadMain(ThreadParams* args) { - NameCurrentThreadForDebugging("SyncEngine_AuthWatcherThread"); - // TODO(timsteele): Remove this; this is temporary to satisfy code that - // compares MessageLoop pointers until AuthWatcher uses a base::Thread. - // We allocate a message_loop from the AuthWatcherThread so that - // GaiaAuth and EventChannel get a valid opaque pointer to the current - // message loop used for comparison. It gets stored in TLS as the 'current' - // loop for this thread. - MessageLoop message_loop; - { - // This short lock ensures our launching function (StartNewAuthAttempt) is - // done. - MutexLock lock(&mutex_); - current_attempt_trigger_ = args->trigger; - } - SaveCredentials save = args->persist_creds_to_disk ? +void AuthWatcher::DoAuthenticate(const AuthRequest& request) { + DCHECK_EQ(MessageLoop::current(), message_loop()); + + AuthWatcherEvent event = { AuthWatcherEvent::AUTHENTICATION_ATTEMPT_START }; + NotifyListeners(&event); + + current_attempt_trigger_ = request.trigger; + + SaveCredentials save = request.persist_creds_to_disk ? PERSIST_TO_DISK : SAVE_IN_MEMORY_ONLY; - int attempt = 0; SignIn const signin = user_settings_-> - RecallSigninType(args->email, GMAIL_SIGNIN); - - gaia_->set_message_loop(&message_loop); - - if (!args->password.empty()) { - // TODO(timsteele): Split this mess up into functions. - while (true) { - bool authenticated; - if (!args->captcha_token.empty() && !args->captcha_value.empty()) { - authenticated = gaia_->Authenticate(args->email, args->password, - save, args->captcha_token, - args->captcha_value, signin); - } else { - authenticated = gaia_->Authenticate(args->email, args->password, - save, signin); - } - if (authenticated) { - if (!ProcessGaiaAuthSuccess()) { - if (3 != ++attempt) { - continue; - } - AuthWatcherEvent event = - { AuthWatcherEvent::SERVICE_CONNECTION_FAILED, 0 }; - NotifyListeners(&event); - } - } else { - ProcessGaiaAuthFailure(); - } - break; // We are done trying to authenticate. + RecallSigninType(request.email, GMAIL_SIGNIN); + + if (!request.password.empty()) { + bool authenticated = false; + if (!request.captcha_token.empty() && !request.captcha_value.empty()) { + authenticated = gaia_->Authenticate(request.email, request.password, + save, request.captcha_token, + request.captcha_value, signin); + } else { + authenticated = gaia_->Authenticate(request.email, request.password, + save, signin); } - } else if (!args->auth_token.empty()) { - AuthenticateWithToken(args->email, args->auth_token); + if (authenticated) { + PersistCredentials(); + DoAuthenticateWithToken(gaia_->email(), gaia_->auth_token()); + } else { + ProcessGaiaAuthFailure(); + } + } else if (!request.auth_token.empty()) { + DoAuthenticateWithToken(request.email, request.auth_token); } else { - LOG(ERROR) << "Attempt to authenticate with no credentials."; - } - - // We're done trying to authenticate. Set state and terminate the thread. - { - MutexLock lock(&mutex_); - authenticating_now_ = false; + LOG(ERROR) << "Attempt to authenticate with no credentials."; } - // TODO(timsteele): Remove this; nothing ever gets posted to this loop. - gaia_->set_message_loop(NULL); - delete args; - return 0; -} - -void AuthWatcher::Reset() { - status_ = NOT_AUTHENTICATED; } void AuthWatcher::NotifyAuthSucceeded(const string& email) { + DCHECK_EQ(MessageLoop::current(), message_loop()); LOG(INFO) << "NotifyAuthSucceeded"; AuthWatcherEvent event = { AuthWatcherEvent::AUTH_SUCCEEDED }; event.user_email = email; @@ -312,76 +252,40 @@ void AuthWatcher::NotifyAuthSucceeded(const string& email) { NotifyListeners(&event); } -bool AuthWatcher::StartNewAuthAttempt(const string& email, - const string& password, const string& auth_token, - const string& captcha_token, const string& captcha_value, - bool persist_creds_to_disk, - AuthWatcherEvent::AuthenticationTrigger trigger) { - AuthWatcherEvent event = { AuthWatcherEvent::AUTHENTICATION_ATTEMPT_START }; - NotifyListeners(&event); - MutexLock lock(&mutex_); - if (authenticating_now_) - return false; - if (thread_handle_valid_) { - int join_return = pthread_join(thread_, 0); - if (0 != join_return) - LOG(ERROR) << "pthread_join failed returning " << join_return; - } - string mail = email; - if (email.find('@') == string::npos) { - mail.push_back('@'); - // TODO(chron): Should this be done only at the UI level? - mail.append(DEFAULT_SIGNIN_DOMAIN); - } - ThreadParams* args = new ThreadParams; - args->self = this; - args->email = mail; - args->password = password; - args->auth_token = auth_token; - args->captcha_token = captcha_token; - args->captcha_value = captcha_value; - args->persist_creds_to_disk = persist_creds_to_disk; - args->trigger = trigger; - if (0 != pthread_create(&thread_, NULL, AuthenticationThreadStartRoutine, - args)) { - LOG(ERROR) << "Failed to create auth thread."; - return false; - } - authenticating_now_ = true; - thread_handle_valid_ = true; - return true; -} - -void AuthWatcher::WaitForAuthThreadFinish() { - { - MutexLock lock(&mutex_); - if (!thread_handle_valid_) - return; - } - pthread_join(thread_, 0); -} - void AuthWatcher::HandleServerConnectionEvent( const ServerConnectionEvent& event) { + message_loop()->PostTask(FROM_HERE, NewRunnableMethod(this, + &AuthWatcher::DoHandleServerConnectionEvent, event, + scm_->auth_token())); +} + +void AuthWatcher::DoHandleServerConnectionEvent( + const ServerConnectionEvent& event, + const std::string& auth_token_snapshot) { + DCHECK_EQ(MessageLoop::current(), message_loop()); if (event.server_reachable && - !authenticating_now_ && + // If the auth_token at the time of the event differs from the current + // one, we have authenticated since then and don't need to re-try. + (auth_token_snapshot == gaia_->auth_token()) && (event.connection_code == HttpResponse::SYNC_AUTH_ERROR || - status_ == LOCALLY_AUTHENTICATED)) { + status_ == LOCALLY_AUTHENTICATED)) { // We're either online or just got reconnected and want to try to // authenticate. If we've got a saved token this should just work. If not // the auth failure should trigger UI indications that we're not logged in. // METRIC: If we get a SYNC_AUTH_ERROR, our token expired. GaiaAuthenticator::AuthResults authresults = gaia_->results(); - if (!StartNewAuthAttempt(authresults.email, authresults.password, - authresults.auth_token, "", "", - PERSIST_TO_DISK == authresults.credentials_saved, - AuthWatcherEvent::EXPIRED_CREDENTIALS)) - LOG(INFO) << "Couldn't start a new auth attempt."; + AuthRequest request = { authresults.email, authresults.password, + authresults.auth_token, std::string(), + std::string(), + PERSIST_TO_DISK == authresults.credentials_saved, + AuthWatcherEvent::EXPIRED_CREDENTIALS }; + DoAuthenticate(request); } } bool AuthWatcher::LoadDirectoryListAndOpen(const PathString& login) { + DCHECK_EQ(MessageLoop::current(), message_loop()); LOG(INFO) << "LoadDirectoryListAndOpen(" << login << ")"; bool initial_sync_ended = false; @@ -395,33 +299,30 @@ bool AuthWatcher::LoadDirectoryListAndOpen(const PathString& login) { } AuthWatcher::~AuthWatcher() { - WaitForAuthThreadFinish(); + auth_backend_thread_.Stop(); + // The gaia authenticator takes a const MessageLoop* because it only uses it + // to ensure all methods are invoked on the given loop. Once our thread has + // stopped, the current message loop will be NULL, and no methods should be + // invoked on |gaia_| after this point. We could set it to NULL, but + // abstaining allows for even more sanity checking that nothing is invoked on + // it from now on. } void AuthWatcher::Authenticate(const string& email, const string& password, const string& captcha_token, const string& captcha_value, bool persist_creds_to_disk) { LOG(INFO) << "AuthWatcher::Authenticate called"; - WaitForAuthThreadFinish(); - // We CHECK here because WaitForAuthThreadFinish should ensure there's no - // ongoing auth attempt. string empty; - CHECK(StartNewAuthAttempt(email, password, empty, captcha_token, - captcha_value, persist_creds_to_disk, - AuthWatcherEvent::USER_INITIATED)); -} - -void AuthWatcher::Logout() { - scm_->ResetAuthStatus(); - Reset(); - WaitForAuthThreadFinish(); - ClearAuthenticationData(); + AuthRequest request = { FormatAsEmailAddress(email), password, empty, + captcha_token, captcha_value, persist_creds_to_disk, + AuthWatcherEvent::USER_INITIATED }; + message_loop()->PostTask(FROM_HERE, NewRunnableMethod(this, + &AuthWatcher::DoAuthenticate, request)); } void AuthWatcher::ClearAuthenticationData() { - sync_service_token_.clear(); - scm_->set_auth_token(sync_service_token()); + scm_->set_auth_token(std::string()); user_settings_->ClearAllServiceTokens(); } diff --git a/chrome/browser/sync/engine/auth_watcher.h b/chrome/browser/sync/engine/auth_watcher.h index 00edcf9..9457e33 100644 --- a/chrome/browser/sync/engine/auth_watcher.h +++ b/chrome/browser/sync/engine/auth_watcher.h @@ -12,11 +12,14 @@ #include <string> #include "base/atomicops.h" +#include "base/ref_counted.h" #include "base/scoped_ptr.h" +#include "base/thread.h" #include "chrome/browser/sync/engine/net/gaia_authenticator.h" +#include "chrome/browser/sync/protocol/service_constants.h" #include "chrome/browser/sync/util/event_sys.h" -#include "chrome/browser/sync/util/pthread_helpers.h" #include "chrome/browser/sync/util/sync_types.h" +#include "testing/gtest/include/gtest/gtest_prod.h" // For FRIEND_TEST namespace syncable { struct DirectoryManagerEvent; @@ -65,7 +68,15 @@ struct AuthWatcherEvent { AuthenticationTrigger trigger; }; -class AuthWatcher { +// The mother-class of Authentication for the sync backend. Handles both gaia +// and sync service authentication via asynchronous Authenticate* methods, +// raising AuthWatcherEvents on success/failure. The implementation currently +// runs its own backend thread for the actual auth processing, which means +// the AuthWatcherEvents can be raised on a different thread than the one that +// invoked authentication. +class AuthWatcher : public base::RefCountedThreadSafe<AuthWatcher> { + friend class AuthWatcherTest; + FRIEND_TEST(AuthWatcherTest, Construction); public: // Normal progression is local -> gaia -> token. enum Status { LOCALLY_AUTHENTICATED, GAIA_AUTHENTICATED, NOT_AUTHENTICATED }; @@ -93,6 +104,8 @@ class AuthWatcher { return channel_.get(); } + // The following 3 flavors of authentication routines are asynchronous and can + // be called from any thread. void Authenticate(const std::string& email, const std::string& password, const std::string& captcha_token, const std::string& captcha_value, bool persist_creds_to_disk); @@ -102,11 +115,14 @@ class AuthWatcher { Authenticate(email, password, "", "", persist_creds_to_disk); } - // Retrieves an auth token for a named service for which a long-lived token - // was obtained at login time. Returns true if a long-lived token can be - // found, false otherwise. - bool GetAuthTokenForService(const std::string& service_name, - std::string* service_token); + // Use this version when you don't need the gaia authentication step because + // you already have a valid token for |gaia_email|. + void AuthenticateWithToken(const std::string& gaia_email, + const std::string& auth_token); + + // Joins on the backend thread. The AuthWatcher is useless after this and + // should be destroyed. + void Shutdown() { auth_backend_thread_.Stop(); } std::string email() const; syncable::DirectoryManager* dirman() const { return dirman_; } @@ -115,33 +131,23 @@ class AuthWatcher { UserSettings* settings() const { return user_settings_; } Status status() const { return (Status)status_; } - void Logout(); - - // For synchronizing other destructors. - void WaitForAuthThreadFinish(); - - bool AuthenticateWithToken(const std::string& email, - const std::string& auth_token); - protected: - void Reset(); void ClearAuthenticationData(); void NotifyAuthSucceeded(const std::string& email); - bool StartNewAuthAttempt(const std::string& email, - const std::string& password, - const std::string& auth_token, const std::string& captcha_token, - const std::string& captcha_value, bool persist_creds_to_disk, - AuthWatcherEvent::AuthenticationTrigger trigger); void HandleServerConnectionEvent(const ServerConnectionEvent& event); void SaveUserSettings(const std::string& username, const std::string& auth_token, const bool save_credentials); + MessageLoop* message_loop() { return auth_backend_thread_.message_loop(); } + + private: // These two helpers should only be called from the auth function. - // Returns false iff we had problems and should try GAIA_AUTH again. - bool ProcessGaiaAuthSuccess(); + // Called when authentication with gaia succeeds, to save credential info. + void PersistCredentials(); + // Called when authentication with gaia fails. void ProcessGaiaAuthFailure(); // Just checks that the user has at least one local share cache. @@ -152,48 +158,52 @@ class AuthWatcher { // Sets the trigger member of the event and sends the event on channel_. void NotifyListeners(AuthWatcherEvent* event); - const std::string& sync_service_token() const { return sync_service_token_; } - - typedef PThreadScopedLock<PThreadMutex> MutexLock; + inline std::string FormatAsEmailAddress(const std::string& email) const { + std::string mail(email); + if (email.find('@') == std::string::npos) { + mail.push_back('@'); + // TODO(chron): Should this be done only at the UI level? + mail.append(DEFAULT_SIGNIN_DOMAIN); + } + return mail; + } - // Passed to newly created threads. - struct ThreadParams { - AuthWatcher* self; - std::string email; - std::string password; - std::string auth_token; - std::string captcha_token; - std::string captcha_value; - bool persist_creds_to_disk; - AuthWatcherEvent::AuthenticationTrigger trigger; + // A struct to marshal various data across to the auth_backend_thread_ on + // Authenticate() and AuthenticateWithToken calls. + struct AuthRequest { + std::string email; + std::string password; + std::string auth_token; + std::string captcha_token; + std::string captcha_value; + bool persist_creds_to_disk; + AuthWatcherEvent::AuthenticationTrigger trigger; }; - // Initial function passed to pthread_create. - static void* AuthenticationThreadStartRoutine(void* arg); - // Member function called by AuthenticationThreadStartRoutine. - void* AuthenticationThreadMain(struct ThreadParams* arg); + // The public interface Authenticate methods are proxies to these, which + // can only be called from |auth_backend_thread_|. + void DoAuthenticate(const AuthRequest& request); + void DoAuthenticateWithToken(const std::string& email, + const std::string& auth_token); + + // The public HandleServerConnectionEvent method proxies to this method, which + // can only be called on |auth_backend_thread_|. + void DoHandleServerConnectionEvent( + const ServerConnectionEvent& event, + const std::string& auth_token_snapshot); scoped_ptr<GaiaAuthenticator> const gaia_; syncable::DirectoryManager* const dirman_; ServerConnectionManager* const scm_; scoped_ptr<EventListenerHookup> connmgr_hookup_; AllStatus* const allstatus_; - // TODO(chron): It is incorrect to make assignments to AtomicWord. - volatile base::subtle::AtomicWord status_; + Status status_; UserSettings* const user_settings_; TalkMediator* talk_mediator_; // Interface to the notifications engine. scoped_ptr<Channel> channel_; - // We store our service token in memory as a workaround to the fact that we - // don't persist it when the user unchecks "remember me". - // We also include it on outgoing requests. - std::string sync_service_token_; + base::Thread auth_backend_thread_; - PThreadMutex mutex_; - // All members below are protected by the above mutex - pthread_t thread_; - bool thread_handle_valid_; - bool authenticating_now_; AuthWatcherEvent::AuthenticationTrigger current_attempt_trigger_; DISALLOW_COPY_AND_ASSIGN(AuthWatcher); }; diff --git a/chrome/browser/sync/engine/auth_watcher_unittest.cc b/chrome/browser/sync/engine/auth_watcher_unittest.cc new file mode 100644 index 0000000..ca5192181 --- /dev/null +++ b/chrome/browser/sync/engine/auth_watcher_unittest.cc @@ -0,0 +1,214 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE entry. + +#include "base/scoped_ptr.h" +#include "base/scoped_temp_dir.h" +#include "base/test_file_util.h" +#include "base/waitable_event.h" +#include "chrome/browser/sync/engine/all_status.h" +#include "chrome/browser/sync/engine/auth_watcher.h" +#include "chrome/browser/sync/engine/net/gaia_authenticator.h" +#include "chrome/browser/sync/engine/net/http_return.h" +#include "chrome/browser/sync/notifier/listener/talk_mediator_impl.h" +#include "chrome/browser/sync/util/user_settings.h" +#include "chrome/browser/sync/util/event_sys-inl.h" +#include "chrome/test/sync/engine/mock_server_connection.h" +#include "chrome/test/sync/engine/test_directory_setter_upper.h" +#include "testing/gtest/include/gtest/gtest.h" + +static const wchar_t* kUserSettingsDB = L"Settings.sqlite3"; +static const char* kTestUserAgent = "useragent"; +static const char* kTestServiceId = "serviceid"; +static const char* kTestGaiaURL = "http://gaia_url"; +static const char* kUserDisplayName = "Mr. Auth Watcher"; +static const char* kUserDisplayEmail = "authwatcherdisplay@gmail.com"; +static const char* kTestEmail = "authwatchertest@gmail.com"; +static const char* kWrongPassword = "wrongpassword"; +static const char* kCorrectPassword = "correctpassword"; +static const char* kValidSID = "validSID"; +static const char* kValidLSID = "validLSID"; +static const char* kInvalidAuthToken = "invalidAuthToken"; +static const char* kValidAuthToken = "validAuthToken"; + +namespace { + +class GaiaAuthMock : public browser_sync::GaiaAuthenticator { + public: + GaiaAuthMock() : browser_sync::GaiaAuthenticator( + kTestUserAgent, kTestServiceId, kTestGaiaURL), + use_bad_auth_token_(false) {} + virtual ~GaiaAuthMock() {} + + void SendBadAuthTokenForNextRequest() { use_bad_auth_token_ = true; } + + protected: + bool PerformGaiaRequest(const AuthParams& params, AuthResults* results) { + if (params.password == kWrongPassword) { + results->auth_error = browser_sync::BadAuthentication; + return false; + } + if (params.password == kCorrectPassword) { + results->sid = kValidSID; + results->lsid = kValidLSID; + results->auth_token = kValidAuthToken; + } + if (use_bad_auth_token_) { + results->auth_token = kInvalidAuthToken; + use_bad_auth_token_ = false; + } + return true; + } + + bool LookupEmail(AuthResults* results) { + results->signin = GMAIL_SIGNIN; + return true; + } + + private: + // Whether we should send an invalid auth token on the next request. + bool use_bad_auth_token_; +}; + +} // namespace + +namespace browser_sync { + +class AuthWatcherTest : public testing::Test { + public: + AuthWatcherTest() : consumer_ready(false, false), + event_produced(false, false), + metadb_(kUserDisplayEmail) {} + virtual void SetUp() { + metadb_.SetUp(); + connection_.reset(new MockConnectionManager(metadb_.manager(), + metadb_.name())); + // Mock out data that would normally be sent back from a server. + connection()->SetAuthenticationResponseInfo(kValidAuthToken, + kUserDisplayName, kUserDisplayEmail, "ID"); + allstatus_.reset(new AllStatus()); + user_settings_.reset(new UserSettings()); + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + PathString user_settings_path = temp_dir_.path().value() + kUserSettingsDB; + user_settings_->Init(user_settings_path); + gaia_auth_ = new GaiaAuthMock(); + talk_mediator_.reset(new TalkMediatorImpl()); + auth_watcher_ = new AuthWatcher(metadb_.manager(), connection_.get(), + allstatus_.get(), kTestUserAgent, kTestServiceId, kTestGaiaURL, + user_settings_.get(), gaia_auth_, talk_mediator_.get()); + authwatcher_hookup_.reset(NewEventListenerHookup(auth_watcher_->channel(), + this, &AuthWatcherTest::HandleAuthWatcherEvent)); + } + + virtual void TearDown() { + metadb_.TearDown(); + auth_watcher_->Shutdown(); + EXPECT_FALSE(auth_watcher()->message_loop()); + } + + void HandleAuthWatcherEvent(const AuthWatcherEvent& event) { + if (event.what_happened == AuthWatcherEvent::AUTHWATCHER_DESTROYED) + return; + consumer_ready.Wait(); // Block progress until the test is ready. + + last_event_reason_ = event.what_happened; + if (event.what_happened == AuthWatcherEvent::AUTH_SUCCEEDED) + user_email_ = event.user_email; + + event_produced.Signal(); + } + + AuthWatcherEvent::WhatHappened ConsumeNextEvent() { + consumer_ready.Signal(); + event_produced.Wait(); + return last_event_reason_; + } + + AuthWatcher* auth_watcher() { return auth_watcher_.get(); } + MockConnectionManager* connection() { return connection_.get(); } + GaiaAuthMock* gaia_auth() { return gaia_auth_; } + const std::string& user_email() { return user_email_; } + + private: + // Responsible for creating / deleting a temp dir containing user settings DB. + ScopedTempDir temp_dir_; + + // The event listener hookup registered for HandleAuthWatcherEvent. + scoped_ptr<EventListenerHookup> authwatcher_hookup_; + + // The sync engine pieces necessary to run an AuthWatcher. + TriggeredOpenTestDirectorySetterUpper metadb_; + scoped_ptr<MockConnectionManager> connection_; + scoped_ptr<AllStatus> allstatus_; + scoped_ptr<UserSettings> user_settings_; + GaiaAuthMock* gaia_auth_; // Owned by auth_watcher_. + scoped_ptr<TalkMediator> talk_mediator_; + scoped_refptr<AuthWatcher> auth_watcher_; + + // This is used to block the AuthWatcherThread when it raises events until we + // are ready to read the event. It is not a manual-reset event, so it goes + // straight back to non-signaled after one thread (the main thread) is + // signaled (or "consumes" the signaled state). + base::WaitableEvent consumer_ready; + + // This is signaled by the AuthWatcherThread after it sets last_event_reason_ + // and possibly user_email_ for us to read. + base::WaitableEvent event_produced; + + // The 'WhatHappened' value from the last AuthWatcherEvent we handled. + AuthWatcherEvent::WhatHappened last_event_reason_; + + // Set when we receive an AUTH_SUCCEEDED event. + std::string user_email_; + + DISALLOW_COPY_AND_ASSIGN(AuthWatcherTest); +}; + +TEST_F(AuthWatcherTest, Construction) { + EXPECT_TRUE(auth_watcher()->message_loop()); + EXPECT_EQ("SyncEngine_AuthWatcherThread", + auth_watcher()->message_loop()->thread_name()); + EXPECT_TRUE(auth_watcher()->auth_backend_thread_.IsRunning()); + EXPECT_EQ(AuthWatcher::NOT_AUTHENTICATED, auth_watcher()->status()); +} + +TEST_F(AuthWatcherTest, AuthenticateGaiaAuthFailure) { + auth_watcher()->Authenticate(kTestEmail, kWrongPassword, + std::string(), // captcha_token + std::string(), // captcha_value + false); // persist_creds_to_disk + EXPECT_EQ(AuthWatcherEvent::AUTHENTICATION_ATTEMPT_START, ConsumeNextEvent()); + EXPECT_EQ(AuthWatcherEvent::GAIA_AUTH_FAILED, ConsumeNextEvent()); +} + +TEST_F(AuthWatcherTest, AuthenticateBadAuthToken) { + gaia_auth()->SendBadAuthTokenForNextRequest(); + auth_watcher()->Authenticate(kTestEmail, kCorrectPassword, std::string(), + std::string(), false); + EXPECT_EQ(AuthWatcherEvent::AUTHENTICATION_ATTEMPT_START, ConsumeNextEvent()); + EXPECT_EQ(AuthWatcherEvent::SERVICE_AUTH_FAILED, ConsumeNextEvent()); +} + +TEST_F(AuthWatcherTest, AuthenticateSuccess) { + auth_watcher()->Authenticate(kTestEmail, kCorrectPassword, std::string(), + std::string(), false); + EXPECT_EQ(AuthWatcherEvent::AUTHENTICATION_ATTEMPT_START, ConsumeNextEvent()); + EXPECT_EQ(AuthWatcherEvent::AUTH_SUCCEEDED, ConsumeNextEvent()); + + // The server responds with a different email than what we used in the call + // to Authenticate, and the AuthWatcher should have told us about. + EXPECT_EQ(kUserDisplayEmail, user_email()); +} + +TEST_F(AuthWatcherTest, AuthenticateWithTokenBadAuthToken) { + auth_watcher()->AuthenticateWithToken(kTestEmail, kInvalidAuthToken); + EXPECT_EQ(AuthWatcherEvent::SERVICE_AUTH_FAILED, ConsumeNextEvent()); +} + +TEST_F(AuthWatcherTest, AuthenticateWithTokenSuccess) { + auth_watcher()->AuthenticateWithToken(kTestEmail, kValidAuthToken); + EXPECT_EQ(AuthWatcherEvent::AUTH_SUCCEEDED, ConsumeNextEvent()); + EXPECT_EQ(kUserDisplayEmail, user_email()); +} + +} // namespace browser_sync diff --git a/chrome/browser/sync/engine/net/gaia_authenticator.h b/chrome/browser/sync/engine/net/gaia_authenticator.h index b490da8..264e499 100644 --- a/chrome/browser/sync/engine/net/gaia_authenticator.h +++ b/chrome/browser/sync/engine/net/gaia_authenticator.h @@ -105,7 +105,7 @@ class GaiaAuthenticator { // This object should only be invoked from the AuthWatcherThread message // loop, which is injected here. - void set_message_loop(MessageLoop* loop) { + void set_message_loop(const MessageLoop* loop) { message_loop_ = loop; } @@ -181,9 +181,10 @@ class GaiaAuthenticator { // The real Authenticate implementations. bool AuthenticateImpl(const AuthParams& params); bool AuthenticateImpl(const AuthParams& params, AuthResults* results); - bool PerformGaiaRequest(const AuthParams& params, AuthResults* results); // virtual for testing purposes. + virtual bool PerformGaiaRequest(const AuthParams& params, + AuthResults* results); virtual bool Post(const GURL& url, const std::string& post_body, unsigned long* response_code, std::string* response_body) { return false; @@ -191,7 +192,7 @@ class GaiaAuthenticator { // Caller should fill in results->LSID before calling. Result in // results->primary_email. - bool LookupEmail(AuthResults* results); + virtual bool LookupEmail(AuthResults* results); public: // Retrieve email. @@ -288,7 +289,7 @@ class GaiaAuthenticator { // The message loop all our methods are invoked on. Generally this is the // SyncEngine_AuthWatcherThread's message loop. - MessageLoop* message_loop_; + const MessageLoop* message_loop_; }; } // namespace browser_sync diff --git a/chrome/browser/sync/engine/net/server_connection_manager.h b/chrome/browser/sync/engine/net/server_connection_manager.h index 768c7e3..92b7b28 100644 --- a/chrome/browser/sync/engine/net/server_connection_manager.h +++ b/chrome/browser/sync/engine/net/server_connection_manager.h @@ -256,6 +256,10 @@ class ServerConnectionManager { auth_token_.assign(auth_token); } + const std::string& auth_token() const { + return auth_token_; + } + protected: PThreadMutex shutdown_event_mutex_; diff --git a/chrome/browser/sync/engine/syncapi.cc b/chrome/browser/sync/engine/syncapi.cc index 5a72fd4..35081c5 100644 --- a/chrome/browser/sync/engine/syncapi.cc +++ b/chrome/browser/sync/engine/syncapi.cc @@ -882,7 +882,7 @@ class SyncManager::SyncInternal { // the initial connectivity and causes the server connection event to be // broadcast, which signals the syncer thread to start syncing. // It has a heavy duty constructor requiring boilerplate so we heap allocate. - scoped_ptr<AuthWatcher> auth_watcher_; + scoped_refptr<AuthWatcher> auth_watcher_; // A store of change records produced by HandleChangeEvent during the // CALCULATE_CHANGES step, and to be processed, and forwarded to the @@ -1033,15 +1033,15 @@ bool SyncManager::SyncInternal::Init( BridgedGaiaAuthenticator* gaia_auth = new BridgedGaiaAuthenticator( gaia_source, service_id, gaia_url, auth_post_factory); - auth_watcher_.reset(new AuthWatcher(dir_manager(), - connection_manager(), - &allstatus_, - gaia_source, - service_id, - gaia_url, - user_settings_.get(), - gaia_auth, - talk_mediator())); + auth_watcher_ = new AuthWatcher(dir_manager(), + connection_manager(), + &allstatus_, + gaia_source, + service_id, + gaia_url, + user_settings_.get(), + gaia_auth, + talk_mediator()); talk_mediator()->WatchAuthWatcher(auth_watcher()); allstatus()->WatchAuthWatcher(auth_watcher()); @@ -1172,7 +1172,8 @@ void SyncManager::SyncInternal::Shutdown() { // it terminates gracefully before we shutdown and close other components. // Otherwise the attempt can complete after we've closed the directory, for // example, and cause initialization to continue, which is bad. - auth_watcher_.reset(); + auth_watcher_->Shutdown(); + auth_watcher_ = NULL; if (syncer_thread()) { if (!syncer_thread()->Stop(kThreadExitTimeoutMsec)) diff --git a/chrome/browser/sync/notifier/listener/talk_mediator_impl.h b/chrome/browser/sync/notifier/listener/talk_mediator_impl.h index 33cff04..84c45f2 100644 --- a/chrome/browser/sync/notifier/listener/talk_mediator_impl.h +++ b/chrome/browser/sync/notifier/listener/talk_mediator_impl.h @@ -15,6 +15,7 @@ #include "chrome/browser/sync/engine/auth_watcher.h" #include "chrome/browser/sync/notifier/listener/mediator_thread.h" #include "chrome/browser/sync/notifier/listener/talk_mediator.h" +#include "chrome/browser/sync/util/pthread_helpers.h" #include "talk/xmpp/xmppclientsettings.h" #include "testing/gtest/include/gtest/gtest_prod.h" // For FRIEND_TEST diff --git a/chrome/chrome.gyp b/chrome/chrome.gyp index d0cff67..06c769b 100755 --- a/chrome/chrome.gyp +++ b/chrome/chrome.gyp @@ -6633,6 +6633,7 @@ 'sources': [ 'browser/sync/engine/all_status_unittest.cc', 'browser/sync/engine/apply_updates_command_unittest.cc', + 'browser/sync/engine/auth_watcher_unittest.cc', 'browser/sync/engine/net/gaia_authenticator_unittest.cc', 'browser/sync/engine/syncer_proto_util_unittest.cc', 'browser/sync/engine/syncer_thread_unittest.cc', diff --git a/chrome/test/sync/engine/mock_server_connection.cc b/chrome/test/sync/engine/mock_server_connection.cc index c4595bd..46ec870 100644 --- a/chrome/test/sync/engine/mock_server_connection.cc +++ b/chrome/test/sync/engine/mock_server_connection.cc @@ -70,22 +70,23 @@ void MockConnectionManager::SetMidCommitObserver( bool MockConnectionManager::PostBufferToPath(const PostBufferParams* params, const string& path, const string& auth_token) { - - ScopedDirLookup directory(directory_manager_, directory_name_); - CHECK(directory.good()); - ClientToServerMessage post; CHECK(post.ParseFromString(params->buffer_in)); client_stuck_ = post.sync_problem_detected(); ClientToServerResponse response; response.Clear(); + + ScopedDirLookup directory(directory_manager_, directory_name_); + // For any non-AUTHENTICATE requests, a valid directory should be set up. // If the Directory's locked when we do this, it's a problem as in normal // use this function could take a while to return because it accesses the // network. As we can't test this we do the next best thing and hang here // when there's an issue. - { + if (post.message_contents() != ClientToServerMessage::AUTHENTICATE) { + CHECK(directory.good()); WriteTransaction wt(directory, syncable::UNITTEST, __FILE__, __LINE__); } + if (fail_next_postbuffer_) { fail_next_postbuffer_ = false; return false; @@ -101,12 +102,15 @@ bool MockConnectionManager::PostBufferToPath(const PostBufferParams* params, return true; } bool result = true; - EXPECT_TRUE(!store_birthday_sent_ || post.has_store_birthday()); + EXPECT_TRUE(!store_birthday_sent_ || post.has_store_birthday() || + post.message_contents() == ClientToServerMessage::AUTHENTICATE); store_birthday_sent_ = true; if (post.message_contents() == ClientToServerMessage::COMMIT) { ProcessCommit(&post, &response); } else if (post.message_contents() == ClientToServerMessage::GET_UPDATES) { ProcessGetUpdates(&post, &response); + } else if (post.message_contents() == ClientToServerMessage::AUTHENTICATE) { + ProcessAuthenticate(&post, &response, auth_token); } else { EXPECT_TRUE(false) << "Unknown/unsupported ClientToServerMessage"; return false; @@ -249,6 +253,33 @@ void MockConnectionManager::ProcessGetUpdates(ClientToServerMessage* csm, ResetUpdates(); } +void MockConnectionManager::ProcessAuthenticate(ClientToServerMessage* csm, + ClientToServerResponse* response, const std::string& auth_token){ + ASSERT_EQ(csm->message_contents(), ClientToServerMessage::AUTHENTICATE); + EXPECT_FALSE(auth_token.empty()); + + if (auth_token != valid_auth_token_) { + response->set_error_code(ClientToServerResponse::AUTH_INVALID); + return; + } + + response->set_error_code(ClientToServerResponse::SUCCESS); + response->mutable_authenticate()->CopyFrom(auth_response_); + auth_response_.Clear(); +} + +void MockConnectionManager::SetAuthenticationResponseInfo( + const std::string& valid_auth_token, + const std::string& user_display_name, + const std::string& user_display_email, + const std::string& user_obfuscated_id) { + valid_auth_token_ = valid_auth_token; + sync_pb::UserIdentification* user = auth_response_.mutable_user(); + user->set_display_name(user_display_name); + user->set_email(user_display_email); + user->set_obfuscated_id(user_obfuscated_id); +} + bool MockConnectionManager::ShouldConflictThisCommit() { bool conflict = false; if (conflict_all_commits_) { diff --git a/chrome/test/sync/engine/mock_server_connection.h b/chrome/test/sync/engine/mock_server_connection.h index 8c0cca7..1c19a5b 100644 --- a/chrome/test/sync/engine/mock_server_connection.h +++ b/chrome/test/sync/engine/mock_server_connection.h @@ -109,6 +109,12 @@ class MockConnectionManager : public browser_sync::ServerConnectionManager { void SetNewTimestamp(int64 ts); void SetNewestTimestamp(int64 ts); + // For AUTHENTICATE responses. + void SetAuthenticationResponseInfo(const std::string& valid_auth_token, + const std::string& user_display_name, + const std::string& user_display_email, + const std::string& user_obfuscated_id); + void FailNextPostBufferToPathCall() { fail_next_postbuffer_ = true; } // Simple inspectors. @@ -144,6 +150,9 @@ class MockConnectionManager : public browser_sync::ServerConnectionManager { // Functions to handle the various types of server request. void ProcessGetUpdates(sync_pb::ClientToServerMessage* csm, sync_pb::ClientToServerResponse* response); + void ProcessAuthenticate(sync_pb::ClientToServerMessage* csm, + sync_pb::ClientToServerResponse* response, + const std::string& auth_token); void ProcessCommit(sync_pb::ClientToServerMessage* csm, sync_pb::ClientToServerResponse* response_buffer); // Locate the most recent update message for purpose of alteration. @@ -190,6 +199,11 @@ class MockConnectionManager : public browser_sync::ServerConnectionManager { TestCallbackFunction mid_commit_callback_function_; MidCommitObserver* mid_commit_observer_; + // The AUTHENTICATE response we'll return for auth requests. + sync_pb::AuthenticateResponse auth_response_; + // What we use to determine if we should return SUCCESS or BAD_AUTH_TOKEN. + std::string valid_auth_token_; + scoped_ptr<sync_pb::ClientCommand> client_command_; // The next value to use for the position_in_parent property. diff --git a/chrome/test/sync/engine/test_directory_setter_upper.cc b/chrome/test/sync/engine/test_directory_setter_upper.cc index 0a7b32d..919ce2a 100644 --- a/chrome/test/sync/engine/test_directory_setter_upper.cc +++ b/chrome/test/sync/engine/test_directory_setter_upper.cc @@ -4,6 +4,7 @@ #include "chrome/test/sync/engine/test_directory_setter_upper.h" +#include "base/string_util.h" #include "chrome/browser/sync/syncable/directory_manager.h" #include "chrome/browser/sync/syncable/syncable.h" #include "chrome/browser/sync/util/compat_file.h" @@ -17,6 +18,8 @@ using syncable::ScopedDirLookup; namespace browser_sync { TestDirectorySetterUpper::TestDirectorySetterUpper() : name_(PSTR("Test")) {} +TestDirectorySetterUpper::TestDirectorySetterUpper(const PathString& name) + : name_(name) {} TestDirectorySetterUpper::~TestDirectorySetterUpper() {} @@ -82,4 +85,22 @@ void ManuallyOpenedTestDirectorySetterUpper::TearDown() { } } +TriggeredOpenTestDirectorySetterUpper::TriggeredOpenTestDirectorySetterUpper( + const std::string& name) : TestDirectorySetterUpper(UTF8ToWide(name)) { +} + +void TriggeredOpenTestDirectorySetterUpper::SetUp() { + Init(); +} + +void TriggeredOpenTestDirectorySetterUpper::TearDown() { + DirectoryManager::DirNames names; + manager()->GetOpenDirectories(&names); + if (!names.empty()) { + ASSERT_EQ(1, names.size()); + ASSERT_EQ(name(), names[0]); + TestDirectorySetterUpper::TearDown(); + } +} + } // namespace browser_sync diff --git a/chrome/test/sync/engine/test_directory_setter_upper.h b/chrome/test/sync/engine/test_directory_setter_upper.h index 0cfa3e6..3c8936a 100644 --- a/chrome/test/sync/engine/test_directory_setter_upper.h +++ b/chrome/test/sync/engine/test_directory_setter_upper.h @@ -59,6 +59,8 @@ class TestDirectorySetterUpper { const PathString& name() const { return name_; } protected: + // Subclasses may want to use a different directory name. + explicit TestDirectorySetterUpper(const PathString& name); virtual void Init(); private: @@ -82,6 +84,18 @@ class ManuallyOpenedTestDirectorySetterUpper : public TestDirectorySetterUpper { bool was_opened_; }; +// Use this flavor if you have a test that will trigger the opening event to +// happen automagically. It is careful on teardown to close only if needed. +class TriggeredOpenTestDirectorySetterUpper : public TestDirectorySetterUpper { + public: + // A triggered open is typically in response to a successful auth event just + // as in "real life". In this case, the name that will be used should be + // deterministically known at construction, and is passed in |name|. + TriggeredOpenTestDirectorySetterUpper(const std::string& name); + virtual void SetUp(); + virtual void TearDown(); +}; + } // namespace browser_sync #endif // CHROME_TEST_SYNC_ENGINE_TEST_DIRECTORY_SETTER_UPPER_H_ |