Index: chrome/browser/extensions/updater/extension_downloader.cc |
diff --git a/chrome/browser/extensions/updater/extension_downloader.cc b/chrome/browser/extensions/updater/extension_downloader.cc |
index 361d41ead591e515e359bd8ec513739b2716fd7c..47c02bd7324e05c6320a02f94d746590d25aba69 100644 |
--- a/chrome/browser/extensions/updater/extension_downloader.cc |
+++ b/chrome/browser/extensions/updater/extension_downloader.cc |
@@ -31,9 +31,12 @@ |
#include "content/public/browser/browser_thread.h" |
#include "content/public/browser/notification_details.h" |
#include "content/public/browser/notification_service.h" |
+#include "google_apis/gaia/identity_provider.h" |
#include "net/base/backoff_entry.h" |
#include "net/base/load_flags.h" |
#include "net/base/net_errors.h" |
+#include "net/http/http_request_headers.h" |
+#include "net/http/http_status_code.h" |
#include "net/url_request/url_fetcher.h" |
#include "net/url_request/url_request_context_getter.h" |
#include "net/url_request/url_request_status.h" |
@@ -77,20 +80,30 @@ const net::BackoffEntry::Policy kDefaultBackoffPolicy = { |
const char kAuthUserQueryKey[] = "authuser"; |
const int kMaxAuthUserValue = 10; |
+const int kMaxOAuth2Attempts = 3; |
const char kNotFromWebstoreInstallSource[] = "notfromwebstore"; |
const char kDefaultInstallSource[] = ""; |
-#define RETRY_HISTOGRAM(name, retry_count, url) \ |
- if ((url).DomainIs("google.com")) { \ |
- UMA_HISTOGRAM_CUSTOM_COUNTS( \ |
- "Extensions." name "RetryCountGoogleUrl", retry_count, 1, \ |
- kMaxRetries, kMaxRetries+1); \ |
- } else { \ |
- UMA_HISTOGRAM_CUSTOM_COUNTS( \ |
- "Extensions." name "RetryCountOtherUrl", retry_count, 1, \ |
- kMaxRetries, kMaxRetries+1); \ |
- } |
+const char kGoogleDotCom[] = "google.com"; |
+const char kTokenServiceConsumerId[] = "extension_downloader"; |
+const char kWebstoreOAuth2Scope[] = |
+ "https://www.googleapis.com/auth/chromewebstore.readonly"; |
+ |
+#define RETRY_HISTOGRAM(name, retry_count, url) \ |
+ if ((url).DomainIs(kGoogleDotCom)) { \ |
+ UMA_HISTOGRAM_CUSTOM_COUNTS("Extensions." name "RetryCountGoogleUrl", \ |
+ retry_count, \ |
+ 1, \ |
+ kMaxRetries, \ |
+ kMaxRetries + 1); \ |
+ } else { \ |
+ UMA_HISTOGRAM_CUSTOM_COUNTS("Extensions." name "RetryCountOtherUrl", \ |
+ retry_count, \ |
+ 1, \ |
+ kMaxRetries, \ |
+ kMaxRetries + 1); \ |
+ } |
bool ShouldRetryRequest(const net::URLRequestStatus& status, |
int response_code) { |
@@ -100,31 +113,6 @@ bool ShouldRetryRequest(const net::URLRequestStatus& status, |
status.status() == net::URLRequestStatus::FAILED); |
} |
-bool ShouldRetryRequestWithCookies(const net::URLRequestStatus& status, |
- int response_code, |
- bool included_cookies) { |
- if (included_cookies) |
- return false; |
- |
- if (status.status() == net::URLRequestStatus::CANCELED) |
- return true; |
- |
- // Retry if a 401 or 403 is received. |
- return (status.status() == net::URLRequestStatus::SUCCESS && |
- (response_code == 401 || response_code == 403)); |
-} |
- |
-bool ShouldRetryRequestWithNextUser(const net::URLRequestStatus& status, |
- int response_code, |
- bool included_cookies) { |
- // Retry if a 403 is received in response to a request including cookies. |
- // Note that receiving a 401 in response to a request which included cookies |
- // should indicate that the |authuser| index was out of bounds for the profile |
- // and therefore Chrome should NOT retry with another index. |
- return (status.status() == net::URLRequestStatus::SUCCESS && |
- response_code == 403 && included_cookies); |
-} |
- |
// This parses and updates a URL query such that the value of the |authuser| |
// query parameter is incremented by 1. If parameter was not present in the URL, |
// it will be added with a value of 1. All other query keys and values are |
@@ -166,7 +154,8 @@ UpdateDetails::UpdateDetails(const std::string& id, const Version& version) |
UpdateDetails::~UpdateDetails() {} |
ExtensionDownloader::ExtensionFetch::ExtensionFetch() |
- : url(), is_protected(false) {} |
+ : url(), credentials(CREDENTIALS_NONE) { |
+} |
ExtensionDownloader::ExtensionFetch::ExtensionFetch( |
const std::string& id, |
@@ -174,24 +163,33 @@ ExtensionDownloader::ExtensionFetch::ExtensionFetch( |
const std::string& package_hash, |
const std::string& version, |
const std::set<int>& request_ids) |
- : id(id), url(url), package_hash(package_hash), version(version), |
- request_ids(request_ids), is_protected(false) {} |
+ : id(id), |
+ url(url), |
+ package_hash(package_hash), |
+ version(version), |
+ request_ids(request_ids), |
+ credentials(CREDENTIALS_NONE), |
+ oauth2_attempt_count(0) { |
+} |
ExtensionDownloader::ExtensionFetch::~ExtensionFetch() {} |
ExtensionDownloader::ExtensionDownloader( |
ExtensionDownloaderDelegate* delegate, |
- net::URLRequestContextGetter* request_context) |
- : delegate_(delegate), |
+ net::URLRequestContextGetter* request_context, |
+ IdentityProvider* webstore_identity_provider) |
+ : OAuth2TokenService::Consumer(kTokenServiceConsumerId), |
+ delegate_(delegate), |
request_context_(request_context), |
weak_ptr_factory_(this), |
manifests_queue_(&kDefaultBackoffPolicy, |
- base::Bind(&ExtensionDownloader::CreateManifestFetcher, |
- base::Unretained(this))), |
+ base::Bind(&ExtensionDownloader::CreateManifestFetcher, |
+ base::Unretained(this))), |
extensions_queue_(&kDefaultBackoffPolicy, |
- base::Bind(&ExtensionDownloader::CreateExtensionFetcher, |
- base::Unretained(this))), |
- extension_cache_(NULL) { |
+ base::Bind(&ExtensionDownloader::CreateExtensionFetcher, |
+ base::Unretained(this))), |
+ extension_cache_(NULL), |
+ identity_provider_(webstore_identity_provider) { |
DCHECK(delegate_); |
DCHECK(request_context_); |
} |
@@ -306,7 +304,7 @@ bool ExtensionDownloader::AddExtensionData(const std::string& id, |
return false; |
} |
- if (update_url.DomainIs("google.com")) { |
+ if (update_url.DomainIs(kGoogleDotCom)) { |
url_stats_.google_url_count++; |
} else if (update_url.is_empty()) { |
url_stats_.no_url_count++; |
@@ -580,7 +578,7 @@ void ExtensionDownloader::HandleManifestResults( |
// If the manifest response included a <daystart> element, we want to save |
// that value for any extensions which had sent a ping in the request. |
- if (fetch_data.base_url().DomainIs("google.com") && |
+ if (fetch_data.base_url().DomainIs(kGoogleDotCom) && |
results->daystart_elapsed_seconds >= 0) { |
Time day_start = |
Time::Now() - TimeDelta::FromSeconds(results->daystart_elapsed_seconds); |
@@ -722,16 +720,19 @@ void ExtensionDownloader::NotifyDelegateDownloadFinished( |
void ExtensionDownloader::CreateExtensionFetcher() { |
const ExtensionFetch* fetch = extensions_queue_.active_request(); |
+ extension_fetcher_.reset(net::URLFetcher::Create( |
+ kExtensionFetcherId, fetch->url, net::URLFetcher::GET, this)); |
+ extension_fetcher_->SetRequestContext(request_context_); |
+ extension_fetcher_->SetAutomaticallyRetryOnNetworkChanges(3); |
+ |
int load_flags = net::LOAD_DISABLE_CACHE; |
- if (!fetch->is_protected || !fetch->url.SchemeIs("https")) { |
+ bool is_secure = fetch->url.SchemeIsSecure(); |
+ if (fetch->credentials != ExtensionFetch::CREDENTIALS_COOKIES || !is_secure) { |
load_flags |= net::LOAD_DO_NOT_SEND_COOKIES | |
net::LOAD_DO_NOT_SAVE_COOKIES; |
} |
- extension_fetcher_.reset(net::URLFetcher::Create( |
- kExtensionFetcherId, fetch->url, net::URLFetcher::GET, this)); |
- extension_fetcher_->SetRequestContext(request_context_); |
extension_fetcher_->SetLoadFlags(load_flags); |
- extension_fetcher_->SetAutomaticallyRetryOnNetworkChanges(3); |
+ |
// Download CRX files to a temp file. The blacklist is small and will be |
// processed in memory, so it is fetched into a string. |
if (fetch->id != kBlacklistAppID) { |
@@ -739,8 +740,29 @@ void ExtensionDownloader::CreateExtensionFetcher() { |
BrowserThread::GetMessageLoopProxyForThread(BrowserThread::FILE)); |
} |
- VLOG(2) << "Starting fetch of " << fetch->url << " for " << fetch->id; |
+ if (fetch->credentials == ExtensionFetch::CREDENTIALS_OAUTH2_TOKEN && |
+ is_secure) { |
+ if (access_token_.empty()) { |
+ // We should try OAuth2, but we have no token cached. This |
+ // ExtensionFetcher will be started once the token fetch is complete, |
+ // in either OnTokenFetchSuccess or OnTokenFetchFailure. |
+ DCHECK(identity_provider_); |
+ OAuth2TokenService::ScopeSet webstore_scopes; |
+ webstore_scopes.insert(kWebstoreOAuth2Scope); |
+ access_token_request_ = |
+ identity_provider_->GetTokenService()->StartRequest( |
+ identity_provider_->GetActiveAccountId(), |
+ webstore_scopes, |
+ this); |
+ return; |
+ } |
+ extension_fetcher_->AddExtraRequestHeader( |
+ base::StringPrintf("%s: Bearer %s", |
+ net::HttpRequestHeaders::kAuthorization, |
+ access_token_.c_str())); |
+ } |
+ VLOG(2) << "Starting fetch of " << fetch->url << " for " << fetch->id; |
extension_fetcher_->Start(); |
} |
@@ -750,7 +772,8 @@ void ExtensionDownloader::OnCRXFetchComplete( |
const net::URLRequestStatus& status, |
int response_code, |
const base::TimeDelta& backoff_delay) { |
- const std::string& id = extensions_queue_.active_request()->id; |
+ ExtensionFetch& active_request = *extensions_queue_.active_request(); |
+ const std::string& id = active_request.id; |
if (status.status() == net::URLRequestStatus::SUCCESS && |
(response_code == 200 || url.SchemeIsFile())) { |
RETRY_HISTOGRAM("CrxFetchSuccess", |
@@ -769,22 +792,13 @@ void ExtensionDownloader::OnCRXFetchComplete( |
} else { |
NotifyDelegateDownloadFinished(fetch_data.Pass(), crx_path, true); |
} |
- } else if (ShouldRetryRequestWithCookies( |
- status, |
- response_code, |
- extensions_queue_.active_request()->is_protected)) { |
- // Requeue the fetch with |is_protected| set, enabling cookies. |
- extensions_queue_.active_request()->is_protected = true; |
- extensions_queue_.RetryRequest(backoff_delay); |
- } else if (ShouldRetryRequestWithNextUser( |
- status, |
- response_code, |
- extensions_queue_.active_request()->is_protected) && |
- IncrementAuthUserIndex(&extensions_queue_.active_request()->url)) { |
+ } else if (IterateFetchCredentialsAfterFailure( |
+ &active_request, |
+ status, |
+ response_code)) { |
extensions_queue_.RetryRequest(backoff_delay); |
} else { |
- const std::set<int>& request_ids = |
- extensions_queue_.active_request()->request_ids; |
+ const std::set<int>& request_ids = active_request.request_ids; |
const ExtensionDownloaderDelegate::PingResult& ping = ping_results_[id]; |
VLOG(1) << "Failed to fetch extension '" << url.possibly_invalid_spec() |
<< "' response code:" << response_code; |
@@ -830,4 +844,85 @@ void ExtensionDownloader::NotifyUpdateFound(const std::string& id, |
content::Details<UpdateDetails>(&updateInfo)); |
} |
+bool ExtensionDownloader::IterateFetchCredentialsAfterFailure( |
+ ExtensionFetch* fetch, |
+ const net::URLRequestStatus& status, |
+ int response_code) { |
+ bool auth_failure = status.status() == net::URLRequestStatus::CANCELED || |
+ (status.status() == net::URLRequestStatus::SUCCESS && |
+ (response_code == net::HTTP_UNAUTHORIZED || |
+ response_code == net::HTTP_FORBIDDEN)); |
+ if (!auth_failure) { |
+ return false; |
+ } |
+ // Here we decide what to do next if the server refused to authorize this |
+ // fetch. |
+ switch (fetch->credentials) { |
+ case ExtensionFetch::CREDENTIALS_NONE: |
+ if (fetch->url.DomainIs(kGoogleDotCom) && identity_provider_) { |
+ fetch->credentials = ExtensionFetch::CREDENTIALS_OAUTH2_TOKEN; |
+ } else { |
+ fetch->credentials = ExtensionFetch::CREDENTIALS_COOKIES; |
+ } |
+ return true; |
+ case ExtensionFetch::CREDENTIALS_OAUTH2_TOKEN: |
+ fetch->oauth2_attempt_count++; |
+ // OAuth2 may fail due to an expired access token, in which case we |
+ // should invalidate the token and try again. |
+ if (response_code == net::HTTP_UNAUTHORIZED && |
+ fetch->oauth2_attempt_count <= kMaxOAuth2Attempts) { |
+ DCHECK(identity_provider_ != NULL); |
+ OAuth2TokenService::ScopeSet webstore_scopes; |
+ webstore_scopes.insert(kWebstoreOAuth2Scope); |
+ identity_provider_->GetTokenService()->InvalidateToken( |
+ identity_provider_->GetActiveAccountId(), |
+ webstore_scopes, |
+ access_token_); |
+ access_token_.clear(); |
+ return true; |
+ } |
+ // Either there is no Gaia identity available, the active identity |
+ // doesn't have access to this resource, or the server keeps returning |
+ // 401s and we've retried too many times. Fall back on cookies. |
+ if (access_token_.empty() || |
+ response_code == net::HTTP_FORBIDDEN || |
+ fetch->oauth2_attempt_count > kMaxOAuth2Attempts) { |
+ fetch->credentials = ExtensionFetch::CREDENTIALS_COOKIES; |
+ return true; |
+ } |
+ // Something else is wrong. Time to give up. |
+ return false; |
+ case ExtensionFetch::CREDENTIALS_COOKIES: |
+ if (response_code == net::HTTP_FORBIDDEN) { |
+ // Try the next session identity, up to some maximum. |
+ return IncrementAuthUserIndex(&fetch->url); |
+ } |
+ return false; |
+ default: |
+ NOTREACHED(); |
+ } |
+ NOTREACHED(); |
+ return false; |
+} |
+ |
+void ExtensionDownloader::OnGetTokenSuccess( |
+ const OAuth2TokenService::Request* request, |
+ const std::string& access_token, |
+ const base::Time& expiration_time) { |
+ access_token_ = access_token; |
+ extension_fetcher_->AddExtraRequestHeader( |
+ base::StringPrintf("%s: Bearer %s", |
+ net::HttpRequestHeaders::kAuthorization, |
+ access_token_.c_str())); |
+ extension_fetcher_->Start(); |
+} |
+ |
+void ExtensionDownloader::OnGetTokenFailure( |
+ const OAuth2TokenService::Request* request, |
+ const GoogleServiceAuthError& error) { |
+ // If we fail to get an access token, kick the pending fetch and let it fall |
+ // back on cookies. |
+ extension_fetcher_->Start(); |
+} |
+ |
} // namespace extensions |