Index: chrome/browser/sync/engine/net/gaia_authenticator.cc |
=================================================================== |
--- chrome/browser/sync/engine/net/gaia_authenticator.cc (revision 0) |
+++ chrome/browser/sync/engine/net/gaia_authenticator.cc (revision 0) |
@@ -0,0 +1,483 @@ |
+// 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 "chrome/browser/sync/engine/net/gaia_authenticator.h" |
+ |
+#include <string> |
+#include <utility> |
+#include <vector> |
+ |
+#include "base/basictypes.h" |
+#include "base/port.h" |
+#include "base/string_util.h" |
+#include "chrome/browser/sync/engine/all_status.h" |
+#include "chrome/browser/sync/engine/net/http_return.h" |
+#include "chrome/browser/sync/engine/net/url_translator.h" |
+#include "chrome/browser/sync/util/event_sys-inl.h" |
+#include "googleurl/src/gurl.h" |
+ |
+using std::pair; |
+using std::string; |
+using std::vector; |
+ |
+// TODO(timsteele): Integrate the following two functions to string_util.h or |
+// somewhere that makes them unit-testable. |
+bool SplitStringIntoKeyValues(const string& line, |
+ char key_value_delimiter, |
+ string* key, vector<string>* values) { |
+ key->clear(); |
+ values->clear(); |
+ |
+ // find the key string |
+ int end_key_pos = line.find_first_of(key_value_delimiter); |
+ if (end_key_pos == string::npos) { |
+ DLOG(INFO) << "cannot parse key from line: " << line; |
+ return false; // no key |
+ } |
+ key->assign(line, 0, end_key_pos); |
+ |
+ // find the values string |
+ string remains(line, end_key_pos, line.size() - end_key_pos); |
+ int begin_values_pos = remains.find_first_not_of(key_value_delimiter); |
+ if (begin_values_pos == string::npos) { |
+ DLOG(INFO) << "cannot parse value from line: " << line; |
+ return false; // no value |
+ } |
+ string values_string(remains, begin_values_pos, |
+ remains.size() - begin_values_pos); |
+ |
+ // construct the values vector |
+ values->push_back(values_string); |
+ return true; |
+} |
+ |
+bool SplitStringIntoKeyValuePairs(const string& line, |
+ char key_value_delimiter, |
+ char key_value_pair_delimiter, |
+ vector<pair<string, string> >* kv_pairs) { |
+ kv_pairs->clear(); |
+ |
+ vector<string> pairs; |
+ SplitString(line, key_value_pair_delimiter, &pairs); |
+ |
+ bool success = true; |
+ for (size_t i = 0; i < pairs.size(); ++i) { |
+ string key; |
+ vector<string> value; |
+ if (!SplitStringIntoKeyValues(pairs[i], |
+ key_value_delimiter, |
+ &key, &value)) { |
+ // Don't return here, to allow for keys without associated |
+ // values; just record that our split failed. |
+ success = false; |
+ } |
+ DCHECK_LE(value.size(), 1); |
+ kv_pairs->push_back(make_pair(key, value.empty()? "" : value[0])); |
+ } |
+ return success; |
+} |
+ |
+namespace browser_sync { |
+ |
+static const char kGaiaV1IssueAuthTokenPath[] = "/accounts/IssueAuthToken"; |
+ |
+static const char kGetUserInfoPath[] = "/accounts/GetUserInfo"; |
+ |
+// Sole constructor with initializers for all fields. |
+GaiaAuthenticator::GaiaAuthenticator(const string& user_agent, |
+ const string& service_id, |
+ const string& gaia_url) |
+ : user_agent_(user_agent), |
+ service_id_(service_id), |
+ gaia_url_(gaia_url), |
+ request_count_(0), |
+ early_auth_attempt_count_(0), |
+ delay_(0), |
+ next_allowed_auth_attempt_time_(0) { |
+ GaiaAuthEvent done = { GaiaAuthEvent::GAIA_AUTHENTICATOR_DESTROYED, None, |
+ this }; |
+ channel_ = new Channel(done); |
+} |
+ |
+GaiaAuthenticator::~GaiaAuthenticator() { |
+ delete channel_; |
+} |
+ |
+bool GaiaAuthenticator::LaunchAuthenticate(const AuthParams& params, |
+ bool synchronous) { |
+ if (synchronous) |
+ return AuthenticateImpl(params); |
+ AuthParams* copy = new AuthParams; |
+ *copy = params; |
+ pthread_t thread_id; |
+ int result = pthread_create(&thread_id, 0, &GaiaAuthenticator::ThreadMain, |
+ copy); |
+ if (result) |
+ return false; |
+ return true; |
+} |
+ |
+ |
+void* GaiaAuthenticator::ThreadMain(void* arg) { |
+ NameCurrentThreadForDebugging("SyncEngine_GaiaAuthenticatorThread"); |
+ AuthParams* const params = reinterpret_cast<AuthParams*>(arg); |
+ params->authenticator->AuthenticateImpl(*params); |
+ delete params; |
+ return 0; |
+} |
+ |
+// mutex_ must be entered before calling this function. |
+GaiaAuthenticator::AuthParams GaiaAuthenticator::MakeParams( |
+ const string& user_name, |
+ const string& password, |
+ SaveCredentials should_save_credentials, |
+ const string& captcha_token, |
+ const string& captcha_value, |
+ SignIn try_first) { |
+ AuthParams params; |
+ params.request_id = ++request_count_; |
+ params.email = user_name; |
+ params.password = password; |
+ params.should_save_credentials = should_save_credentials; |
+ params.captcha_token = captcha_token; |
+ params.captcha_value = captcha_value; |
+ params.authenticator = this; |
+ params.try_first = try_first; |
+ return params; |
+} |
+ |
+bool GaiaAuthenticator::Authenticate(const string& user_name, |
+ const string& password, |
+ SaveCredentials should_save_credentials, |
+ bool synchronous, |
+ const string& captcha_token, |
+ const string& captcha_value, |
+ SignIn try_first) { |
+ mutex_.Lock(); |
+ AuthParams const params = |
+ MakeParams(user_name, password, should_save_credentials, captcha_token, |
+ captcha_value, try_first); |
+ mutex_.Unlock(); |
+ return LaunchAuthenticate(params, synchronous); |
+} |
+ |
+bool GaiaAuthenticator::AuthenticateImpl(const AuthParams& params) { |
+ AuthResults results; |
+ const bool succeeded = AuthenticateImpl(params, &results); |
+ mutex_.Lock(); |
+ if (params.request_id == request_count_) { |
+ auth_results_ = results; |
+ GaiaAuthEvent event = { succeeded ? GaiaAuthEvent::GAIA_AUTH_SUCCEEDED |
+ : GaiaAuthEvent::GAIA_AUTH_FAILED, |
+ results.auth_error, this }; |
+ mutex_.Unlock(); |
+ channel_->NotifyListeners(event); |
+ } else { |
+ mutex_.Unlock(); |
+ } |
+ return succeeded; |
+} |
+ |
+// This method makes an HTTP request to the Gaia server, and calls other |
+// methods to help parse the response. If authentication succeeded, then |
+// Gaia-issued cookies are available in the respective variables; if |
+// authentication failed, then the exact error is available as an enum. If the |
+// client wishes to save the credentials, the last parameter must be true. |
+// If a subsequent request is made with fresh credentials, the saved credentials |
+// are wiped out; any subsequent request to the zero-parameter overload of this |
+// method preserves the saved credentials. |
+bool GaiaAuthenticator::AuthenticateImpl(const AuthParams& params, |
+ AuthResults* results) { |
+ results->credentials_saved = params.should_save_credentials; |
+ results->auth_error = ConnectionUnavailable; |
+ // Save credentials if so requested. |
+ if (params.should_save_credentials != DONT_SAVE_CREDENTIALS) { |
+ results->email = params.email.data(); |
+ results->password = params.password; |
+ } else { // Explicitly clear previously-saved credentials. |
+ results->email = ""; |
+ results->password = ""; |
+ } |
+ |
+ // The aim of this code is to start failing requests if due to a logic error |
+ // in the program we're hammering GAIA. |
+ time_t now = time(0); |
+ if (now > next_allowed_auth_attempt_time_) { |
+ next_allowed_auth_attempt_time_ = now + 1; |
+ // If we're more than 2 minutes past the allowed time we reset the early |
+ // attempt count. |
+ if (now - next_allowed_auth_attempt_time_ > 2 * 60) { |
+ delay_ = 1; |
+ early_auth_attempt_count_ = 0; |
+ } |
+ } else { |
+ ++early_auth_attempt_count_; |
+ // Allow 3 attempts, but then limit. |
+ if (early_auth_attempt_count_ > 3) { |
+ delay_ = AllStatus::GetRecommendedDelaySeconds(delay_); |
+ next_allowed_auth_attempt_time_ = now + delay_; |
+ return false; |
+ } |
+ } |
+ |
+ return PerformGaiaRequest(params, results); |
+} |
+ |
+bool GaiaAuthenticator::PerformGaiaRequest(const AuthParams& params, |
+ AuthResults* results) { |
+ GURL gaia_auth_url(gaia_url_); |
+ |
+ string post_body; |
+ post_body += "Email=" + CgiEscapeString(params.email); |
+ post_body += "&Passwd=" + CgiEscapeString(params.password); |
+ post_body += "&source=" + CgiEscapeString(user_agent_); |
+ post_body += "&service=" + service_id_; |
+ if (!params.captcha_token.empty() && !params.captcha_value.empty()) { |
+ post_body += "&logintoken=" + CgiEscapeString(params.captcha_token); |
+ post_body += "&logincaptcha=" + CgiEscapeString(params.captcha_value); |
+ } |
+ post_body += "&PersistentCookie=true"; |
+ // We set it to GOOGLE (and not HOSTED or HOSTED_OR_GOOGLE) because we only |
+ // allow consumer logins. |
+ post_body += "&accountType=GOOGLE"; |
+ |
+ string message_text; |
+ unsigned long server_response_code; |
+ if (!Post(gaia_auth_url, post_body, &server_response_code, |
+ &message_text)) { |
+ results->auth_error = ConnectionUnavailable; |
+ return false; |
+ } |
+ |
+ // Parse reply in two different ways, depending on if request failed or |
+ // succeeded. |
+ if (RC_FORBIDDEN == server_response_code) { |
+ ExtractAuthErrorFrom(message_text, results); |
+ return false; |
+ } else if (RC_REQUEST_OK == server_response_code) { |
+ ExtractTokensFrom(message_text, results); |
+ const bool old_gaia = |
+ results->auth_token.empty() && !results->lsid.empty(); |
+ const bool long_lived_token = |
+ params.should_save_credentials == PERSIST_TO_DISK; |
+ if ((old_gaia || long_lived_token) && |
+ !IssueAuthToken(results, service_id_, long_lived_token)) |
+ return false; |
+ |
+ return LookupEmail(results); |
+ } else { |
+ results->auth_error = Unknown; |
+ return false; |
+ } |
+} |
+ |
+bool GaiaAuthenticator::LookupEmail(AuthResults* results) { |
+ // Use the provided Gaia server, but change the path to what V1 expects. |
+ GURL url(gaia_url_); // Gaia server |
+ GURL::Replacements repl; |
+ // Needs to stay in scope till GURL is out of scope |
+ string path(kGetUserInfoPath); |
+ repl.SetPathStr(path); |
+ url = url.ReplaceComponents(repl); |
+ |
+ string post_body; |
+ post_body += "LSID="; |
+ post_body += CgiEscapeString(results->lsid); |
+ |
+ unsigned long server_response_code; |
+ string message_text; |
+ if (!Post(url, post_body, &server_response_code, &message_text)) { |
+ return false; |
+ } |
+ |
+ // Check if we received a valid AuthToken; if not, ignore it. |
+ if (RC_FORBIDDEN == server_response_code) { |
+ // Server says we're not authenticated. |
+ ExtractAuthErrorFrom(message_text, results); |
+ return false; |
+ } else if (RC_REQUEST_OK == server_response_code) { |
+ typedef vector<pair<string, string> > Tokens; |
+ Tokens tokens; |
+ SplitStringIntoKeyValuePairs(message_text, '=', '\n', &tokens); |
+ for (Tokens::iterator i = tokens.begin(); i != tokens.end(); ++i) { |
+ if ("accountType" == i->first) { |
+ // We never authenticate an email as a hosted account. |
+ DCHECK_EQ("GOOGLE", i->second); |
+ results->signin = GMAIL_SIGNIN; |
+ } else if ("email" == i->first) { |
+ results->primary_email = i->second; |
+ } |
+ } |
+ return true; |
+ } |
+ return false; |
+} |
+ |
+// We need to call this explicitly when we need to obtain a long-lived session |
+// token. |
+bool GaiaAuthenticator::IssueAuthToken(AuthResults* results, |
+ const string& service_id, |
+ bool long_lived) { |
+ // Use the provided Gaia server, but change the path to what V1 expects. |
+ GURL url(gaia_url_); // Gaia server |
+ GURL::Replacements repl; |
+ // Needs to stay in scope till GURL is out of scope |
+ string path(kGaiaV1IssueAuthTokenPath); |
+ repl.SetPathStr(path); |
+ url = url.ReplaceComponents(repl); |
+ |
+ string post_body; |
+ post_body += "LSID="; |
+ post_body += CgiEscapeString(results->lsid); |
+ post_body += "&service=" + service_id; |
+ if (long_lived) { |
+ post_body += "&Session=true"; |
+ } |
+ |
+ unsigned long server_response_code; |
+ string message_text; |
+ if (!Post(url, post_body, |
+ &server_response_code, &message_text)) { |
+ return false; |
+ } |
+ |
+ // Check if we received a valid AuthToken; if not, ignore it. |
+ if (RC_FORBIDDEN == server_response_code) { |
+ // Server says we're not authenticated. |
+ ExtractAuthErrorFrom(message_text, results); |
+ return false; |
+ } else if (RC_REQUEST_OK == server_response_code) { |
+ // Note that the format of message_text is different from what is returned |
+ // in the first request, or to the sole request that is made to Gaia V2. |
+ // Specifically, the entire string is the AuthToken, and looks like: |
+ // "<token>" rather than "AuthToken=<token>". Thus, we need not use |
+ // ExtractTokensFrom(...), but simply assign the token. |
+ int last_index = message_text.length() - 1; |
+ if ('\n' == message_text[last_index]) |
+ message_text.erase(last_index); |
+ results->auth_token = message_text; |
+ return true; |
+ } |
+ return false; |
+} |
+ |
+// TOOD(sync): This passing around of AuthResults makes it really unclear who |
+// actually owns the authentication state and when it is valid, but this is |
+// endemic to this implementation. We should fix this. |
+bool GaiaAuthenticator::AuthenticateService(const string& service_id, |
+ const string& sid, |
+ const string& lsid, |
+ string* other_service_cookie) { |
+ // Copy the AuthResults structure and overload the auth_token field |
+ // in the copy, local_results, to mean the auth_token for service_id. |
+ AuthResults local_results; |
+ local_results.sid = sid; |
+ local_results.lsid = lsid; |
+ |
+ if (!IssueAuthToken(&local_results, service_id, true)) { |
+ LOG(ERROR) << "[AUTH] Failed to obtain cookie for " << service_id; |
+ return false; |
+ } |
+ |
+ swap(*other_service_cookie, local_results.auth_token); |
+ return true; |
+} |
+ |
+// Helper method that extracts tokens from a successful reply, and saves them |
+// in the right fields. |
+void GaiaAuthenticator::ExtractTokensFrom(const string& response, |
+ AuthResults* results) { |
+ vector<pair<string, string> > tokens; |
+ SplitStringIntoKeyValuePairs(response, '=', '\n', &tokens); |
+ for (vector<pair<string, string> >::iterator i = tokens.begin(); |
+ i != tokens.end(); ++i) { |
+ if (i->first == "SID") { |
+ results->sid = i->second; |
+ } else if (i->first == "LSID") { |
+ results->lsid = i->second; |
+ } else if (i->first == "Auth") { |
+ results->auth_token = i->second; |
+ } |
+ } |
+} |
+ |
+// Helper method that extracts tokens from a failure response, and saves them |
+// in the right fields. |
+void GaiaAuthenticator::ExtractAuthErrorFrom(const string& response, |
+ AuthResults* results) { |
+ vector<pair<string, string> > tokens; |
+ SplitStringIntoKeyValuePairs(response, '=', '\n', &tokens); |
+ for (vector<pair<string, string> >::iterator i = tokens.begin(); |
+ i != tokens.end(); ++i) { |
+ if (i->first == "Error") { |
+ results->error_msg = i->second; |
+ } else if (i->first == "Url") { |
+ results->auth_error_url = i->second; |
+ } else if (i->first == "CaptchaToken") { |
+ results->captcha_token = i->second; |
+ } else if (i->first == "CaptchaUrl") { |
+ results->captcha_url = i->second; |
+ } |
+ } |
+ |
+ // Convert string error messages to enum values. Each case has two different |
+ // strings; the first one is the most current and the second one is |
+ // deprecated, but available. |
+ const string& error_msg = results->error_msg; |
+ if (error_msg == "BadAuthentication" || error_msg == "badauth") { |
+ results->auth_error = BadAuthentication; |
+ } else if (error_msg == "NotVerified" || error_msg == "nv") { |
+ results->auth_error = NotVerified; |
+ } else if (error_msg == "TermsNotAgreed" || error_msg == "tna") { |
+ results->auth_error = TermsNotAgreed; |
+ } else if (error_msg == "Unknown" || error_msg == "unknown") { |
+ results->auth_error = Unknown; |
+ } else if (error_msg == "AccountDeleted" || error_msg == "adel") { |
+ results->auth_error = AccountDeleted; |
+ } else if (error_msg == "AccountDisabled" || error_msg == "adis") { |
+ results->auth_error = AccountDisabled; |
+ } else if (error_msg == "CaptchaRequired" || error_msg == "cr") { |
+ results->auth_error = CaptchaRequired; |
+ } else if (error_msg == "ServiceUnavailable" || error_msg == "ire") { |
+ results->auth_error = ServiceUnavailable; |
+ } |
+} |
+ |
+// Reset all stored credentials, perhaps in preparation for letting a different |
+// user sign in. |
+void GaiaAuthenticator::ResetCredentials() { |
+ PThreadScopedLock<PThreadMutex> enter(&mutex_); |
+ AuthResults blank; |
+ auth_results_ = blank; |
+} |
+ |
+void GaiaAuthenticator::SetUsernamePassword(const string& username, |
+ const string& password) { |
+ PThreadScopedLock<PThreadMutex> enter(&mutex_); |
+ auth_results_.password = password; |
+ auth_results_.email = username; |
+} |
+ |
+void GaiaAuthenticator::SetUsername(const string& username) { |
+ PThreadScopedLock<PThreadMutex> enter(&mutex_); |
+ auth_results_.email = username; |
+} |
+ |
+void GaiaAuthenticator::SetAuthToken(const string& auth_token, |
+ SaveCredentials save) { |
+ PThreadScopedLock<PThreadMutex> enter(&mutex_); |
+ auth_results_.auth_token = auth_token; |
+ auth_results_.credentials_saved = save; |
+} |
+ |
+bool GaiaAuthenticator::Authenticate(const string& user_name, |
+ const string& password, |
+ SaveCredentials should_save_credentials, |
+ bool synchronous, SignIn try_first) { |
+ const string empty; |
+ return Authenticate(user_name, password, should_save_credentials, synchronous, |
+ empty, empty, try_first); |
+} |
+ |
+} // namespace browser_sync |
Property changes on: chrome\browser\sync\engine\net\gaia_authenticator.cc |
___________________________________________________________________ |
Added: svn:eol-style |
+ LF |