diff options
Diffstat (limited to 'chrome/browser/extensions/extension_updater.cc')
-rw-r--r-- | chrome/browser/extensions/extension_updater.cc | 560 |
1 files changed, 560 insertions, 0 deletions
diff --git a/chrome/browser/extensions/extension_updater.cc b/chrome/browser/extensions/extension_updater.cc new file mode 100644 index 0000000..c14281c --- /dev/null +++ b/chrome/browser/extensions/extension_updater.cc @@ -0,0 +1,560 @@ +// 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/extensions/extension_updater.h" + +#include "base/logging.h" +#include "base/file_util.h" +#include "base/file_version_info.h" +#include "base/string_util.h" +#include "base/thread.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/extensions/extensions_service.h" +#include "chrome/browser/profile.h" +#include "chrome/common/extensions/extension.h" +#include "chrome/common/extensions/extension_error_reporter.h" +#include "chrome/common/libxml_utils.h" +#include "googleurl/src/gurl.h" +#include "net/url_request/url_request_status.h" +#include "libxml/tree.h" + +const char* ExtensionUpdater::kExpectedGupdateProtocol = "2.0"; +const char* ExtensionUpdater::kExpectedGupdateXmlns = + "http://www.google.com/update2/response"; + +// For sanity checking on update frequency - enforced in release mode only. +static const int kMinUpdateFrequencySeconds = 60 * 60; // 1 hour +static const int kMaxUpdateFrequencySeconds = 60 * 60 * 24 * 7; // 7 days + +// A utility class to do file handling on the file I/O thread. +class ExtensionUpdaterFileHandler + : public base::RefCountedThreadSafe<ExtensionUpdaterFileHandler> { + public: + ExtensionUpdaterFileHandler(MessageLoop* updater_loop, + MessageLoop* file_io_loop) + : updater_loop_(updater_loop), file_io_loop_(file_io_loop) {} + + // Writes crx file data into a tempfile, and calls back the updater. + void WriteTempFile(const std::string& extension_id, const std::string& data, + scoped_refptr<ExtensionUpdater> updater) { + // Make sure we're running in the right thread. + DCHECK(MessageLoop::current() == file_io_loop_); + + FilePath path; + if (!file_util::CreateTemporaryFileName(&path)) { + LOG(WARNING) << "Failed to create temporary file path"; + return; + } + if (file_util::WriteFile(path, data.c_str(), data.length()) != + static_cast<int>(data.length())) { + // TODO(asargent) - It would be nice to back off updating alltogether if + // the disk is full. (http://crbug.com/12763). + LOG(ERROR) << "Failed to write temporary file"; + file_util::Delete(path, false); + return; + } + + // The ExtensionUpdater is now responsible for cleaning up the temp file + // from disk. + updater_loop_->PostTask(FROM_HERE, NewRunnableMethod( + updater.get(), &ExtensionUpdater::OnCRXFileWritten, extension_id, + path)); + } + + void DeleteFile(const FilePath& path) { + DCHECK(MessageLoop::current() == file_io_loop_); + if (!file_util::Delete(path, false)) { + LOG(WARNING) << "Failed to delete temp file " << path.value(); + } + } + + private: + // The MessageLoop we use to call back the ExtensionUpdater. + MessageLoop* updater_loop_; + + // The MessageLoop we should be operating on for file operations. + MessageLoop* file_io_loop_; +}; + + +ExtensionUpdater::ExtensionUpdater(ExtensionUpdateService* service, + int frequency_seconds, + MessageLoop* file_io_loop) + : service_(service), frequency_seconds_(frequency_seconds), + file_io_loop_(file_io_loop), + file_handler_(new ExtensionUpdaterFileHandler(MessageLoop::current(), + file_io_loop_)) { + Init(); +} + +void ExtensionUpdater::Init() { + // Unless we're in a unit test, expect that the file_io_loop_ is on the + // browser file thread. + if (g_browser_process->file_thread() != NULL) { + DCHECK(file_io_loop_ == g_browser_process->file_thread()->message_loop()); + } + + DCHECK_GE(frequency_seconds_, 5); + DCHECK(frequency_seconds_ <= kMaxUpdateFrequencySeconds); +#ifdef NDEBUG + // In Release mode we enforce that update checks don't happen too often. + frequency_seconds_ = std::max(frequency_seconds_, kMinUpdateFrequencySeconds); +#endif + frequency_seconds_ = std::min(frequency_seconds_, kMaxUpdateFrequencySeconds); +} + +ExtensionUpdater::~ExtensionUpdater() {} + +void ExtensionUpdater::Start() { + // TODO(asargent) Make sure update schedules work across browser restarts by + // reading/writing update frequency in profile/settings. *But*, make sure to + // wait a reasonable amount of time after browser startup to do the first + // check during a given run of the browser. Also remember to update the header + // comments when this is implemented. (http://crbug.com/12545). + timer_.Start(base::TimeDelta::FromSeconds(frequency_seconds_), this, + &ExtensionUpdater::TimerFired); +} + +void ExtensionUpdater::Stop() { + timer_.Stop(); + manifest_fetcher_.reset(); + extension_fetcher_.reset(); + manifests_pending_.clear(); + extensions_pending_.clear(); +} + +void ExtensionUpdater::OnURLFetchComplete( + const URLFetcher* source, const GURL& url, const URLRequestStatus& status, + int response_code, const ResponseCookies& cookies, + const std::string& data) { + + if (source == manifest_fetcher_.get()) { + OnManifestFetchComplete(url, status, response_code, data); + } else if (source == extension_fetcher_.get()) { + OnCRXFetchComplete(url, status, response_code, data); + } else { + NOTREACHED(); + } +} + +void ExtensionUpdater::OnManifestFetchComplete(const GURL& url, + const URLRequestStatus& status, + int response_code, + const std::string& data) { + // We want to try parsing the manifest, and if it indicates updates are + // available, we want to fire off requests to fetch those updates. + if (status.status() == URLRequestStatus::SUCCESS && response_code == 200) { + ScopedVector<ParseResult> parsed; + // TODO(asargent) - We should do the xml parsing in a sandboxed process. + // (http://crbug.com/12677). + if (Parse(data, &parsed.get())) { + std::vector<int> updates = DetermineUpdates(parsed.get()); + for (size_t i = 0; i < updates.size(); i++) { + ParseResult* update = parsed[updates[i]]; + FetchUpdatedExtension(update->extension_id, update->crx_url); + } + } + } else { + // TODO(asargent) Do exponential backoff here. (http://crbug.com/12546). + LOG(INFO) << "Failed to fetch manifst '" << url.possibly_invalid_spec() << + "' response code:" << response_code; + } + manifest_fetcher_.reset(); + + // If we have any pending manifest requests, fire off the next one. + if (!manifests_pending_.empty()) { + GURL url = manifests_pending_.front(); + manifests_pending_.pop_front(); + StartUpdateCheck(url); + } +} + +void ExtensionUpdater::OnCRXFetchComplete(const GURL& url, + const URLRequestStatus& status, + int response_code, + const std::string& data) { + if (url != current_extension_fetch_.url) { + LOG(ERROR) << "Called with unexpected url:'" << url.spec() + << "' expected:'" << current_extension_fetch_.url.spec() << "'"; + NOTREACHED(); + } else if (status.status() == URLRequestStatus::SUCCESS && + response_code == 200) { + // Successfully fetched - now write crx to a file so we can have the + // ExtensionsService install it. + file_io_loop_->PostTask(FROM_HERE, NewRunnableMethod( + file_handler_.get(), &ExtensionUpdaterFileHandler::WriteTempFile, + current_extension_fetch_.id, data, this)); + } else { + // TODO(asargent) do things like exponential backoff, handling + // 503 Service Unavailable / Retry-After headers, etc. here. + // (http://crbug.com/12546). + LOG(INFO) << "Failed to fetch extension '" << + url.possibly_invalid_spec() << "' response code:" << response_code; + } + extension_fetcher_.reset(); + current_extension_fetch_ = ExtensionFetch(); + + // If there are any pending downloads left, start one. + if (extensions_pending_.size() > 0) { + ExtensionFetch next = extensions_pending_.front(); + extensions_pending_.pop_front(); + FetchUpdatedExtension(next.id, next.url); + } +} + +void ExtensionUpdater::OnCRXFileWritten(const std::string& id, + const FilePath& path) { + // TODO(asargent) - Instead of calling InstallExtension here, we should have + // an UpdateExtension method in ExtensionsService and rely on it to check + // that the extension is still installed, and still an older version than + // what we're handing it. + // (http://crbug.com/12764). + ExtensionInstallCallback* callback = + NewCallback(this, &ExtensionUpdater::OnExtensionInstallFinished); + service_->UpdateExtension(id, path, false, callback); +} + +void ExtensionUpdater::OnExtensionInstallFinished(const FilePath& path, + Extension* extension) { + // Have the file_handler_ delete the temp file on the file I/O thread. + file_io_loop_->PostTask(FROM_HERE, NewRunnableMethod( + file_handler_.get(), &ExtensionUpdaterFileHandler::DeleteFile, path)); +} + +static const char* kURLEncodedEquals = "%3D"; // '=' +static const char* kURLEncodedAnd = "%26"; // '&' + +void ExtensionUpdater::TimerFired() { + // Generate a set of update urls for loaded extensions. + std::set<GURL> urls; + const ExtensionList* extensions = service_->extensions(); + for (ExtensionList::const_iterator iter = extensions->begin(); + iter != extensions->end(); ++iter) { + Extension* extension = (*iter); + const GURL& update_url = extension->update_url(); + if (update_url.is_empty() || extension->id().empty()) { + continue; + } + + DCHECK(update_url.is_valid()); + DCHECK(!update_url.has_ref()); + + // Append extension information to the url. + std::string full_url_string = update_url.spec(); + full_url_string.append(update_url.has_query() ? "&" : "?"); + full_url_string.append("x="); + + full_url_string.append("id"); + full_url_string.append(kURLEncodedEquals); + full_url_string.append(extension->id()); + + const Version* version = extension->version(); + DCHECK(version); + full_url_string.append("v"); + full_url_string.append(kURLEncodedEquals); + full_url_string.append(version->GetString()); + full_url_string.append(kURLEncodedAnd); + + GURL full_url(full_url_string); + if (!full_url.is_valid()) { + LOG(ERROR) << "invalid url: " << full_url.possibly_invalid_spec(); + NOTREACHED(); + } else { + urls.insert(full_url); + } + } + + // Now do an update check for each url we found. + for (std::set<GURL>::iterator iter = urls.begin(); iter != urls.end(); + ++iter) { + // StartUpdateCheck makes sure the url isn't already downloading or + // scheduled, so we don't need to check before calling it. + StartUpdateCheck(*iter); + } + timer_.Reset(); +} + +static void ManifestParseError(const char* details, ...) { + va_list args; + va_start(args, details); + std::string message("Extension update manifest parse error: "); + StringAppendV(&message, details, args); + ExtensionErrorReporter::GetInstance()->ReportError(message, false); +} + +// Checks whether a given node's name matches |expected_name| and +// |expected_namespace|. +static bool TagNameEquals(const xmlNode* node, const char* expected_name, + const xmlNs* expected_namespace) { + if (node->ns != expected_namespace) { + return false; + } + return 0 == strcmp(expected_name, reinterpret_cast<const char*>(node->name)); +} + +// Returns child nodes of |root| with name |name| in namespace |xml_namespace|. +static std::vector<xmlNode*> GetChildren(xmlNode* root, xmlNs* xml_namespace, + const char* name) { + std::vector<xmlNode*> result; + for (xmlNode* child = root->children; child != NULL; child = child->next) { + if (!TagNameEquals(child, name, xml_namespace)) { + continue; + } + result.push_back(child); + } + return result; +} + +// Returns the value of a named attribute, or the empty string. +static std::string GetAttribute(xmlNode* node, const char* attribute_name) { + const xmlChar* name = reinterpret_cast<const xmlChar*>(attribute_name); + for (xmlAttr* attr = node->properties; attr != NULL; attr = attr->next) { + if (!xmlStrcmp(attr->name, name) && attr->children && + attr->children->content) { + return std::string(reinterpret_cast<const char*>( + attr->children->content)); + } + } + return std::string(); +} + +// This is used for the xml parser to report errors. This assumes the context +// is a pointer to a std::string where the error message should be appended. +static void XmlErrorFunc(void *context, const char *message, ...) { + va_list args; + va_start(args, message); + std::string* error = static_cast<std::string*>(context); + StringAppendV(error, message, args); +} + +// Utility class for cleaning up xml parser state when leaving a scope. +class ScopedXmlParserCleanup { + public: + ScopedXmlParserCleanup() : document_(NULL) {} + ~ScopedXmlParserCleanup() { + if (document_) + xmlFreeDoc(document_); + xmlCleanupParser(); + } + void set_document(xmlDocPtr document) { + document_ = document; + } + + private: + xmlDocPtr document_; +}; + +// Returns a pointer to the xmlNs on |node| with the |expected_href|, or +// NULL if there isn't one with that href. +static xmlNs* GetNamespace(xmlNode* node, const char* expected_href) { + const xmlChar* href = reinterpret_cast<const xmlChar*>(expected_href); + for (xmlNs* ns = node->ns; ns != NULL; ns = ns->next) { + if (ns->href && !xmlStrcmp(ns->href, href)) { + return ns; + } + } + return NULL; +} + +// This is a separate sub-class so that we can have access to the private +// ParseResult struct, but avoid making the .h file include the xml api headers. +class ExtensionUpdater::ParseHelper { + public: + // Helper function for ExtensionUpdater::Parse that reads in values for a + // single <app> tag. It returns a boolean indicating success or failure. + static bool ParseSingleAppTag(xmlNode* app_node, xmlNs* xml_namespace, + ParseResult* result) { + // Read the extension id. + result->extension_id = GetAttribute(app_node, "appid"); + if (result->extension_id.length() == 0) { + ManifestParseError("Missing appid on app node"); + return false; + } + + // Get the updatecheck node. + std::vector<xmlNode*> updates = GetChildren(app_node, xml_namespace, + "updatecheck"); + if (updates.size() > 1) { + ManifestParseError("Too many updatecheck tags on app (expecting only 1)"); + return false; + } + if (updates.size() == 0) { + ManifestParseError("Missing updatecheck on app"); + return false; + } + xmlNode *updatecheck = updates[0]; + + // Find the url to the crx file. + result->crx_url = GURL(GetAttribute(updatecheck, "codebase")); + if (!result->crx_url.is_valid()) { + ManifestParseError("Invalid codebase url"); + return false; + } + + // Get the version. + std::string tmp = GetAttribute(updatecheck, "version"); + if (tmp.length() == 0) { + ManifestParseError("Missing version for updatecheck"); + return false; + } + result->version.reset(Version::GetVersionFromString(tmp)); + if (!result->version.get()) { + ManifestParseError("Invalid version"); + return false; + } + + // Get the minimum browser version (not required). + tmp = GetAttribute(updatecheck, "prodversionmin"); + if (tmp.length()) { + result->browser_min_version.reset(Version::GetVersionFromString(tmp)); + if (!result->browser_min_version.get()) { + ManifestParseError("Invalid prodversionmin"); + return false; + } + } + return true; + } +}; + +bool ExtensionUpdater::Parse(const std::string& manifest_xml, + ParseResultList* results) { + std::string xml_errors; + ScopedXmlErrorFunc error_func(&xml_errors, &XmlErrorFunc); + ScopedXmlParserCleanup xml_cleanup; + + xmlDocPtr document = xmlParseDoc( + reinterpret_cast<const xmlChar*>(manifest_xml.c_str())); + if (!document) { + ManifestParseError(xml_errors.c_str()); + return false; + } + xml_cleanup.set_document(document); + + xmlNode *root = xmlDocGetRootElement(document); + if (!root) { + ManifestParseError("Missing root node"); + return false; + } + + // Look for the required namespace declaration. + xmlNs* gupdate_ns = GetNamespace(root, kExpectedGupdateXmlns); + if (!gupdate_ns) { + ManifestParseError("Missing or incorrect xmlns on gupdate tag"); + return false; + } + + if (!TagNameEquals(root, "gupdate", gupdate_ns)) { + ManifestParseError("Missing gupdate tag"); + return false; + } + + // Check for the gupdate "protocol" attribute. + if (GetAttribute(root, "protocol") != kExpectedGupdateProtocol) { + ManifestParseError("Missing/incorrect protocol on gupdate tag " + "(expected '%s')", kExpectedGupdateProtocol); + return false; + } + + // Parse each of the <app> tags. + ScopedVector<ParseResult> tmp_results; + std::vector<xmlNode*> apps = GetChildren(root, gupdate_ns, "app"); + for (unsigned int i = 0; i < apps.size(); i++) { + ParseResult* current = new ParseResult(); + tmp_results.push_back(current); + if (!ParseHelper::ParseSingleAppTag(apps[i], gupdate_ns, current)) { + return false; + } + } + tmp_results.release(results); + + return true; +} + +std::vector<int> ExtensionUpdater::DetermineUpdates( + const ParseResultList& possible_updates) { + + std::vector<int> result; + + // This will only get set if one of possible_updates specifies + // browser_min_version. + scoped_ptr<Version> browser_version; + + for (size_t i = 0; i < possible_updates.size(); i++) { + ParseResult* update = possible_updates[i]; + + Extension* extension = service_->GetExtensionById(update->extension_id); + if (!extension) + continue; + + // If the update version is the same or older than what's already installed, + // we don't want it. + if (update->version.get()->CompareTo(*(extension->version())) <= 0) + continue; + + // If the update specifies a browser minimum version, do we qualify? + if (update->browser_min_version.get()) { + // First determine the browser version if we haven't already. + if (!browser_version.get()) { + scoped_ptr<FileVersionInfo> version_info( + FileVersionInfo::CreateFileVersionInfoForCurrentModule()); + if (version_info.get()) { + browser_version.reset(Version::GetVersionFromString( + version_info->product_version())); + } + } + if (browser_version.get() && + update->browser_min_version->CompareTo(*browser_version.get()) > 0) { + // TODO(asargent) - We may want this to show up in the extensions UI + // eventually. (http://crbug.com/12547). + LOG(WARNING) << "Updated version of extension " << extension->id() << + " available, but requires chrome version " << + update->browser_min_version->GetString(); + continue; + } + } + result.push_back(i); + } + return result; +} + +void ExtensionUpdater::StartUpdateCheck(const GURL& url) { + if (std::find(manifests_pending_.begin(), manifests_pending_.end(), url) != + manifests_pending_.end()) { + return; // already scheduled + } + + if (manifest_fetcher_.get() != NULL) { + if (manifest_fetcher_->url() != url) { + manifests_pending_.push_back(url); + } + } else { + manifest_fetcher_.reset( + URLFetcher::Create(kManifestFetcherId, url, URLFetcher::GET, this)); + manifest_fetcher_->set_request_context(Profile::GetDefaultRequestContext()); + manifest_fetcher_->Start(); + } +} + +void ExtensionUpdater::FetchUpdatedExtension(const std::string& id, GURL url) { + for (std::deque<ExtensionFetch>::const_iterator iter = + extensions_pending_.begin(); + iter != extensions_pending_.end(); ++iter) { + if (iter->id == id || iter->url == url) { + return; // already scheduled + } + } + + if (extension_fetcher_.get() != NULL) { + if (extension_fetcher_->url() != url) { + extensions_pending_.push_back(ExtensionFetch(id, url)); + } + } else { + extension_fetcher_.reset( + URLFetcher::Create(kExtensionFetcherId, url, URLFetcher::GET, this)); + extension_fetcher_->set_request_context( + Profile::GetDefaultRequestContext()); + extension_fetcher_->Start(); + current_extension_fetch_ = ExtensionFetch(id, url); + } +} |