#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"
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) {
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
UpdateDetails::~UpdateDetails() {}
ExtensionDownloader::ExtensionFetch::ExtensionFetch()
- : url(), is_protected(false) {}
+ : url(), credentials(CREDENTIALS_NONE) {
+}
ExtensionDownloader::ExtensionFetch::ExtensionFetch(
const std::string& id,
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),
+ : 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))),
+ base::Bind(&ExtensionDownloader::CreateExtensionFetcher,
+ base::Unretained(this))),
extension_cache_(NULL) {
DCHECK(delegate_);
DCHECK(request_context_);
StartUpdateCheck(blacklist_fetch.Pass());
}
+void ExtensionDownloader::SetWebstoreIdentityProvider(
+ scoped_ptr<IdentityProvider> identity_provider) {
+ identity_provider_.swap(identity_provider);
+}
+
bool ExtensionDownloader::AddExtensionData(const std::string& id,
const Version& version,
Manifest::Type extension_type,
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++;
// 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);
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) {
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_.get());
+ 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();
}
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",
} 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;
const std::string& version) {
UpdateDetails updateInfo(id, Version(version));
content::NotificationService::current()->Notify(
- chrome::NOTIFICATION_EXTENSION_UPDATE_FOUND,
+ extensions::NOTIFICATION_EXTENSION_UPDATE_FOUND,
content::NotificationService::AllBrowserContextsAndSources(),
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_.get());
+ 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