/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /** * The AddonUpdateChecker is responsible for retrieving the update information * from an add-on's remote update manifest. */ "use strict"; const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; this.EXPORTED_SYMBOLS = [ "AddonUpdateChecker" ]; const TIMEOUT = 60 * 1000; const PREFIX_NS_RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"; const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#"; const PREFIX_ITEM = "urn:mozilla:item:"; const PREFIX_EXTENSION = "urn:mozilla:extension:"; const PREFIX_THEME = "urn:mozilla:theme:"; const TOOLKIT_ID = "toolkit@mozilla.org" #ifdef MOZ_PHOENIX_EXTENSIONS const FIREFOX_ID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}" #endif const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml" const PREF_UPDATE_REQUIREBUILTINCERTS = "extensions.update.requireBuiltInCerts"; Components.utils.import("resource://gre/modules/Services.jsm"); Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", "resource://gre/modules/addons/AddonRepository.jsm"); // Shared code for suppressing bad cert dialogs. XPCOMUtils.defineLazyGetter(this, "CertUtils", function certUtilsLazyGetter() { let certUtils = {}; Components.utils.import("resource://gre/modules/CertUtils.jsm", certUtils); return certUtils; }); var gRDF = Cc["@mozilla.org/rdf/rdf-service;1"]. getService(Ci.nsIRDFService); Cu.import("resource://gre/modules/Log.jsm"); const LOGGER_ID = "addons.update-checker"; // Create a new logger for use by the Addons Update Checker // (Requires AddonManager.jsm) let logger = Log.repository.getLogger(LOGGER_ID); /** * A serialisation method for RDF data that produces an identical string * for matching RDF graphs. * The serialisation is not complete, only assertions stemming from a given * resource are included, multiple references to the same resource are not * permitted, and the RDF prolog and epilog are not included. * RDF Blob and Date literals are not supported. */ function RDFSerializer() { this.cUtils = Cc["@mozilla.org/rdf/container-utils;1"]. getService(Ci.nsIRDFContainerUtils); this.resources = []; } RDFSerializer.prototype = { INDENT: " ", // The indent used for pretty-printing resources: null, // Array of the resources that have been found /** * Escapes characters from a string that should not appear in XML. * * @param aString * The string to be escaped * @return a string with all characters invalid in XML character data * converted to entity references. */ escapeEntities: function RDFS_escapeEntities(aString) { aString = aString.replace(/&/g, "&"); aString = aString.replace(/</g, "<"); aString = aString.replace(/>/g, ">"); return aString.replace(/"/g, """); }, /** * Serializes all the elements of an RDF container. * * @param aDs * The RDF datasource * @param aContainer * The RDF container to output the child elements of * @param aIndent * The current level of indent for pretty-printing * @return a string containing the serialized elements. */ serializeContainerItems: function RDFS_serializeContainerItems(aDs, aContainer, aIndent) { var result = ""; var items = aContainer.GetElements(); while (items.hasMoreElements()) { var item = items.getNext().QueryInterface(Ci.nsIRDFResource); result += aIndent + "<RDF:li>\n" result += this.serializeResource(aDs, item, aIndent + this.INDENT); result += aIndent + "</RDF:li>\n" } return result; }, /** * Serializes all em:* (see EM_NS) properties of an RDF resource except for * the em:signature property. As this serialization is to be compared against * the manifest signature it cannot contain the em:signature property itself. * * @param aDs * The RDF datasource * @param aResource * The RDF resource that contains the properties to serialize * @param aIndent * The current level of indent for pretty-printing * @return a string containing the serialized properties. * @throws if the resource contains a property that cannot be serialized */ serializeResourceProperties: function RDFS_serializeResourceProperties(aDs, aResource, aIndent) { var result = ""; var items = []; var arcs = aDs.ArcLabelsOut(aResource); while (arcs.hasMoreElements()) { var arc = arcs.getNext().QueryInterface(Ci.nsIRDFResource); if (arc.ValueUTF8.substring(0, PREFIX_NS_EM.length) != PREFIX_NS_EM) continue; var prop = arc.ValueUTF8.substring(PREFIX_NS_EM.length); if (prop == "signature") continue; var targets = aDs.GetTargets(aResource, arc, true); while (targets.hasMoreElements()) { var target = targets.getNext(); if (target instanceof Ci.nsIRDFResource) { var item = aIndent + "<em:" + prop + ">\n"; item += this.serializeResource(aDs, target, aIndent + this.INDENT); item += aIndent + "</em:" + prop + ">\n"; items.push(item); } else if (target instanceof Ci.nsIRDFLiteral) { items.push(aIndent + "<em:" + prop + ">" + this.escapeEntities(target.Value) + "</em:" + prop + ">\n"); } else if (target instanceof Ci.nsIRDFInt) { items.push(aIndent + "<em:" + prop + " NC:parseType=\"Integer\">" + target.Value + "</em:" + prop + ">\n"); } else { throw Components.Exception("Cannot serialize unknown literal type"); } } } items.sort(); result += items.join(""); return result; }, /** * Recursively serializes an RDF resource and all resources it links to. * This will only output EM_NS properties and will ignore any em:signature * property. * * @param aDs * The RDF datasource * @param aResource * The RDF resource to serialize * @param aIndent * The current level of indent for pretty-printing. If undefined no * indent will be added * @return a string containing the serialized resource. * @throws if the RDF data contains multiple references to the same resource. */ serializeResource: function RDFS_serializeResource(aDs, aResource, aIndent) { if (this.resources.indexOf(aResource) != -1 ) { // We cannot output multiple references to the same resource. throw Components.Exception("Cannot serialize multiple references to " + aResource.Value); } if (aIndent === undefined) aIndent = ""; this.resources.push(aResource); var container = null; var type = "Description"; if (this.cUtils.IsSeq(aDs, aResource)) { type = "Seq"; container = this.cUtils.MakeSeq(aDs, aResource); } else if (this.cUtils.IsAlt(aDs, aResource)) { type = "Alt"; container = this.cUtils.MakeAlt(aDs, aResource); } else if (this.cUtils.IsBag(aDs, aResource)) { type = "Bag"; container = this.cUtils.MakeBag(aDs, aResource); } var result = aIndent + "<RDF:" + type; if (!gRDF.IsAnonymousResource(aResource)) result += " about=\"" + this.escapeEntities(aResource.ValueUTF8) + "\""; result += ">\n"; if (container) result += this.serializeContainerItems(aDs, container, aIndent + this.INDENT); result += this.serializeResourceProperties(aDs, aResource, aIndent + this.INDENT); result += aIndent + "</RDF:" + type + ">\n"; return result; } } /** * Parses an RDF style update manifest into an array of update objects. * * @param aId * The ID of the add-on being checked for updates * @param aUpdateKey * An optional update key for the add-on * @param aRequest * The XMLHttpRequest that has retrieved the update manifest * @return an array of update objects * @throws if the update manifest is invalid in any way */ function parseRDFManifest(aId, aUpdateKey, aRequest) { function EM_R(aProp) { return gRDF.GetResource(PREFIX_NS_EM + aProp); } function getValue(aLiteral) { if (aLiteral instanceof Ci.nsIRDFLiteral) return aLiteral.Value; if (aLiteral instanceof Ci.nsIRDFResource) return aLiteral.Value; if (aLiteral instanceof Ci.nsIRDFInt) return aLiteral.Value; return null; } function getProperty(aDs, aSource, aProperty) { return getValue(aDs.GetTarget(aSource, EM_R(aProperty), true)); } function getBooleanProperty(aDs, aSource, aProperty) { let propValue = aDs.GetTarget(aSource, EM_R(aProperty), true); if (!propValue) return undefined; return getValue(propValue) == "true"; } function getRequiredProperty(aDs, aSource, aProperty) { let value = getProperty(aDs, aSource, aProperty); if (!value) throw Components.Exception("Update manifest is missing a required " + aProperty + " property."); return value; } let rdfParser = Cc["@mozilla.org/rdf/xml-parser;1"]. createInstance(Ci.nsIRDFXMLParser); let ds = Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"]. createInstance(Ci.nsIRDFDataSource); rdfParser.parseString(ds, aRequest.channel.URI, aRequest.responseText); // Differentiating between add-on types is deprecated let extensionRes = gRDF.GetResource(PREFIX_EXTENSION + aId); let themeRes = gRDF.GetResource(PREFIX_THEME + aId); let itemRes = gRDF.GetResource(PREFIX_ITEM + aId); let addonRes = ds.ArcLabelsOut(extensionRes).hasMoreElements() ? extensionRes : ds.ArcLabelsOut(themeRes).hasMoreElements() ? themeRes : itemRes; // If we have an update key then the update manifest must be signed if (aUpdateKey) { let signature = getProperty(ds, addonRes, "signature"); if (!signature) throw Components.Exception("Update manifest for " + aId + " does not contain a required signature"); let serializer = new RDFSerializer(); let updateString = null; try { updateString = serializer.serializeResource(ds, addonRes); } catch (e) { throw Components.Exception("Failed to generate signed string for " + aId + ". Serializer threw " + e, e.result); } let result = false; try { let verifier = Cc["@mozilla.org/security/datasignatureverifier;1"]. getService(Ci.nsIDataSignatureVerifier); result = verifier.verifyData(updateString, signature, aUpdateKey); } catch (e) { throw Components.Exception("The signature or updateKey for " + aId + " is malformed." + "Verifier threw " + e, e.result); } if (!result) throw Components.Exception("The signature for " + aId + " was not created by the add-on's updateKey"); } let updates = ds.GetTarget(addonRes, EM_R("updates"), true); // A missing updates property doesn't count as a failure, just as no avialable // update information if (!updates) { logger.warn("Update manifest for " + aId + " did not contain an updates property"); return []; } if (!(updates instanceof Ci.nsIRDFResource)) throw Components.Exception("Missing updates property for " + addonRes.Value); let cu = Cc["@mozilla.org/rdf/container-utils;1"]. getService(Ci.nsIRDFContainerUtils); if (!cu.IsContainer(ds, updates)) throw Components.Exception("Updates property was not an RDF container"); let results = []; let ctr = Cc["@mozilla.org/rdf/container;1"]. createInstance(Ci.nsIRDFContainer); ctr.Init(ds, updates); let items = ctr.GetElements(); while (items.hasMoreElements()) { let item = items.getNext().QueryInterface(Ci.nsIRDFResource); let version = getProperty(ds, item, "version"); if (!version) { logger.warn("Update manifest is missing a required version property."); continue; } logger.debug("Found an update entry for " + aId + " version " + version); let targetApps = ds.GetTargets(item, EM_R("targetApplication"), true); while (targetApps.hasMoreElements()) { let targetApp = targetApps.getNext().QueryInterface(Ci.nsIRDFResource); let appEntry = {}; try { appEntry.id = getRequiredProperty(ds, targetApp, "id"); appEntry.minVersion = getRequiredProperty(ds, targetApp, "minVersion"); appEntry.maxVersion = getRequiredProperty(ds, targetApp, "maxVersion"); } catch (e) { logger.warn(e); continue; } let result = { id: aId, version: version, multiprocessCompatible: getBooleanProperty(ds, item, "multiprocessCompatible"), updateURL: getProperty(ds, targetApp, "updateLink"), updateHash: getProperty(ds, targetApp, "updateHash"), updateInfoURL: getProperty(ds, targetApp, "updateInfoURL"), strictCompatibility: !!getBooleanProperty(ds, targetApp, "strictCompatibility"), targetApplications: [appEntry] }; if (result.updateURL && AddonManager.checkUpdateSecurity && result.updateURL.substring(0, 6) != "https:" && (!result.updateHash || result.updateHash.substring(0, 3) != "sha")) { logger.warn("updateLink " + result.updateURL + " is not secure and is not verified" + " by a strong enough hash (needs to be sha1 or stronger)."); delete result.updateURL; delete result.updateHash; } results.push(result); } } return results; } /** * Starts downloading an update manifest and then passes it to an appropriate * parser to convert to an array of update objects * * @param aId * The ID of the add-on being checked for updates * @param aUpdateKey * An optional update key for the add-on * @param aUrl * The URL of the update manifest * @param aObserver * An observer to pass results to */ function UpdateParser(aId, aUpdateKey, aUrl, aObserver) { this.id = aId; this.updateKey = aUpdateKey; this.observer = aObserver; this.url = aUrl; let requireBuiltIn = true; try { requireBuiltIn = Services.prefs.getBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS); } catch (e) { } logger.debug("Requesting " + aUrl); try { this.request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. createInstance(Ci.nsIXMLHttpRequest); this.request.open("GET", this.url, true); this.request.channel.notificationCallbacks = new CertUtils.BadCertHandler(!requireBuiltIn); this.request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; // Prevent the request from writing to cache. this.request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; this.request.overrideMimeType("text/xml"); this.request.setRequestHeader("Moz-XPI-Update", "1", true); this.request.timeout = TIMEOUT; var self = this; this.request.addEventListener("load", function loadEventListener(event) { self.onLoad() }, false); this.request.addEventListener("error", function errorEventListener(event) { self.onError() }, false); this.request.addEventListener("timeout", function timeoutEventListener(event) { self.onTimeout() }, false); this.request.send(null); } catch (e) { logger.error("Failed to request update manifest", e); } } UpdateParser.prototype = { id: null, updateKey: null, observer: null, request: null, url: null, /** * Called when the manifest has been successfully loaded. */ onLoad: function UP_onLoad() { let request = this.request; this.request = null; this._doneAt = new Error("place holder"); let requireBuiltIn = true; try { requireBuiltIn = Services.prefs.getBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS); } catch (e) { } try { CertUtils.checkCert(request.channel, !requireBuiltIn); } catch (e) { this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR); return; } if (!Components.isSuccessCode(request.status)) { logger.warn("Request failed: " + this.url + " - " + request.status); this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR); return; } let channel = request.channel; if (channel instanceof Ci.nsIHttpChannel && !channel.requestSucceeded) { logger.warn("Request failed: " + this.url + " - " + channel.responseStatus + ": " + channel.responseStatusText); this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR); return; } let xml = request.responseXML; if (!xml || xml.documentElement.namespaceURI == XMLURI_PARSE_ERROR) { logger.warn("Update manifest was not valid XML"); this.notifyError(AddonUpdateChecker.ERROR_PARSE_ERROR); return; } // We currently only know about RDF update manifests if (xml.documentElement.namespaceURI == PREFIX_NS_RDF) { let results = null; try { results = parseRDFManifest(this.id, this.updateKey, request); } catch (e) { logger.warn("onUpdateCheckComplete failed to parse RDF manifest", e); this.notifyError(AddonUpdateChecker.ERROR_PARSE_ERROR); return; } if ("onUpdateCheckComplete" in this.observer) { try { this.observer.onUpdateCheckComplete(results); } catch (e) { logger.warn("onUpdateCheckComplete notification failed", e); } } else { logger.warn("onUpdateCheckComplete may not properly cancel", new Error("stack marker")); } return; } logger.warn("Update manifest had an unrecognised namespace: " + xml.documentElement.namespaceURI); this.notifyError(AddonUpdateChecker.ERROR_UNKNOWN_FORMAT); }, /** * Called when the request times out */ onTimeout: function() { this.request = null; this._doneAt = new Error("Timed out"); logger.warn("Request for " + this.url + " timed out"); this.notifyError(AddonUpdateChecker.ERROR_TIMEOUT); }, /** * Called when the manifest failed to load. */ onError: function UP_onError() { if (!Components.isSuccessCode(this.request.status)) { logger.warn("Request failed: " + this.url + " - " + this.request.status); } else if (this.request.channel instanceof Ci.nsIHttpChannel) { try { if (this.request.channel.requestSucceeded) { logger.warn("Request failed: " + this.url + " - " + this.request.channel.responseStatus + ": " + this.request.channel.responseStatusText); } } catch (e) { logger.warn("HTTP Request failed for an unknown reason"); } } else { logger.warn("Request failed for an unknown reason"); } this.request = null; this._doneAt = new Error("UP_onError"); this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR); }, /** * Helper method to notify the observer that an error occured. */ notifyError: function UP_notifyError(aStatus) { if ("onUpdateCheckError" in this.observer) { try { this.observer.onUpdateCheckError(aStatus); } catch (e) { logger.warn("onUpdateCheckError notification failed", e); } } }, /** * Called to cancel an in-progress update check. */ cancel: function UP_cancel() { if (!this.request) { logger.error("Trying to cancel already-complete request", this._doneAt); return; } this.request.abort(); this.request = null; this._doneAt = new Error("UP_cancel"); this.notifyError(AddonUpdateChecker.ERROR_CANCELLED); } }; /** * Tests if an update matches a version of the application or platform * * @param aUpdate * The available update * @param aAppVersion * The application version to use * @param aPlatformVersion * The platform version to use * @param aIgnoreMaxVersion * Ignore maxVersion when testing if an update matches. Optional. * @param aIgnoreStrictCompat * Ignore strictCompatibility when testing if an update matches. Optional. * @param aCompatOverrides * AddonCompatibilityOverride objects to match against. Optional. * @return true if the update is compatible with the application/platform */ function matchesVersions(aUpdate, aAppVersion, aPlatformVersion, aIgnoreMaxVersion, aIgnoreStrictCompat, aCompatOverrides) { if (aCompatOverrides) { let override = AddonRepository.findMatchingCompatOverride(aUpdate.version, aCompatOverrides, aAppVersion, aPlatformVersion); if (override && override.type == "incompatible") return false; } if (aUpdate.strictCompatibility && !aIgnoreStrictCompat) aIgnoreMaxVersion = false; let result = false; for (let app of aUpdate.targetApplications) { if (app.id == Services.appinfo.ID) { return (Services.vc.compare(aAppVersion, app.minVersion) >= 0) && (aIgnoreMaxVersion || (Services.vc.compare(aAppVersion, app.maxVersion) <= 0)); } #ifdef MOZ_PHOENIX_EXTENSIONS if (app.id == FIREFOX_ID) { return (Services.vc.compare(aAppVersion, app.minVersion) >= 0) && (aIgnoreMaxVersion || (Services.vc.compare(aAppVersion, app.maxVersion) <= 0)); } #endif if (app.id == TOOLKIT_ID) { result = (Services.vc.compare(aPlatformVersion, app.minVersion) >= 0) && (aIgnoreMaxVersion || (Services.vc.compare(aPlatformVersion, app.maxVersion) <= 0)); } } return result; } this.AddonUpdateChecker = { // These must be kept in sync with AddonManager // The update check timed out ERROR_TIMEOUT: -1, // There was an error while downloading the update information. ERROR_DOWNLOAD_ERROR: -2, // The update information was malformed in some way. ERROR_PARSE_ERROR: -3, // The update information was not in any known format. ERROR_UNKNOWN_FORMAT: -4, // The update information was not correctly signed or there was an SSL error. ERROR_SECURITY_ERROR: -5, // The update was cancelled ERROR_CANCELLED: -6, /** * Retrieves the best matching compatibility update for the application from * a list of available update objects. * * @param aUpdates * An array of update objects * @param aVersion * The version of the add-on to get new compatibility information for * @param aIgnoreCompatibility * An optional parameter to get the first compatibility update that * is compatible with any version of the application or toolkit * @param aAppVersion * The version of the application or null to use the current version * @param aPlatformVersion * The version of the platform or null to use the current version * @param aIgnoreMaxVersion * Ignore maxVersion when testing if an update matches. Optional. * @param aIgnoreStrictCompat * Ignore strictCompatibility when testing if an update matches. Optional. * @return an update object if one matches or null if not */ getCompatibilityUpdate: function AUC_getCompatibilityUpdate(aUpdates, aVersion, aIgnoreCompatibility, aAppVersion, aPlatformVersion, aIgnoreMaxVersion, aIgnoreStrictCompat) { if (!aAppVersion) aAppVersion = Services.appinfo.version; if (!aPlatformVersion) aPlatformVersion = Services.appinfo.platformVersion; for (let update of aUpdates) { if (Services.vc.compare(update.version, aVersion) == 0) { if (aIgnoreCompatibility) { for (let targetApp of update.targetApplications) { let id = targetApp.id; #ifdef MOZ_PHOENIX_EXTENSIONS if (id == Services.appinfo.ID || id == FIREFOX_ID || id == TOOLKIT_ID) #else if (id == Services.appinfo.ID || id == TOOLKIT_ID) #endif return update; } } else if (matchesVersions(update, aAppVersion, aPlatformVersion, aIgnoreMaxVersion, aIgnoreStrictCompat)) { return update; } } } return null; }, /** * Returns the newest available update from a list of update objects. * * @param aUpdates * An array of update objects * @param aAppVersion * The version of the application or null to use the current version * @param aPlatformVersion * The version of the platform or null to use the current version * @param aIgnoreMaxVersion * When determining compatible updates, ignore maxVersion. Optional. * @param aIgnoreStrictCompat * When determining compatible updates, ignore strictCompatibility. Optional. * @param aCompatOverrides * Array of AddonCompatibilityOverride to take into account. Optional. * @return an update object if one matches or null if not */ getNewestCompatibleUpdate: function AUC_getNewestCompatibleUpdate(aUpdates, aAppVersion, aPlatformVersion, aIgnoreMaxVersion, aIgnoreStrictCompat, aCompatOverrides) { if (!aAppVersion) aAppVersion = Services.appinfo.version; if (!aPlatformVersion) aPlatformVersion = Services.appinfo.platformVersion; let blocklist = Cc["@mozilla.org/extensions/blocklist;1"]. getService(Ci.nsIBlocklistService); let newest = null; for (let update of aUpdates) { if (!update.updateURL) continue; let state = blocklist.getAddonBlocklistState(update, aAppVersion, aPlatformVersion); if (state != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) continue; if ((newest == null || (Services.vc.compare(newest.version, update.version) < 0)) && matchesVersions(update, aAppVersion, aPlatformVersion, aIgnoreMaxVersion, aIgnoreStrictCompat, aCompatOverrides)) { newest = update; } } return newest; }, /** * Starts an update check. * * @param aId * The ID of the add-on being checked for updates * @param aUpdateKey * An optional update key for the add-on * @param aUrl * The URL of the add-on's update manifest * @param aObserver * An observer to notify of results * @return UpdateParser so that the caller can use UpdateParser.cancel() to shut * down in-progress update requests */ checkForUpdates: function AUC_checkForUpdates(aId, aUpdateKey, aUrl, aObserver) { // Exclude default theme if (aId != "{972ce4c6-7e08-4474-a285-3208198ce6fd}") return new UpdateParser(aId, aUpdateKey, aUrl, aObserver); } };