// 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 "remoting/host/signaling_connector.h" #include "base/bind.h" #include "base/callback.h" #include "base/strings/string_util.h" #include "google_apis/google_api_keys.h" #include "net/url_request/url_fetcher.h" #include "net/url_request/url_request_context_getter.h" #include "remoting/base/logging.h" #include "remoting/host/dns_blackhole_checker.h" namespace remoting { namespace { // The delay between reconnect attempts will increase exponentially up // to the maximum specified here. const int kMaxReconnectDelaySeconds = 10 * 60; // Time when we we try to update OAuth token before its expiration. const int kTokenUpdateTimeBeforeExpirySeconds = 60; } // namespace SignalingConnector::OAuthCredentials::OAuthCredentials( const std::string& login_value, const std::string& refresh_token_value, bool is_service_account) : login(login_value), refresh_token(refresh_token_value), is_service_account(is_service_account) { } SignalingConnector::SignalingConnector( XmppSignalStrategy* signal_strategy, scoped_refptr url_request_context_getter, scoped_ptr dns_blackhole_checker, const base::Closure& auth_failed_callback) : signal_strategy_(signal_strategy), url_request_context_getter_(url_request_context_getter), auth_failed_callback_(auth_failed_callback), dns_blackhole_checker_(dns_blackhole_checker.Pass()), reconnect_attempts_(0), refreshing_oauth_token_(false) { DCHECK(!auth_failed_callback_.is_null()); DCHECK(dns_blackhole_checker_.get()); net::NetworkChangeNotifier::AddConnectionTypeObserver(this); net::NetworkChangeNotifier::AddIPAddressObserver(this); signal_strategy_->AddListener(this); ScheduleTryReconnect(); } SignalingConnector::~SignalingConnector() { signal_strategy_->RemoveListener(this); net::NetworkChangeNotifier::RemoveConnectionTypeObserver(this); net::NetworkChangeNotifier::RemoveIPAddressObserver(this); } void SignalingConnector::EnableOAuth( scoped_ptr oauth_credentials) { oauth_credentials_ = oauth_credentials.Pass(); gaia_oauth_client_.reset( new gaia::GaiaOAuthClient(url_request_context_getter_.get())); } void SignalingConnector::OnSignalStrategyStateChange( SignalStrategy::State state) { DCHECK(CalledOnValidThread()); if (state == SignalStrategy::CONNECTED) { HOST_LOG << "Signaling connected."; reconnect_attempts_ = 0; } else if (state == SignalStrategy::DISCONNECTED) { HOST_LOG << "Signaling disconnected."; reconnect_attempts_++; // If authentication failed then we have an invalid OAuth token, // inform the upper layer about it. if (signal_strategy_->GetError() == SignalStrategy::AUTHENTICATION_FAILED) { auth_failed_callback_.Run(); } else { ScheduleTryReconnect(); } } } bool SignalingConnector::OnSignalStrategyIncomingStanza( const buzz::XmlElement* stanza) { return false; } void SignalingConnector::OnConnectionTypeChanged( net::NetworkChangeNotifier::ConnectionType type) { DCHECK(CalledOnValidThread()); if (type != net::NetworkChangeNotifier::CONNECTION_NONE && signal_strategy_->GetState() == SignalStrategy::DISCONNECTED) { HOST_LOG << "Network state changed to online."; ResetAndTryReconnect(); } } void SignalingConnector::OnIPAddressChanged() { DCHECK(CalledOnValidThread()); if (signal_strategy_->GetState() == SignalStrategy::DISCONNECTED) { HOST_LOG << "IP address has changed."; ResetAndTryReconnect(); } } void SignalingConnector::OnGetTokensResponse(const std::string& user_email, const std::string& access_token, int expires_seconds) { NOTREACHED(); } void SignalingConnector::OnRefreshTokenResponse( const std::string& access_token, int expires_seconds) { DCHECK(CalledOnValidThread()); DCHECK(oauth_credentials_.get()); HOST_LOG << "Received OAuth token."; oauth_access_token_ = access_token; auth_token_expiry_time_ = base::Time::Now() + base::TimeDelta::FromSeconds(expires_seconds) - base::TimeDelta::FromSeconds(kTokenUpdateTimeBeforeExpirySeconds); gaia_oauth_client_->GetUserEmail(access_token, 1, this); } void SignalingConnector::OnGetUserEmailResponse(const std::string& user_email) { DCHECK(CalledOnValidThread()); DCHECK(oauth_credentials_.get()); HOST_LOG << "Received user info."; if (user_email != oauth_credentials_->login) { LOG(ERROR) << "OAuth token and email address do not refer to " "the same account."; auth_failed_callback_.Run(); return; } signal_strategy_->SetAuthInfo(oauth_credentials_->login, oauth_access_token_, "oauth2"); refreshing_oauth_token_ = false; // Now that we've refreshed the token and verified that it's for the correct // user account, try to connect using the new token. DCHECK_EQ(signal_strategy_->GetState(), SignalStrategy::DISCONNECTED); signal_strategy_->Connect(); } void SignalingConnector::OnOAuthError() { DCHECK(CalledOnValidThread()); LOG(ERROR) << "OAuth: invalid credentials."; refreshing_oauth_token_ = false; reconnect_attempts_++; auth_failed_callback_.Run(); } void SignalingConnector::OnNetworkError(int response_code) { DCHECK(CalledOnValidThread()); LOG(ERROR) << "Network error when trying to update OAuth token: " << response_code; refreshing_oauth_token_ = false; reconnect_attempts_++; ScheduleTryReconnect(); } void SignalingConnector::ScheduleTryReconnect() { DCHECK(CalledOnValidThread()); if (timer_.IsRunning() || net::NetworkChangeNotifier::IsOffline()) return; int delay_s = std::min(1 << reconnect_attempts_, kMaxReconnectDelaySeconds); timer_.Start(FROM_HERE, base::TimeDelta::FromSeconds(delay_s), this, &SignalingConnector::TryReconnect); } void SignalingConnector::ResetAndTryReconnect() { DCHECK(CalledOnValidThread()); signal_strategy_->Disconnect(); reconnect_attempts_ = 0; timer_.Stop(); ScheduleTryReconnect(); } void SignalingConnector::TryReconnect() { DCHECK(CalledOnValidThread()); DCHECK(dns_blackhole_checker_.get()); // This will check if this machine is allowed to access the chromoting // host talkgadget. dns_blackhole_checker_->CheckForDnsBlackhole( base::Bind(&SignalingConnector::OnDnsBlackholeCheckerDone, base::Unretained(this))); } void SignalingConnector::OnDnsBlackholeCheckerDone(bool allow) { DCHECK(CalledOnValidThread()); // Unable to access the host talkgadget. Don't allow the connection, but // schedule a reconnect in case this is a transient problem rather than // an outright block. if (!allow) { reconnect_attempts_++; HOST_LOG << "Talkgadget check failed. Scheduling reconnect. Attempt " << reconnect_attempts_; ScheduleTryReconnect(); return; } if (signal_strategy_->GetState() == SignalStrategy::DISCONNECTED) { bool need_new_auth_token = oauth_credentials_.get() && (auth_token_expiry_time_.is_null() || base::Time::Now() >= auth_token_expiry_time_); if (need_new_auth_token) { RefreshOAuthToken(); } else { HOST_LOG << "Attempting to connect signaling."; signal_strategy_->Connect(); } } } void SignalingConnector::RefreshOAuthToken() { DCHECK(CalledOnValidThread()); HOST_LOG << "Refreshing OAuth token."; DCHECK(!refreshing_oauth_token_); // Service accounts use different API keys, as they use the client app flow. google_apis::OAuth2Client oauth2_client; if (oauth_credentials_->is_service_account) { oauth2_client = google_apis::CLIENT_REMOTING_HOST; } else { oauth2_client = google_apis::CLIENT_REMOTING; } gaia::OAuthClientInfo client_info = { google_apis::GetOAuth2ClientID(oauth2_client), google_apis::GetOAuth2ClientSecret(oauth2_client), // Redirect URL is only used when getting tokens from auth code. It // is not required when getting access tokens. "" }; refreshing_oauth_token_ = true; std::vector empty_scope_list; // (Use scope from refresh token.) gaia_oauth_client_->RefreshToken( client_info, oauth_credentials_->refresh_token, empty_scope_list, 1, this); } } // namespace remoting