// 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 #include #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/base/escape.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 = 30; 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 { 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 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(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 parsed; // TODO(asargent) - We should do the xml parsing in a sandboxed process. // (http://crbug.com/12677). if (Parse(data, &parsed.get())) { std::vector 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) { service_->UpdateExtension(id, path); } 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)); } // Helper function for building up request parameters in update check urls. It // appends information about one extension to a request parameter string. The // format for request parameters in update checks is: // // ?x=EXT1_INFO&x=EXT2_INFO // // where EXT1_INFO and EXT2_INFO are url-encoded strings of the form: // // id=EXTENSION_ID&v=VERSION&uc // // So for two extensions like: // Extension 1- id:aaaa version:1.1 // Extension 2- id:bbbb version:2.0 // // the full update url would be: // http://somehost/path?x=id%3Daaaa%26v%3D1.1%26uc&x=id%3Dbbbb%26v%3D2.0%26uc // // (Note that '=' is %3D and '&' is %26 when urlencoded.) // // Again, this function would just append one extension's worth of data, e.g. // "x=id%3Daaaa%26v%3D1.1%26uc" void AppendExtensionInfo(std::string* str, const Extension& extension) { const Version* version = extension.version(); DCHECK(version); std::vector parts; // Push extension id, version, and uc (indicates an update check to Omaha). parts.push_back("id=" + extension.id()); parts.push_back("v=" + version->GetString()); parts.push_back("uc"); str->append("x=" + EscapeQueryParamValue(JoinString(parts, '&'))); } void ExtensionUpdater::TimerFired() { // Generate a set of update urls for loaded extensions. std::set 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() ? "&" : "?"); AppendExtensionInfo(&full_url_string, *extension); 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::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(node->name)); } // Returns child nodes of |root| with name |name| in namespace |xml_namespace|. static std::vector GetChildren(xmlNode* root, xmlNs* xml_namespace, const char* name) { std::vector 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(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( 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(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(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 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 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(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 tags. ScopedVector tmp_results; std::vector 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; } } results->insert(results->end(), tmp_results.begin(), tmp_results.end()); tmp_results.get().clear(); return true; } std::vector ExtensionUpdater::DetermineUpdates( const ParseResultList& possible_updates) { std::vector result; // This will only get set if one of possible_updates specifies // browser_min_version. scoped_ptr 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 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, const GURL& url) { for (std::deque::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); } }