// 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 file. #include #include "base/logging.h" #include "base/message_loop.h" #include "base/task.h" #include "base/thread.h" #include "chrome/browser/sync/notifier/base/ssl_adapter.h" #include "chrome/browser/sync/notifier/gaia_auth/gaiaauth.h" #include "talk/base/asynchttprequest.h" #include "talk/base/firewallsocketserver.h" #include "talk/base/httpclient.h" #include "talk/base/physicalsocketserver.h" #include "talk/base/socketadapters.h" #include "talk/base/socketpool.h" #include "talk/base/stringutils.h" #include "talk/base/urlencode.h" #include "talk/xmpp/saslcookiemechanism.h" #include "talk/xmpp/saslplainmechanism.h" namespace buzz { static const int kGaiaAuthTimeoutMs = 30 * 1000; // 30 sec // Warning, this is externed. GaiaServer g_gaia_server; /////////////////////////////////////////////////////////////////////////////// // GaiaAuth::WorkerTask /////////////////////////////////////////////////////////////////////////////// // GaiaAuth is NOT invoked during SASL authentication, but it is invoked even // before XMPP login begins. As a PreXmppAuth object, it is driven by // XmppClient before the XMPP socket is opened. The job of GaiaAuth is to goes // out using HTTPS POST to grab cookies from GAIA. // // It is used by XmppClient. It grabs a SaslAuthenticator which knows how to // play the cookie login. class GaiaAuth::WorkerTask { public: WorkerTask(const std::string& username, const talk_base::CryptString& pass, const std::string& service, const talk_base::ProxyInfo& proxy, talk_base::FirewallManager* firewall, const std::string& token, const CaptchaAnswer& captcha_answer, bool obtain_auth, const std::string& user_agent, const std::string& signature, const std::string& token_service) : username_(username), pass_(pass), service_(service), proxy_(proxy), firewall_(firewall), success_(false), error_(true), error_code_(0), proxy_auth_required_(false), certificate_expired_(false), auth_token_(token), captcha_answer_(captcha_answer), fresh_auth_token_(false), obtain_auth_(obtain_auth), agent_(user_agent), signature_(signature), token_service_(token_service) {} ~WorkerTask() {} void DoWork(MessageLoop* parent_message_loop, Task* on_work_done_task) { DCHECK(parent_message_loop); DCHECK(on_work_done_task); LOG(INFO) << "GaiaAuth Begin"; // Maybe we already have an auth token, then there is nothing to do. if (!auth_token_.empty()) { LOG(INFO) << "Reusing auth token:" << auth_token_; success_ = true; error_ = false; } else { // SocketFactory::CreateSSLAdapter() is called on the following // object somewhere in the depths of libjingle so we wrap it // with SSLAdapterSocketFactory to make sure it returns the // right SSLAdapter (see http://crbug.com/30721 ). notifier::SSLAdapterSocketFactory physical; talk_base::SocketServer* ss = &physical; if (firewall_) { ss = new talk_base::FirewallSocketServer(ss, firewall_); } talk_base::SslSocketFactory factory(ss, agent_); factory.SetProxy(proxy_); if (g_gaia_server.use_ssl()) { factory.SetIgnoreBadCert(true); factory.UseSSL(g_gaia_server.hostname().c_str()); } factory.SetLogging(talk_base::LS_VERBOSE, "GaiaAuth"); #if defined(OS_WIN) talk_base::ReuseSocketPool pool(&factory); #else // On non-Windows platforms our SSL socket wrapper // implementation does not support restartable SSL sockets, so // we turn it off and use a NewSocketPool instead. This means // that we create separate connections for each HTTP request but // this is okay since we do only two (in GaiaRequestSid() and // GaiaRequestAuthToken()). factory.SetUseRestartableSSLSockets(false); talk_base::NewSocketPool pool(&factory); #endif talk_base::HttpClient http(agent_, &pool); talk_base::HttpMonitor monitor(ss); monitor.Connect(&http); // If we do not already have a SID, let's get one using our password. if (sid_.empty() || (auth_.empty() && obtain_auth_)) { GaiaRequestSid(&http, username_, pass_, signature_, obtain_auth_ ? service_ : "", captcha_answer_, g_gaia_server); // TODO(akalin): handle timeouts better; this can cause jank, // e.g. when we're waiting on this and the user stops syncing, // which causes the GaiaAuth object to be destroyed, which // waits on this. (bug: http://crbug.com/31981 ) ss->Wait(kGaiaAuthTimeoutMs, true); error_code_ = monitor.error(); // Save off the error code. if (!monitor.done()) { LOG(INFO) << "GaiaAuth request timed out"; goto Cleanup; } else if (monitor.error()) { LOG(INFO) << "GaiaAuth request error: " << monitor.error(); if (monitor.error() == talk_base::HE_AUTH) { success_ = false; proxy_auth_required_ = true; } else if (monitor.error() == talk_base::HE_CERTIFICATE_EXPIRED) { success_ = false; certificate_expired_ = true; } goto Cleanup; } else { std::string captcha_token, captcha_url; switch (GaiaParseSidResponse(http, g_gaia_server, &captcha_token, &captcha_url, &sid_, &lsid_, &auth_)) { case GR_ERROR: goto Cleanup; case GR_UNAUTHORIZED: if (!captcha_url.empty()) { captcha_challenge_ = buzz::CaptchaChallenge(captcha_token, captcha_url); } // We had no "error" - we were just unauthorized. error_ = false; error_code_ = 0; goto Cleanup; case GR_SUCCESS: break; } } } // If all we need is a SID, then we are done now. if (service_.empty() || obtain_auth_) { success_ = true; error_ = false; error_code_ = 0; goto Cleanup; } monitor.reset(); GaiaRequestAuthToken(&http, sid_, lsid_, service_, g_gaia_server); ss->Wait(kGaiaAuthTimeoutMs, true); error_code_ = monitor.error(); // Save off the error code. if (!monitor.done()) { LOG(INFO) << "GaiaAuth request timed out"; } else if (monitor.error()) { LOG(INFO) << "GaiaAuth request error: " << monitor.error(); if (monitor.error() == talk_base::HE_AUTH) { success_ = false; proxy_auth_required_ = true; } else if (monitor.error() == talk_base::HE_CERTIFICATE_EXPIRED) { success_ = false; certificate_expired_ = true; } } else { if (GR_SUCCESS == GaiaParseAuthTokenResponse(http, &auth_token_)) { fresh_auth_token_ = true; success_ = true; error_ = false; error_code_ = 0; } } } // Done authenticating. Cleanup: LOG(INFO) << "GaiaAuth done"; parent_message_loop->PostTask(FROM_HERE, on_work_done_task); } bool Succeeded() const { return success_; } bool HadError() const { return error_; } int GetError() const { return error_code_; } bool ProxyAuthRequired() const { return proxy_auth_required_; } bool CertificateExpired() const { return certificate_expired_; } const buzz::CaptchaChallenge& GetCaptchaChallenge() { return captcha_challenge_; } bool fresh_auth_token() const { return fresh_auth_token_; } talk_base::CryptString GetPassword() const { return pass_; } std::string GetSID() const { return sid_; } std::string GetAuth() const { return auth_; } std::string GetToken() const { return auth_token_; } std::string GetUsername() const { return username_; } std::string GetTokenService() const { return token_service_; } private: std::string username_; talk_base::CryptString pass_; std::string service_; talk_base::ProxyInfo proxy_; talk_base::FirewallManager * firewall_; bool done_; bool success_; bool error_; int error_code_; bool proxy_auth_required_; bool certificate_expired_; std::string sid_; std::string lsid_; std::string auth_; std::string auth_token_; buzz::CaptchaChallenge captcha_challenge_; CaptchaAnswer captcha_answer_; bool fresh_auth_token_; bool obtain_auth_; std::string agent_; std::string signature_; std::string token_service_; DISALLOW_COPY_AND_ASSIGN(WorkerTask); }; } // namespace buzz // We outlive any runnable methods we create, so stub out // RunnableMethodTraits for GaiaAuth and WorkerTask. template <> struct RunnableMethodTraits { void RetainCallee(buzz::GaiaAuth* gaia_auth) {} void ReleaseCallee(buzz::GaiaAuth* gaia_auth) {} }; template <> struct RunnableMethodTraits { void RetainCallee(buzz::GaiaAuth::WorkerTask* gaia_auth) {} void ReleaseCallee(buzz::GaiaAuth::WorkerTask* gaia_auth) {} }; namespace buzz { /////////////////////////////////////////////////////////////////////////////// // GaiaAuth /////////////////////////////////////////////////////////////////////////////// GaiaAuth::GaiaAuth(const std::string &user_agent, const std::string &sig) : agent_(user_agent), signature_(sig), firewall_(0), worker_task_(NULL), on_work_done_task_(NULL), worker_thread_("GaiaAuth worker thread"), done_(false), current_message_loop_(MessageLoop::current()) { DCHECK(current_message_loop_); } GaiaAuth::~GaiaAuth() { DCHECK_EQ(MessageLoop::current(), current_message_loop_); // Wait until worker thread stops running. worker_thread_.Stop(); // Then, if the worker thread posted an OnAuthDone() task that is // still pending, cancel it. if (on_work_done_task_) { on_work_done_task_->Cancel(); } } void GaiaAuth::StartPreXmppAuth(const buzz::Jid& jid, const talk_base::SocketAddress& server, const talk_base::CryptString& pass, const std::string & auth_cookie) { InternalStartGaiaAuth(jid, server, pass, auth_cookie, "mail", false); } void GaiaAuth::StartTokenAuth(const buzz::Jid& jid, const talk_base::CryptString& pass, const std::string& service) { InternalStartGaiaAuth(jid, talk_base::SocketAddress(), pass, "", service, false); } void GaiaAuth::StartAuth(const buzz::Jid& jid, const talk_base::CryptString& pass, const std::string & service) { InternalStartGaiaAuth(jid, talk_base::SocketAddress(), pass, "", service, true); } void GaiaAuth::StartAuthFromSid(const buzz::Jid& jid, const std::string& sid, const std::string& service) { InternalStartGaiaAuth(jid, talk_base::SocketAddress(), talk_base::CryptString(), sid, service, false); } void GaiaAuth::InternalStartGaiaAuth(const buzz::Jid& jid, const talk_base::SocketAddress& server, const talk_base::CryptString& pass, const std::string& token, const std::string& service, bool obtain_auth) { DCHECK_EQ(MessageLoop::current(), current_message_loop_); worker_task_.reset( new WorkerTask(jid.Str(), pass, service, proxy_, firewall_, token, captcha_answer_, obtain_auth, agent_, signature_, token_service_)); on_work_done_task_ = NewRunnableMethod(this, &GaiaAuth::OnAuthDone); LOG(INFO) << "GaiaAuth begin (async)"; worker_thread_.Start(); worker_thread_.message_loop()->PostTask( FROM_HERE, NewRunnableMethod(worker_task_.get(), &WorkerTask::DoWork, current_message_loop_, on_work_done_task_)); } void GaiaAuth::OnAuthDone() { DCHECK_EQ(MessageLoop::current(), current_message_loop_); LOG(INFO) << "GaiaAuth done (async)"; worker_thread_.Stop(); on_work_done_task_ = NULL; done_ = true; if (worker_task_->fresh_auth_token()) { SignalFreshAuthCookie(worker_task_->GetToken()); } if (worker_task_->ProxyAuthRequired()) { SignalAuthenticationError(); } if (worker_task_->CertificateExpired()) { SignalCertificateExpired(); } SignalAuthDone(); } std::string GaiaAuth::ChooseBestSaslMechanism( const std::vector & mechanisms, bool encrypted) { if (!done_) return ""; std::vector::const_iterator it; // A token is the weakest auth - 15s, service-limited, so prefer it. it = std::find(mechanisms.begin(), mechanisms.end(), "X-GOOGLE-TOKEN"); if (it != mechanisms.end()) return "X-GOOGLE-TOKEN"; // A cookie is the next weakest - 14 days. it = std::find(mechanisms.begin(), mechanisms.end(), "X-GOOGLE-COOKIE"); if (it != mechanisms.end()) return "X-GOOGLE-COOKIE"; // Never pass @google.com passwords without encryption!! if (!encrypted && buzz::Jid(worker_task_->GetUsername()).domain() == "google.com") { return ""; } // As a last resort, use plain authentication. if (buzz::Jid(worker_task_->GetUsername()).domain() != "google.com") { it = std::find(mechanisms.begin(), mechanisms.end(), "PLAIN"); if (it != mechanisms.end()) return "PLAIN"; } // No good mechanism found. return ""; } buzz::SaslMechanism* GaiaAuth::CreateSaslMechanism( const std::string& mechanism) { if (!done_) { return NULL; } if (mechanism == "X-GOOGLE-TOKEN") { return new buzz::SaslCookieMechanism( mechanism, worker_task_->GetUsername(), worker_task_->GetToken(), worker_task_->GetTokenService()); } if (mechanism == "X-GOOGLE-COOKIE") { return new buzz::SaslCookieMechanism( "X-GOOGLE-COOKIE", worker_task_->GetUsername(), worker_task_->GetSID(), worker_task_->GetTokenService()); } if (mechanism == "PLAIN") { return new buzz::SaslPlainMechanism(buzz::Jid(worker_task_->GetUsername()), worker_task_->GetPassword()); } // Oh well - none of the above. return NULL; } std::string GaiaAuth::CreateAuthenticatedUrl( const std::string & continue_url, const std::string & service) { if (!done_ || worker_task_->GetToken().empty()) return ""; std::string url; // Note that http_prefix always ends with a "/". url += g_gaia_server.http_prefix() + "accounts/TokenAuth?auth=" // Do not URL encode - GAIA doesn't like that. + worker_task_->GetToken(); url += "&service=" + service; url += "&continue=" + UrlEncodeString(continue_url); url += "&source=" + signature_; return url; } std::string GaiaAuth::GetAuthCookie() { assert(IsAuthDone() && IsAuthorized()); if (!done_ || !worker_task_->Succeeded()) { return ""; } return worker_task_->GetToken(); } std::string GaiaAuth::GetAuth() { assert(IsAuthDone() && IsAuthorized()); if (!done_ || !worker_task_->Succeeded()) { return ""; } return worker_task_->GetAuth(); } std::string GaiaAuth::GetSID() { assert(IsAuthDone() && IsAuthorized()); if (!done_ || !worker_task_->Succeeded()) { return ""; } return worker_task_->GetSID(); } bool GaiaAuth::IsAuthDone() { return done_; } bool GaiaAuth::IsAuthorized() { return done_ && worker_task_ != NULL && worker_task_->Succeeded(); } bool GaiaAuth::HadError() { return done_ && worker_task_ != NULL && worker_task_->HadError(); } int GaiaAuth::GetError() { if (done_ && worker_task_ != NULL) { return worker_task_->GetError(); } return 0; } buzz::CaptchaChallenge GaiaAuth::GetCaptchaChallenge() { if (!done_ || worker_task_->Succeeded()) { return buzz::CaptchaChallenge(); } return worker_task_->GetCaptchaChallenge(); } } // namespace buzz