| // 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 "chrome/browser/google_apis/operations_base.h" |
| |
| #include "base/json/json_reader.h" |
| #include "base/metrics/histogram.h" |
| #include "base/string_number_conversions.h" |
| #include "base/stringprintf.h" |
| #include "base/values.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/common/chrome_version_info.h" |
| #include "chrome/common/net/url_util.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "google_apis/gaia/gaia_urls.h" |
| #include "google_apis/gaia/google_service_auth_error.h" |
| #include "google_apis/gaia/oauth2_access_token_fetcher.h" |
| #include "net/base/load_flags.h" |
| #include "net/http/http_util.h" |
| #include "net/url_request/url_fetcher.h" |
| #include "net/url_request/url_request_status.h" |
| #include "webkit/user_agent/user_agent_util.h" |
| |
| using content::BrowserThread; |
| using net::URLFetcher; |
| |
| namespace { |
| |
| // Used for success ratio histograms. 0 for failure, 1 for success, |
| // 2 for no connection (likely offline). |
| const int kSuccessRatioHistogramFailure = 0; |
| const int kSuccessRatioHistogramSuccess = 1; |
| const int kSuccessRatioHistogramNoConnection = 2; |
| const int kSuccessRatioHistogramMaxValue = 3; // The max value is exclusive. |
| |
| // Template for optional OAuth2 authorization HTTP header. |
| const char kAuthorizationHeaderFormat[] = "Authorization: Bearer %s"; |
| // Template for GData API version HTTP header. |
| const char kGDataVersionHeader[] = "GData-Version: 3.0"; |
| |
| // Maximum number of attempts for re-authentication per operation. |
| const int kMaxReAuthenticateAttemptsPerOperation = 1; |
| |
| // Parse JSON string to base::Value object. |
| void ParseJsonOnBlockingPool(const std::string& data, |
| scoped_ptr<base::Value>* value) { |
| DCHECK(!BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| |
| int error_code = -1; |
| std::string error_message; |
| value->reset(base::JSONReader::ReadAndReturnError(data, |
| base::JSON_PARSE_RFC, |
| &error_code, |
| &error_message)); |
| |
| if (!value->get()) { |
| LOG(ERROR) << "Error while parsing entry response: " |
| << error_message |
| << ", code: " |
| << error_code |
| << ", data:\n" |
| << data; |
| } |
| } |
| |
| // Returns a user agent string used for communicating with the Drive backend, |
| // both WAPI and Drive API. The user agent looks like: |
| // |
| // chromedrive-<VERSION> chrome-cc/none (<OS_CPU_INFO>) |
| // chromedrive-24.0.1274.0 chrome-cc/none (CrOS x86_64 0.4.0) |
| // |
| // TODO(satorux): Move this function to somewhere else: crbug.com/151605 |
| std::string GetDriveUserAgent() { |
| const char kDriveClientName[] = "chromedrive"; |
| |
| chrome::VersionInfo version_info; |
| const std::string version = (version_info.is_valid() ? |
| version_info.Version() : |
| std::string("unknown")); |
| |
| // This part is <client_name>/<version>. |
| const char kLibraryInfo[] = "chrome-cc/none"; |
| |
| const std::string os_cpu_info = webkit_glue::BuildOSCpuInfo(); |
| |
| return base::StringPrintf("%s-%s %s (%s)", |
| kDriveClientName, |
| version.c_str(), |
| kLibraryInfo, |
| os_cpu_info.c_str()); |
| } |
| |
| } // namespace |
| |
| namespace gdata { |
| |
| //================================ AuthOperation =============================== |
| |
| AuthOperation::AuthOperation(OperationRegistry* registry, |
| const AuthStatusCallback& callback, |
| const std::vector<std::string>& scopes, |
| const std::string& refresh_token) |
| : OperationRegistry::Operation(registry), |
| refresh_token_(refresh_token), |
| callback_(callback), |
| scopes_(scopes) { |
| } |
| |
| AuthOperation::~AuthOperation() {} |
| |
| void AuthOperation::Start() { |
| DCHECK(!refresh_token_.empty()); |
| oauth2_access_token_fetcher_.reset(new OAuth2AccessTokenFetcher( |
| this, g_browser_process->system_request_context())); |
| NotifyStart(); |
| oauth2_access_token_fetcher_->Start( |
| GaiaUrls::GetInstance()->oauth2_chrome_client_id(), |
| GaiaUrls::GetInstance()->oauth2_chrome_client_secret(), |
| refresh_token_, |
| scopes_); |
| } |
| |
| void AuthOperation::DoCancel() { |
| oauth2_access_token_fetcher_->CancelRequest(); |
| if (!callback_.is_null()) |
| callback_.Run(GDATA_CANCELLED, std::string()); |
| } |
| |
| // Callback for OAuth2AccessTokenFetcher on success. |access_token| is the token |
| // used to start fetching user data. |
| void AuthOperation::OnGetTokenSuccess(const std::string& access_token, |
| const base::Time& expiration_time) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| |
| UMA_HISTOGRAM_ENUMERATION("GData.AuthSuccess", |
| kSuccessRatioHistogramSuccess, |
| kSuccessRatioHistogramMaxValue); |
| |
| callback_.Run(HTTP_SUCCESS, access_token); |
| NotifyFinish(OPERATION_COMPLETED); |
| } |
| |
| // Callback for OAuth2AccessTokenFetcher on failure. |
| void AuthOperation::OnGetTokenFailure(const GoogleServiceAuthError& error) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| |
| LOG(WARNING) << "AuthOperation: token request using refresh token failed" |
| << error.ToString(); |
| |
| // There are many ways to fail, but if the failure is due to connection, |
| // it's likely that the device is off-line. We treat the error differently |
| // so that the file manager works while off-line. |
| if (error.state() == GoogleServiceAuthError::CONNECTION_FAILED) { |
| UMA_HISTOGRAM_ENUMERATION("GData.AuthSuccess", |
| kSuccessRatioHistogramNoConnection, |
| kSuccessRatioHistogramMaxValue); |
| callback_.Run(GDATA_NO_CONNECTION, std::string()); |
| } else { |
| UMA_HISTOGRAM_ENUMERATION("GData.AuthSuccess", |
| kSuccessRatioHistogramFailure, |
| kSuccessRatioHistogramMaxValue); |
| callback_.Run(HTTP_UNAUTHORIZED, std::string()); |
| } |
| NotifyFinish(OPERATION_FAILED); |
| } |
| |
| //============================ UrlFetchOperationBase =========================== |
| |
| UrlFetchOperationBase::UrlFetchOperationBase(OperationRegistry* registry) |
| : OperationRegistry::Operation(registry), |
| re_authenticate_count_(0), |
| save_temp_file_(false), |
| started_(false) { |
| } |
| |
| UrlFetchOperationBase::UrlFetchOperationBase(OperationRegistry* registry, |
| OperationType type, |
| const FilePath& path) |
| : OperationRegistry::Operation(registry, type, path), |
| re_authenticate_count_(0), |
| save_temp_file_(false), |
| started_(false) { |
| } |
| |
| UrlFetchOperationBase::~UrlFetchOperationBase() {} |
| |
| void UrlFetchOperationBase::Start(const std::string& auth_token) { |
| DCHECK(!auth_token.empty()); |
| |
| GURL url = GetURL(); |
| DCHECK(!url.is_empty()); |
| DVLOG(1) << "URL: " << url.spec(); |
| |
| url_fetcher_.reset( |
| URLFetcher::Create(url, GetRequestType(), this)); |
| url_fetcher_->SetRequestContext(g_browser_process->system_request_context()); |
| // Always set flags to neither send nor save cookies. |
| url_fetcher_->SetLoadFlags( |
| net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SAVE_COOKIES | |
| net::LOAD_DISABLE_CACHE); |
| if (save_temp_file_) { |
| url_fetcher_->SaveResponseToTemporaryFile( |
| BrowserThread::GetMessageLoopProxyForThread(BrowserThread::FILE)); |
| } else if (!output_file_path_.empty()) { |
| url_fetcher_->SaveResponseToFileAtPath(output_file_path_, |
| BrowserThread::GetMessageLoopProxyForThread(BrowserThread::FILE)); |
| } |
| |
| // Add request headers. |
| // Note that SetExtraRequestHeaders clears the current headers and sets it |
| // to the passed-in headers, so calling it for each header will result in |
| // only the last header being set in request headers. |
| // |
| // TODO(satorux): The custom user-agent should be set only for Drive |
| // operations. crbug.com/151605 |
| url_fetcher_->AddExtraRequestHeader("User-Agent: " + GetDriveUserAgent()); |
| url_fetcher_->AddExtraRequestHeader(kGDataVersionHeader); |
| url_fetcher_->AddExtraRequestHeader( |
| base::StringPrintf(kAuthorizationHeaderFormat, auth_token.data())); |
| std::vector<std::string> headers = GetExtraRequestHeaders(); |
| for (size_t i = 0; i < headers.size(); ++i) { |
| url_fetcher_->AddExtraRequestHeader(headers[i]); |
| DVLOG(1) << "Extra header: " << headers[i]; |
| } |
| |
| // Set upload data if available. |
| std::string upload_content_type; |
| std::string upload_content; |
| if (GetContentData(&upload_content_type, &upload_content)) { |
| url_fetcher_->SetUploadData(upload_content_type, upload_content); |
| } |
| |
| // Register to operation registry. |
| NotifyStartToOperationRegistry(); |
| |
| url_fetcher_->Start(); |
| started_ = true; |
| } |
| |
| void UrlFetchOperationBase::SetReAuthenticateCallback( |
| const ReAuthenticateCallback& callback) { |
| DCHECK(re_authenticate_callback_.is_null()); |
| |
| re_authenticate_callback_ = callback; |
| } |
| |
| URLFetcher::RequestType UrlFetchOperationBase::GetRequestType() const { |
| return URLFetcher::GET; |
| } |
| |
| std::vector<std::string> UrlFetchOperationBase::GetExtraRequestHeaders() const { |
| return std::vector<std::string>(); |
| } |
| |
| bool UrlFetchOperationBase::GetContentData(std::string* upload_content_type, |
| std::string* upload_content) { |
| return false; |
| } |
| |
| void UrlFetchOperationBase::DoCancel() { |
| url_fetcher_.reset(NULL); |
| RunCallbackOnPrematureFailure(GDATA_CANCELLED); |
| } |
| |
| GDataErrorCode UrlFetchOperationBase::GetErrorCode( |
| const URLFetcher* source) const { |
| GDataErrorCode code = static_cast<GDataErrorCode>(source->GetResponseCode()); |
| if (code == HTTP_SUCCESS && !source->GetStatus().is_success()) { |
| // If the HTTP response code is SUCCESS yet the URL request failed, it is |
| // likely that the failure is due to loss of connection. |
| code = GDATA_NO_CONNECTION; |
| } |
| return code; |
| } |
| |
| void UrlFetchOperationBase::OnProcessURLFetchResultsComplete(bool result) { |
| if (result) |
| NotifySuccessToOperationRegistry(); |
| else |
| NotifyFinish(OPERATION_FAILED); |
| } |
| |
| void UrlFetchOperationBase::OnURLFetchComplete(const URLFetcher* source) { |
| GDataErrorCode code = GetErrorCode(source); |
| DVLOG(1) << "Response headers:\n" << GetResponseHeadersAsString(source); |
| |
| if (code == HTTP_UNAUTHORIZED) { |
| if (!re_authenticate_callback_.is_null() && |
| ++re_authenticate_count_ <= kMaxReAuthenticateAttemptsPerOperation) { |
| re_authenticate_callback_.Run(this); |
| return; |
| } |
| |
| OnAuthFailed(code); |
| return; |
| } |
| |
| // Overridden by each specialization |
| ProcessURLFetchResults(source); |
| } |
| |
| void UrlFetchOperationBase::NotifySuccessToOperationRegistry() { |
| NotifyFinish(OPERATION_COMPLETED); |
| } |
| |
| void UrlFetchOperationBase::NotifyStartToOperationRegistry() { |
| NotifyStart(); |
| } |
| |
| void UrlFetchOperationBase::OnAuthFailed(GDataErrorCode code) { |
| RunCallbackOnPrematureFailure(code); |
| |
| // Notify authentication failed. |
| NotifyAuthFailed(); |
| |
| // Check if this failed before we even started fetching. If so, register |
| // for start so we can properly unregister with finish. |
| if (!started_) |
| NotifyStart(); |
| |
| // Note: NotifyFinish() must be invoked at the end, after all other callbacks |
| // and notifications. Once NotifyFinish() is called, the current instance of |
| // gdata operation will be deleted from the OperationRegistry and become |
| // invalid. |
| NotifyFinish(OPERATION_FAILED); |
| } |
| |
| std::string UrlFetchOperationBase::GetResponseHeadersAsString( |
| const URLFetcher* url_fetcher) { |
| // net::HttpResponseHeaders::raw_headers(), as the name implies, stores |
| // all headers in their raw format, i.e each header is null-terminated. |
| // So logging raw_headers() only shows the first header, which is probably |
| // the status line. GetNormalizedHeaders, on the other hand, will show all |
| // the headers, one per line, which is probably what we want. |
| std::string headers; |
| // Check that response code indicates response headers are valid (i.e. not |
| // malformed) before we retrieve the headers. |
| if (url_fetcher->GetResponseCode() == URLFetcher::RESPONSE_CODE_INVALID) { |
| headers.assign("Response headers are malformed!!"); |
| } else { |
| url_fetcher->GetResponseHeaders()->GetNormalizedHeaders(&headers); |
| } |
| return headers; |
| } |
| |
| //============================ EntryActionOperation ============================ |
| |
| EntryActionOperation::EntryActionOperation(OperationRegistry* registry, |
| const EntryActionCallback& callback, |
| const GURL& document_url) |
| : UrlFetchOperationBase(registry), |
| callback_(callback), |
| document_url_(document_url) { |
| } |
| |
| EntryActionOperation::~EntryActionOperation() {} |
| |
| void EntryActionOperation::ProcessURLFetchResults(const URLFetcher* source) { |
| if (!callback_.is_null()) { |
| GDataErrorCode code = GetErrorCode(source); |
| callback_.Run(code, document_url_); |
| } |
| const bool success = true; |
| OnProcessURLFetchResultsComplete(success); |
| } |
| |
| void EntryActionOperation::RunCallbackOnPrematureFailure(GDataErrorCode code) { |
| if (!callback_.is_null()) |
| callback_.Run(code, document_url_); |
| } |
| |
| //============================== GetDataOperation ============================== |
| |
| GetDataOperation::GetDataOperation(OperationRegistry* registry, |
| const GetDataCallback& callback) |
| : UrlFetchOperationBase(registry), |
| callback_(callback), |
| ALLOW_THIS_IN_INITIALIZER_LIST(weak_ptr_factory_(this)) { |
| } |
| |
| GetDataOperation::~GetDataOperation() {} |
| |
| void GetDataOperation::ProcessURLFetchResults(const URLFetcher* source) { |
| std::string data; |
| source->GetResponseAsString(&data); |
| scoped_ptr<base::Value> root_value; |
| GDataErrorCode fetch_error_code = GetErrorCode(source); |
| |
| switch (fetch_error_code) { |
| case HTTP_SUCCESS: |
| case HTTP_CREATED: |
| ParseResponse(fetch_error_code, data); |
| break; |
| default: |
| RunCallback(fetch_error_code, scoped_ptr<base::Value>()); |
| const bool success = false; |
| OnProcessURLFetchResultsComplete(success); |
| break; |
| } |
| } |
| |
| void GetDataOperation::RunCallbackOnPrematureFailure( |
| GDataErrorCode fetch_error_code) { |
| if (!callback_.is_null()) { |
| scoped_ptr<base::Value> root_value; |
| callback_.Run(fetch_error_code, root_value.Pass()); |
| } |
| } |
| |
| void GetDataOperation::ParseResponse(GDataErrorCode fetch_error_code, |
| const std::string& data) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| |
| // Uses this hack to avoid deep-copy of json object because json might be so |
| // big. This pointer of scped_ptr is to ensure a deletion of the parsed json |
| // value object. |
| scoped_ptr<base::Value>* parsed_value = new scoped_ptr<base::Value>(); |
| |
| BrowserThread::PostBlockingPoolTaskAndReply( |
| FROM_HERE, |
| base::Bind(&ParseJsonOnBlockingPool, |
| data, |
| parsed_value), |
| base::Bind(&GetDataOperation::OnDataParsed, |
| weak_ptr_factory_.GetWeakPtr(), |
| fetch_error_code, |
| base::Owned(parsed_value))); |
| } |
| |
| void GetDataOperation::OnDataParsed( |
| gdata::GDataErrorCode fetch_error_code, |
| scoped_ptr<base::Value>* value) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| |
| bool success = true; |
| if (!value->get()) { |
| fetch_error_code = gdata::GDATA_PARSE_ERROR; |
| success = false; |
| } |
| |
| // The ownership of the parsed json object is transfered to RunCallBack(), |
| // keeping the ownership of the |value| here. |
| RunCallback(fetch_error_code, value->Pass()); |
| |
| DCHECK(!value->get()); |
| |
| OnProcessURLFetchResultsComplete(success); |
| // |value| will be deleted after return because it is base::Owned()'d. |
| } |
| |
| void GetDataOperation::RunCallback(GDataErrorCode fetch_error_code, |
| scoped_ptr<base::Value> value) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| if (!callback_.is_null()) |
| callback_.Run(fetch_error_code, value.Pass()); |
| } |
| |
| } // namespace gdata |