// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "google_apis/gaia/merge_session_helper.h" #include #include "base/json/json_reader.h" #include "base/stl_util.h" #include "base/strings/string_util.h" #include "base/strings/stringprintf.h" #include "base/time/time.h" #include "base/values.h" #include "google_apis/gaia/gaia_auth_fetcher.h" #include "google_apis/gaia/gaia_constants.h" #include "google_apis/gaia/gaia_urls.h" #include "google_apis/gaia/oauth2_token_service.h" #include "net/base/load_flags.h" #include "net/http/http_status_code.h" #include "net/url_request/url_fetcher.h" #include "net/url_request/url_fetcher_delegate.h" MergeSessionHelper::ExternalCcResultFetcher::ExternalCcResultFetcher( MergeSessionHelper* helper) : helper_(helper) { DCHECK(helper_); } MergeSessionHelper::ExternalCcResultFetcher::~ExternalCcResultFetcher() { CleanupTransientState(); } std::string MergeSessionHelper::ExternalCcResultFetcher::GetExternalCcResult() { std::vector results; for (ResultMap::const_iterator it = results_.begin(); it != results_.end(); ++it) { results.push_back(it->first + ":" + it->second); } return JoinString(results, ","); } void MergeSessionHelper::ExternalCcResultFetcher::Start() { CleanupTransientState(); results_.clear(); gaia_auth_fetcher_.reset( new GaiaAuthFetcher(this, helper_->source_, helper_->request_context())); gaia_auth_fetcher_->StartGetCheckConnectionInfo(); // Some fetches may timeout. Start a timer to decide when the result fetcher // has waited long enough. // TODO(rogerta): I have no idea how long to wait before timing out. // Gaia folks say this should take no more than 2 second even in mobile. // This will need to be tweaked. timer_.Start(FROM_HERE, base::TimeDelta::FromSeconds(5), this, &MergeSessionHelper::ExternalCcResultFetcher::Timeout); } bool MergeSessionHelper::ExternalCcResultFetcher::IsRunning() { return gaia_auth_fetcher_ || fetchers_.size() > 0u; } void MergeSessionHelper::ExternalCcResultFetcher::TimeoutForTests() { Timeout(); } void MergeSessionHelper::ExternalCcResultFetcher::OnGetCheckConnectionInfoSuccess( const std::string& data) { scoped_ptr value(base::JSONReader::Read(data)); const base::ListValue* list; if (!value || !value->GetAsList(&list)) { CleanupTransientState(); FireGetCheckConnectionInfoCompleted(false); return; } // If there is nothing to check, terminate immediately. if (list->GetSize() == 0) { CleanupTransientState(); FireGetCheckConnectionInfoCompleted(true); return; } // Start a fetcher for each connection URL that needs to be checked. for (size_t i = 0; i < list->GetSize(); ++i) { const base::DictionaryValue* dict; if (list->GetDictionary(i, &dict)) { std::string token; std::string url; if (dict->GetString("carryBackToken", &token) && dict->GetString("url", &url)) { results_[token] = "null"; net::URLFetcher* fetcher = CreateFetcher(GURL(url)); fetchers_[fetcher->GetOriginalURL()] = std::make_pair(token, fetcher); fetcher->Start(); } } } } void MergeSessionHelper::ExternalCcResultFetcher::OnGetCheckConnectionInfoError( const GoogleServiceAuthError& error) { CleanupTransientState(); FireGetCheckConnectionInfoCompleted(false); } net::URLFetcher* MergeSessionHelper::ExternalCcResultFetcher::CreateFetcher( const GURL& url) { net::URLFetcher* fetcher = net::URLFetcher::Create( 0, url, net::URLFetcher::GET, this); fetcher->SetRequestContext(helper_->request_context()); fetcher->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SAVE_COOKIES); // Fetchers are sometimes cancelled because a network change was detected, // especially at startup and after sign-in on ChromeOS. fetcher->SetAutomaticallyRetryOnNetworkChanges(1); return fetcher; } void MergeSessionHelper::ExternalCcResultFetcher::OnURLFetchComplete( const net::URLFetcher* source) { const GURL& url = source->GetOriginalURL(); const net::URLRequestStatus& status = source->GetStatus(); int response_code = source->GetResponseCode(); if (status.is_success() && response_code == net::HTTP_OK && fetchers_.count(url) > 0) { std::string data; source->GetResponseAsString(&data); // Only up to the first 16 characters of the response are important to GAIA. // Truncate if needed to keep amount data sent back to GAIA down. if (data.size() > 16) data.resize(16); results_[fetchers_[url].first] = data; // Clean up tracking of this fetcher. The rest will be cleaned up after // the timer expires in CleanupTransientState(). DCHECK_EQ(source, fetchers_[url].second); fetchers_.erase(url); delete source; // If all expected responses have been received, cancel the timer and // report the result. if (fetchers_.empty()) { CleanupTransientState(); FireGetCheckConnectionInfoCompleted(true); } } } void MergeSessionHelper::ExternalCcResultFetcher::Timeout() { CleanupTransientState(); FireGetCheckConnectionInfoCompleted(false); } void MergeSessionHelper::ExternalCcResultFetcher::CleanupTransientState() { timer_.Stop(); gaia_auth_fetcher_.reset(); for (URLToTokenAndFetcher::const_iterator it = fetchers_.begin(); it != fetchers_.end(); ++it) { delete it->second.second; } fetchers_.clear(); } void MergeSessionHelper::ExternalCcResultFetcher:: FireGetCheckConnectionInfoCompleted(bool succeeded) { FOR_EACH_OBSERVER(Observer, helper_->observer_list_, GetCheckConnectionInfoCompleted(succeeded)); } MergeSessionHelper::MergeSessionHelper( OAuth2TokenService* token_service, const std::string& source, net::URLRequestContextGetter* request_context, Observer* observer) : token_service_(token_service), request_context_(request_context), result_fetcher_(this), source_(source) { if (observer) AddObserver(observer); } MergeSessionHelper::~MergeSessionHelper() { DCHECK(accounts_.empty()); } void MergeSessionHelper::LogIn(const std::string& account_id) { DCHECK(!account_id.empty()); VLOG(1) << "MergeSessionHelper::LogIn: " << account_id; accounts_.push_back(account_id); if (accounts_.size() == 1) StartFetching(); } void MergeSessionHelper::AddObserver(Observer* observer) { observer_list_.AddObserver(observer); } void MergeSessionHelper::RemoveObserver(Observer* observer) { observer_list_.RemoveObserver(observer); } void MergeSessionHelper::CancelAll() { VLOG(1) << "MergeSessionHelper::CancelAll"; gaia_auth_fetcher_.reset(); uber_token_fetcher_.reset(); accounts_.clear(); } void MergeSessionHelper::LogOut( const std::string& account_id, const std::vector& accounts) { DCHECK(!account_id.empty()); VLOG(1) << "MergeSessionHelper::LogOut: " << account_id << " accounts=" << accounts.size(); LogOutInternal(account_id, accounts); } void MergeSessionHelper::LogOutInternal( const std::string& account_id, const std::vector& accounts) { bool pending = !accounts_.empty(); if (pending) { for (std::deque::const_iterator it = accounts_.begin() + 1; it != accounts_.end(); it++) { if (!it->empty() && (std::find(accounts.begin(), accounts.end(), *it) == accounts.end() || *it == account_id)) { // We have a pending log in request for an account followed by // a signout. GoogleServiceAuthError error(GoogleServiceAuthError::REQUEST_CANCELED); SignalComplete(*it, error); } } // Remove every thing in the work list besides the one that is running. accounts_.resize(1); } // Signal a logout to be the next thing to do unless the pending // action is already a logout. if (!pending || !accounts_.front().empty()) accounts_.push_back(""); for (std::vector::const_iterator it = accounts.begin(); it != accounts.end(); it++) { if (*it != account_id) { DCHECK(!it->empty()); accounts_.push_back(*it); } } if (!pending) StartLogOutUrlFetch(); } void MergeSessionHelper::LogOutAllAccounts() { VLOG(1) << "MergeSessionHelper::LogOutAllAccounts"; LogOutInternal("", std::vector()); } void MergeSessionHelper::SignalComplete( const std::string& account_id, const GoogleServiceAuthError& error) { // Its possible for the observer to delete |this| object. Don't access // access any members after this calling the observer. This method should // be the last call in any other method. FOR_EACH_OBSERVER(Observer, observer_list_, MergeSessionCompleted(account_id, error)); } void MergeSessionHelper::StartFetchingExternalCcResult() { result_fetcher_.Start(); } bool MergeSessionHelper::StillFetchingExternalCcResult() { return result_fetcher_.IsRunning(); } void MergeSessionHelper::StartLogOutUrlFetch() { DCHECK(accounts_.front().empty()); VLOG(1) << "MergeSessionHelper::StartLogOutUrlFetch"; GURL logout_url(GaiaUrls::GetInstance()->service_logout_url().Resolve( base::StringPrintf("?source=%s", source_.c_str()))); net::URLFetcher* fetcher = net::URLFetcher::Create(logout_url, net::URLFetcher::GET, this); fetcher->SetRequestContext(request_context_); fetcher->Start(); } void MergeSessionHelper::OnUbertokenSuccess(const std::string& uber_token) { VLOG(1) << "MergeSessionHelper::OnUbertokenSuccess" << " account=" << accounts_.front(); gaia_auth_fetcher_.reset(new GaiaAuthFetcher(this, source_, request_context_)); // It's possible that not all external checks have completed. // GetExternalCcResult() returns results for those that have. gaia_auth_fetcher_->StartMergeSession(uber_token, result_fetcher_.GetExternalCcResult()); } void MergeSessionHelper::OnUbertokenFailure( const GoogleServiceAuthError& error) { VLOG(1) << "Failed to retrieve ubertoken" << " account=" << accounts_.front() << " error=" << error.ToString(); const std::string account_id = accounts_.front(); HandleNextAccount(); SignalComplete(account_id, error); } void MergeSessionHelper::OnMergeSessionSuccess(const std::string& data) { VLOG(1) << "MergeSession successful account=" << accounts_.front(); const std::string account_id = accounts_.front(); HandleNextAccount(); SignalComplete(account_id, GoogleServiceAuthError::AuthErrorNone()); } void MergeSessionHelper::OnMergeSessionFailure( const GoogleServiceAuthError& error) { VLOG(1) << "Failed MergeSession" << " account=" << accounts_.front() << " error=" << error.ToString(); const std::string account_id = accounts_.front(); HandleNextAccount(); SignalComplete(account_id, error); } void MergeSessionHelper::StartFetching() { VLOG(1) << "MergeSessionHelper::StartFetching account_id=" << accounts_.front(); uber_token_fetcher_.reset(new UbertokenFetcher(token_service_, this, source_, request_context_)); uber_token_fetcher_->StartFetchingToken(accounts_.front()); } void MergeSessionHelper::OnURLFetchComplete(const net::URLFetcher* source) { DCHECK(accounts_.front().empty()); VLOG(1) << "MergeSessionHelper::OnURLFetchComplete"; HandleNextAccount(); } void MergeSessionHelper::HandleNextAccount() { VLOG(1) << "MergeSessionHelper::HandleNextAccount"; accounts_.pop_front(); gaia_auth_fetcher_.reset(); if (accounts_.empty()) { VLOG(1) << "MergeSessionHelper::HandleNextAccount: no more"; uber_token_fetcher_.reset(); } else { if (accounts_.front().empty()) { StartLogOutUrlFetch(); } else { StartFetching(); } } }