mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-12 12:55:46 +00:00
Bug 1192924: Split out add-on update.xml parsing code from GMP modules. r=spohl
The system add-on update checks will use the same update.xml format as GMP so this splits out the code for parsing and downloading files into a standalone module that both can reuse. --HG-- extra : commitid : I89HsxRnP9T extra : rebase_source : 1b38a03e202f73ba214604e083745e8c6b5984b5
This commit is contained in:
parent
a22c2d1f9e
commit
77f3ee0b44
@ -8,10 +8,6 @@ this.EXPORTED_SYMBOLS = [];
|
|||||||
|
|
||||||
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu, manager: Cm} =
|
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu, manager: Cm} =
|
||||||
Components;
|
Components;
|
||||||
// Chunk size for the incremental downloader
|
|
||||||
const DOWNLOAD_CHUNK_BYTES_SIZE = 300000;
|
|
||||||
// Incremental downloader interval
|
|
||||||
const DOWNLOAD_INTERVAL = 0;
|
|
||||||
// 1 day default
|
// 1 day default
|
||||||
const DEFAULT_SECONDS_BETWEEN_CHECKS = 60 * 60 * 24;
|
const DEFAULT_SECONDS_BETWEEN_CHECKS = 60 * 60 * 24;
|
||||||
|
|
||||||
@ -31,6 +27,7 @@ Cu.import("resource://gre/modules/Log.jsm");
|
|||||||
Cu.import("resource://gre/modules/osfile.jsm");
|
Cu.import("resource://gre/modules/osfile.jsm");
|
||||||
Cu.import("resource://gre/modules/Task.jsm");
|
Cu.import("resource://gre/modules/Task.jsm");
|
||||||
Cu.import("resource://gre/modules/GMPUtils.jsm");
|
Cu.import("resource://gre/modules/GMPUtils.jsm");
|
||||||
|
Cu.import("resource://gre/modules/addons/ProductAddonChecker.jsm");
|
||||||
|
|
||||||
this.EXPORTED_SYMBOLS = ["GMPInstallManager", "GMPExtractor", "GMPDownloader",
|
this.EXPORTED_SYMBOLS = ["GMPInstallManager", "GMPExtractor", "GMPDownloader",
|
||||||
"GMPAddon"];
|
"GMPAddon"];
|
||||||
@ -45,16 +42,6 @@ XPCOMUtils.defineLazyGetter(this, "gCertUtils", function() {
|
|||||||
XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
|
XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
|
||||||
"resource://gre/modules/UpdateUtils.jsm");
|
"resource://gre/modules/UpdateUtils.jsm");
|
||||||
|
|
||||||
/**
|
|
||||||
* Number of milliseconds after which we need to cancel `checkForAddons`.
|
|
||||||
*
|
|
||||||
* Bug 1087674 suggests that the XHR we use in `checkForAddons` may
|
|
||||||
* never terminate in presence of network nuisances (e.g. strange
|
|
||||||
* antivirus behavior). This timeout is a defensive measure to ensure
|
|
||||||
* that we fail cleanly in such case.
|
|
||||||
*/
|
|
||||||
const CHECK_FOR_ADDONS_TIMEOUT_DELAY_MS = 20000;
|
|
||||||
|
|
||||||
function getScopedLogger(prefix) {
|
function getScopedLogger(prefix) {
|
||||||
// `PARENT_LOGGER_ID.` being passed here effectively links this logger
|
// `PARENT_LOGGER_ID.` being passed here effectively links this logger
|
||||||
// to the parentLogger.
|
// to the parentLogger.
|
||||||
@ -108,38 +95,27 @@ GMPInstallManager.prototype = {
|
|||||||
this._deferred = Promise.defer();
|
this._deferred = Promise.defer();
|
||||||
let url = this._getURL();
|
let url = this._getURL();
|
||||||
|
|
||||||
this._request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
|
let allowNonBuiltIn = true;
|
||||||
createInstance(Ci.nsISupports);
|
let certs = null;
|
||||||
// This is here to let unit test code override XHR
|
if (!Services.prefs.prefHasUserValue(GMPPrefs.KEY_URL_OVERRIDE)) {
|
||||||
if (this._request.wrappedJSObject) {
|
allowNonBuiltIn = !GMPPrefs.get(GMPPrefs.KEY_CERT_REQUIREBUILTIN, true);
|
||||||
this._request = this._request.wrappedJSObject;
|
if (GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS, true)) {
|
||||||
|
certs = gCertUtils.readCertPrefs(GMPPrefs.KEY_CERTS_BRANCH);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this._request.open("GET", url, true);
|
|
||||||
let allowNonBuiltIn = !GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS, true);
|
|
||||||
this._request.channel.notificationCallbacks =
|
|
||||||
new gCertUtils.BadCertHandler(allowNonBuiltIn);
|
|
||||||
// Prevent the request from reading from the cache.
|
|
||||||
this._request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
|
|
||||||
// Prevent the request from writing to the cache.
|
|
||||||
this._request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
|
|
||||||
|
|
||||||
this._request.overrideMimeType("text/xml");
|
ProductAddonChecker.getProductAddonList(url, allowNonBuiltIn, certs).then((addons) => {
|
||||||
// The Cache-Control header is only interpreted by proxies and the
|
if (!addons) {
|
||||||
// final destination. It does not help if a resource is already
|
this._deferred.resolve([]);
|
||||||
// cached locally.
|
}
|
||||||
this._request.setRequestHeader("Cache-Control", "no-cache");
|
else {
|
||||||
// HTTP/1.0 servers might not implement Cache-Control and
|
this._deferred.resolve([for (a of addons) new GMPAddon(a)]);
|
||||||
// might only implement Pragma: no-cache
|
}
|
||||||
this._request.setRequestHeader("Pragma", "no-cache");
|
delete this._deferred;
|
||||||
|
}, (ex) => {
|
||||||
this._request.timeout = CHECK_FOR_ADDONS_TIMEOUT_DELAY_MS;
|
this._deferred.reject(ex);
|
||||||
this._request.addEventListener("error", event => this.onFailXML("onErrorXML", event), false);
|
delete this._deferred;
|
||||||
this._request.addEventListener("abort", event => this.onFailXML("onAbortXML", event), false);
|
});
|
||||||
this._request.addEventListener("timeout", event => this.onFailXML("onTimeoutXML", event), false);
|
|
||||||
this._request.addEventListener("load", event => this.onLoadXML(event), false);
|
|
||||||
|
|
||||||
log.info("sending request to: " + url);
|
|
||||||
this._request.send(null);
|
|
||||||
|
|
||||||
return this._deferred.promise;
|
return this._deferred.promise;
|
||||||
},
|
},
|
||||||
@ -341,132 +317,6 @@ GMPInstallManager.prototype = {
|
|||||||
* This is useful for tests.
|
* This is useful for tests.
|
||||||
*/
|
*/
|
||||||
overrideLeaveDownloadedZip: false,
|
overrideLeaveDownloadedZip: false,
|
||||||
|
|
||||||
/**
|
|
||||||
* The XMLHttpRequest succeeded and the document was loaded.
|
|
||||||
* @param event The nsIDOMEvent for the load
|
|
||||||
*/
|
|
||||||
onLoadXML: function(event) {
|
|
||||||
let log = getScopedLogger("GMPInstallManager.onLoadXML");
|
|
||||||
try {
|
|
||||||
log.info("request completed downloading document");
|
|
||||||
let certs = null;
|
|
||||||
if (!Services.prefs.prefHasUserValue(GMPPrefs.KEY_URL_OVERRIDE) &&
|
|
||||||
GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS, true)) {
|
|
||||||
certs = gCertUtils.readCertPrefs(GMPPrefs.KEY_CERTS_BRANCH);
|
|
||||||
}
|
|
||||||
|
|
||||||
let allowNonBuiltIn = !GMPPrefs.get(GMPPrefs.KEY_CERT_REQUIREBUILTIN,
|
|
||||||
true);
|
|
||||||
log.info("allowNonBuiltIn: " + allowNonBuiltIn);
|
|
||||||
|
|
||||||
gCertUtils.checkCert(this._request.channel, allowNonBuiltIn, certs);
|
|
||||||
|
|
||||||
this.parseResponseXML();
|
|
||||||
} catch (ex) {
|
|
||||||
log.error("could not load xml: " + ex);
|
|
||||||
this._deferred.reject({
|
|
||||||
target: event.target,
|
|
||||||
status: this._getChannelStatus(event.target),
|
|
||||||
message: "" + ex,
|
|
||||||
});
|
|
||||||
delete this._deferred;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the status code for the XMLHttpRequest
|
|
||||||
*/
|
|
||||||
_getChannelStatus: function(request) {
|
|
||||||
let log = getScopedLogger("GMPInstallManager._getChannelStatus");
|
|
||||||
let status = null;
|
|
||||||
try {
|
|
||||||
status = request.status;
|
|
||||||
log.info("request.status is: " + request.status);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status == null) {
|
|
||||||
status = request.channel.QueryInterface(Ci.nsIRequest).status;
|
|
||||||
}
|
|
||||||
return status;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* There was an error of some kind during the XMLHttpRequest. This
|
|
||||||
* error may have been caused by external factors (e.g. network
|
|
||||||
* issues) or internally (by a timeout).
|
|
||||||
*
|
|
||||||
* @param event The nsIDOMEvent for the error
|
|
||||||
*/
|
|
||||||
onFailXML: function(failure, event) {
|
|
||||||
let log = getScopedLogger("GMPInstallManager.onFailXML " + failure);
|
|
||||||
let request = event.target;
|
|
||||||
let status = this._getChannelStatus(request);
|
|
||||||
let message = "request.status: " + status + " (" + event.type + ")";
|
|
||||||
log.warn(message);
|
|
||||||
this._deferred.reject({
|
|
||||||
target: request,
|
|
||||||
status: status,
|
|
||||||
message: message
|
|
||||||
});
|
|
||||||
delete this._deferred;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an array of GMPAddon objects discovered by the update check.
|
|
||||||
* Or returns an empty array if there were any problems with parsing.
|
|
||||||
* If there's an error, it will be logged if logging is enabled.
|
|
||||||
*/
|
|
||||||
parseResponseXML: function() {
|
|
||||||
try {
|
|
||||||
let log = getScopedLogger("GMPInstallManager.parseResponseXML");
|
|
||||||
let updatesElement = this._request.responseXML.documentElement;
|
|
||||||
if (!updatesElement) {
|
|
||||||
let message = "empty updates document";
|
|
||||||
log.warn(message);
|
|
||||||
this._deferred.reject({
|
|
||||||
target: this._request,
|
|
||||||
message: message
|
|
||||||
});
|
|
||||||
delete this._deferred;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updatesElement.nodeName != "updates") {
|
|
||||||
let message = "got node name: " + updatesElement.nodeName +
|
|
||||||
", expected: updates";
|
|
||||||
log.warn(message);
|
|
||||||
this._deferred.reject({
|
|
||||||
target: this._request,
|
|
||||||
message: message
|
|
||||||
});
|
|
||||||
delete this._deferred;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ELEMENT_NODE = Ci.nsIDOMNode.ELEMENT_NODE;
|
|
||||||
let gmpResults = [];
|
|
||||||
for (let i = 0; i < updatesElement.childNodes.length; ++i) {
|
|
||||||
let updatesChildElement = updatesElement.childNodes.item(i);
|
|
||||||
if (updatesChildElement.nodeType != ELEMENT_NODE) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (updatesChildElement.localName == "addons") {
|
|
||||||
gmpResults = GMPAddon.parseGMPAddonsNode(updatesChildElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._deferred.resolve(gmpResults);
|
|
||||||
delete this._deferred;
|
|
||||||
} catch (e) {
|
|
||||||
this._deferred.reject({
|
|
||||||
target: this._request,
|
|
||||||
message: e
|
|
||||||
});
|
|
||||||
delete this._deferred;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -474,49 +324,16 @@ GMPInstallManager.prototype = {
|
|||||||
* GMPAddon objects are returns from GMPInstallManager.checkForAddons
|
* GMPAddon objects are returns from GMPInstallManager.checkForAddons
|
||||||
* GMPAddon objects can also be used in calls to GMPInstallManager.installAddon
|
* GMPAddon objects can also be used in calls to GMPInstallManager.installAddon
|
||||||
*
|
*
|
||||||
* @param gmpAddon The AUS response XML's DOM element `addon`
|
* @param addon The ProductAddonChecker `addon` object
|
||||||
*/
|
*/
|
||||||
function GMPAddon(gmpAddon) {
|
function GMPAddon(addon) {
|
||||||
let log = getScopedLogger("GMPAddon.constructor");
|
let log = getScopedLogger("GMPAddon.constructor");
|
||||||
gmpAddon.QueryInterface(Ci.nsIDOMElement);
|
for (let name of Object.keys(addon)) {
|
||||||
["id", "URL", "hashFunction",
|
this[name] = addon[name];
|
||||||
"hashValue", "version", "size"].forEach(name => {
|
}
|
||||||
if (gmpAddon.hasAttribute(name)) {
|
|
||||||
this[name] = gmpAddon.getAttribute(name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.size = Number(this.size) || undefined;
|
|
||||||
log.info ("Created new addon: " + this.toString());
|
log.info ("Created new addon: " + this.toString());
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Parses an XML GMP addons node from AUS into an array
|
|
||||||
* @param addonsElement An nsIDOMElement compatible node with XML from AUS
|
|
||||||
* @return An array of GMPAddon results
|
|
||||||
*/
|
|
||||||
GMPAddon.parseGMPAddonsNode = function(addonsElement) {
|
|
||||||
let log = getScopedLogger("GMPAddon.parseGMPAddonsNode");
|
|
||||||
let gmpResults = [];
|
|
||||||
if (addonsElement.localName !== "addons") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
addonsElement.QueryInterface(Ci.nsIDOMElement);
|
|
||||||
let addonCount = addonsElement.childNodes.length;
|
|
||||||
for (let i = 0; i < addonCount; ++i) {
|
|
||||||
let addonElement = addonsElement.childNodes.item(i);
|
|
||||||
if (addonElement.localName !== "addon") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
addonElement.QueryInterface(Ci.nsIDOMElement);
|
|
||||||
try {
|
|
||||||
gmpResults.push(new GMPAddon(addonElement));
|
|
||||||
} catch (e) {
|
|
||||||
log.warn("invalid addon: " + e);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return gmpResults;
|
|
||||||
};
|
|
||||||
GMPAddon.prototype = {
|
GMPAddon.prototype = {
|
||||||
/**
|
/**
|
||||||
* Returns a string representation of the addon
|
* Returns a string representation of the addon
|
||||||
@ -647,38 +464,7 @@ function GMPDownloader(gmpAddon)
|
|||||||
{
|
{
|
||||||
this._gmpAddon = gmpAddon;
|
this._gmpAddon = gmpAddon;
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Computes the file hash of fileToHash with the specified hash function
|
|
||||||
* @param hashFunctionName A hash function name such as sha512
|
|
||||||
* @param fileToHash An nsIFile to hash
|
|
||||||
* @return a promise which resolve to a digest in binary hex format
|
|
||||||
*/
|
|
||||||
GMPDownloader.computeHash = function(hashFunctionName, fileToHash) {
|
|
||||||
let log = getScopedLogger("GMPDownloader.computeHash");
|
|
||||||
let digest;
|
|
||||||
let fileStream = Cc["@mozilla.org/network/file-input-stream;1"].
|
|
||||||
createInstance(Ci.nsIFileInputStream);
|
|
||||||
fileStream.init(fileToHash, FileUtils.MODE_RDONLY,
|
|
||||||
FileUtils.PERMS_FILE, 0);
|
|
||||||
try {
|
|
||||||
let hash = Cc["@mozilla.org/security/hash;1"].
|
|
||||||
createInstance(Ci.nsICryptoHash);
|
|
||||||
let hashFunction =
|
|
||||||
Ci.nsICryptoHash[hashFunctionName.toUpperCase()];
|
|
||||||
if (!hashFunction) {
|
|
||||||
log.error("could not get hash function");
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
hash.init(hashFunction);
|
|
||||||
hash.updateFromStream(fileStream, -1);
|
|
||||||
digest = binaryToHex(hash.finish(false));
|
|
||||||
} catch (e) {
|
|
||||||
log.warn("failed to compute hash: " + e);
|
|
||||||
digest = "";
|
|
||||||
}
|
|
||||||
fileStream.close();
|
|
||||||
return Promise.resolve(digest);
|
|
||||||
},
|
|
||||||
GMPDownloader.prototype = {
|
GMPDownloader.prototype = {
|
||||||
/**
|
/**
|
||||||
* Starts the download process for an addon.
|
* Starts the download process for an addon.
|
||||||
@ -686,9 +472,10 @@ GMPDownloader.prototype = {
|
|||||||
* See GMPInstallManager.installAddon for resolve/rejected info
|
* See GMPInstallManager.installAddon for resolve/rejected info
|
||||||
*/
|
*/
|
||||||
start: function() {
|
start: function() {
|
||||||
let log = getScopedLogger("GMPDownloader.start");
|
let log = getScopedLogger("GMPDownloader");
|
||||||
this._deferred = Promise.defer();
|
let gmpAddon = this._gmpAddon;
|
||||||
if (!this._gmpAddon.isValid) {
|
|
||||||
|
if (!gmpAddon.isValid) {
|
||||||
log.info("gmpAddon is not valid, will not continue");
|
log.info("gmpAddon is not valid, will not continue");
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
target: this,
|
target: this,
|
||||||
@ -697,55 +484,14 @@ GMPDownloader.prototype = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let uri = Services.io.newURI(this._gmpAddon.URL, null, null);
|
return ProductAddonChecker.downloadAddon(gmpAddon).then((zipPath) => {
|
||||||
this._request = Cc["@mozilla.org/network/incremental-download;1"].
|
|
||||||
createInstance(Ci.nsIIncrementalDownload);
|
|
||||||
let gmpFile = FileUtils.getFile("TmpD", [this._gmpAddon.id + ".zip"]);
|
|
||||||
if (gmpFile.exists()) {
|
|
||||||
gmpFile.remove(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("downloading from " + uri.spec + " to " + gmpFile.path);
|
|
||||||
this._request.init(uri, gmpFile, DOWNLOAD_CHUNK_BYTES_SIZE,
|
|
||||||
DOWNLOAD_INTERVAL);
|
|
||||||
this._request.start(this, null);
|
|
||||||
return this._deferred.promise;
|
|
||||||
},
|
|
||||||
// For nsIRequestObserver
|
|
||||||
onStartRequest: function(request, context) {
|
|
||||||
},
|
|
||||||
// For nsIRequestObserver
|
|
||||||
// Called when the GMP addon zip file is downloaded
|
|
||||||
onStopRequest: function(request, context, status) {
|
|
||||||
let log = getScopedLogger("GMPDownloader.onStopRequest");
|
|
||||||
log.info("onStopRequest called");
|
|
||||||
if (!Components.isSuccessCode(status)) {
|
|
||||||
log.info("status failed: " + status);
|
|
||||||
this._deferred.reject({
|
|
||||||
target: this,
|
|
||||||
status: status,
|
|
||||||
type: "downloaderr"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let promise = this._verifyDownload();
|
|
||||||
promise.then(() => {
|
|
||||||
log.info("GMP file is ready to unzip");
|
|
||||||
let destination = this._request.destination;
|
|
||||||
|
|
||||||
let zipPath = destination.path;
|
|
||||||
let gmpAddon = this._gmpAddon;
|
|
||||||
let installToDirPath = Cc["@mozilla.org/file/local;1"].
|
|
||||||
createInstance(Ci.nsIFile);
|
|
||||||
let path = OS.Path.join(OS.Constants.Path.profileDir,
|
let path = OS.Path.join(OS.Constants.Path.profileDir,
|
||||||
gmpAddon.id,
|
gmpAddon.id,
|
||||||
gmpAddon.version);
|
gmpAddon.version);
|
||||||
installToDirPath.initWithPath(path);
|
log.info("install to directory path: " + path);
|
||||||
log.info("install to directory path: " + installToDirPath.path);
|
let gmpInstaller = new GMPExtractor(zipPath, path);
|
||||||
let gmpInstaller = new GMPExtractor(zipPath, installToDirPath.path);
|
|
||||||
let installPromise = gmpInstaller.install();
|
let installPromise = gmpInstaller.install();
|
||||||
installPromise.then(extractedPaths => {
|
return installPromise.then(extractedPaths => {
|
||||||
// Success, set the prefs
|
// Success, set the prefs
|
||||||
let now = Math.round(Date.now() / 1000);
|
let now = Math.round(Date.now() / 1000);
|
||||||
GMPPrefs.set(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, now, gmpAddon.id);
|
GMPPrefs.set(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, now, gmpAddon.id);
|
||||||
@ -761,73 +507,8 @@ GMPDownloader.prototype = {
|
|||||||
// if you need to set other prefs etc. do it before this.
|
// if you need to set other prefs etc. do it before this.
|
||||||
GMPPrefs.set(GMPPrefs.KEY_PLUGIN_VERSION, gmpAddon.version,
|
GMPPrefs.set(GMPPrefs.KEY_PLUGIN_VERSION, gmpAddon.version,
|
||||||
gmpAddon.id);
|
gmpAddon.id);
|
||||||
this._deferred.resolve(extractedPaths);
|
return extractedPaths;
|
||||||
}, err => {
|
|
||||||
this._deferred.reject(err);
|
|
||||||
});
|
|
||||||
}, err => {
|
|
||||||
log.warn("verifyDownload check failed");
|
|
||||||
this._deferred.reject({
|
|
||||||
target: this,
|
|
||||||
status: 200,
|
|
||||||
type: "verifyerr"
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
* Verifies that the downloaded zip file's hash matches the GMPAddon hash.
|
|
||||||
* @return a promise which resolves if the download verifies
|
|
||||||
*/
|
|
||||||
_verifyDownload: function() {
|
|
||||||
let verifyDownloadDeferred = Promise.defer();
|
|
||||||
let log = getScopedLogger("GMPDownloader._verifyDownload");
|
|
||||||
log.info("_verifyDownload called");
|
|
||||||
if (!this._request) {
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
|
|
||||||
let destination = this._request.destination;
|
|
||||||
log.info("for path: " + destination.path);
|
|
||||||
|
|
||||||
// Ensure that the file size matches the expected file size.
|
|
||||||
if (this._gmpAddon.size !== undefined &&
|
|
||||||
destination.fileSize != this._gmpAddon.size) {
|
|
||||||
log.warn("Downloader:_verifyDownload downloaded size " +
|
|
||||||
destination.fileSize + " != expected size " +
|
|
||||||
this._gmpAddon.size + ".");
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
|
|
||||||
let promise = GMPDownloader.computeHash(this._gmpAddon.hashFunction, destination);
|
|
||||||
promise.then(digest => {
|
|
||||||
let expectedDigest = this._gmpAddon.hashValue.toLowerCase();
|
|
||||||
if (digest !== expectedDigest) {
|
|
||||||
log.warn("hashes do not match! Got: `" +
|
|
||||||
digest + "`, expected: `" + expectedDigest + "`");
|
|
||||||
this._deferred.reject();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("hashes match!");
|
|
||||||
verifyDownloadDeferred.resolve();
|
|
||||||
}, err => {
|
|
||||||
verifyDownloadDeferred.reject();
|
|
||||||
});
|
|
||||||
return verifyDownloadDeferred.promise;
|
|
||||||
},
|
|
||||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver])
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a string containing binary values to hex.
|
|
||||||
*/
|
|
||||||
function binaryToHex(input) {
|
|
||||||
let result = "";
|
|
||||||
for (let i = 0; i < input.length; ++i) {
|
|
||||||
let hex = input.charCodeAt(i).toString(16);
|
|
||||||
if (hex.length == 1)
|
|
||||||
hex = "0" + hex;
|
|
||||||
result += hex;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
@ -13,6 +13,8 @@ Cu.import("resource://gre/modules/Promise.jsm");
|
|||||||
Cu.import("resource://gre/modules/Preferences.jsm")
|
Cu.import("resource://gre/modules/Preferences.jsm")
|
||||||
Cu.import("resource://gre/modules/UpdateUtils.jsm");
|
Cu.import("resource://gre/modules/UpdateUtils.jsm");
|
||||||
|
|
||||||
|
let { computeHash } = Cu.import("resource://gre/modules/addons/ProductAddonChecker.jsm");
|
||||||
|
|
||||||
do_get_profile();
|
do_get_profile();
|
||||||
|
|
||||||
function run_test() {Cu.import("resource://gre/modules/Preferences.jsm")
|
function run_test() {Cu.import("resource://gre/modules/Preferences.jsm")
|
||||||
@ -431,7 +433,7 @@ function* test_checkForAddons_installAddon(id, includeSize, wantInstallReject) {
|
|||||||
let data = "e~=0.5772156649";
|
let data = "e~=0.5772156649";
|
||||||
let zipFile = createNewZipFile(zipFileName, data);
|
let zipFile = createNewZipFile(zipFileName, data);
|
||||||
let hashFunc = "sha256";
|
let hashFunc = "sha256";
|
||||||
let expectedDigest = yield GMPDownloader.computeHash(hashFunc, zipFile);
|
let expectedDigest = yield computeHash(hashFunc, zipFile.path);
|
||||||
let fileSize = zipFile.fileSize;
|
let fileSize = zipFile.fileSize;
|
||||||
if (wantInstallReject) {
|
if (wantInstallReject) {
|
||||||
fileSize = 1;
|
fileSize = 1;
|
||||||
@ -457,7 +459,6 @@ function* test_checkForAddons_installAddon(id, includeSize, wantInstallReject) {
|
|||||||
let gmpAddon = gmpAddons[0];
|
let gmpAddon = gmpAddons[0];
|
||||||
do_check_false(gmpAddon.isInstalled);
|
do_check_false(gmpAddon.isInstalled);
|
||||||
|
|
||||||
GMPInstallManager.overrideLeaveDownloadedZip = true;
|
|
||||||
try {
|
try {
|
||||||
let extractedPaths = yield installManager.installAddon(gmpAddon);
|
let extractedPaths = yield installManager.installAddon(gmpAddon);
|
||||||
if (wantInstallReject) {
|
if (wantInstallReject) {
|
||||||
@ -475,14 +476,6 @@ function* test_checkForAddons_installAddon(id, includeSize, wantInstallReject) {
|
|||||||
let readData = readStringFromFile(extractedFile);
|
let readData = readStringFromFile(extractedFile);
|
||||||
do_check_eq(readData, data);
|
do_check_eq(readData, data);
|
||||||
|
|
||||||
// Check that the downloaded zip matches the offered zip exactly
|
|
||||||
let downloadedGMPFile = FileUtils.getFile("TmpD",
|
|
||||||
[gmpAddon.id + ".zip"]);
|
|
||||||
do_check_true(downloadedGMPFile.exists());
|
|
||||||
let downloadedBytes = getBinaryFileData(downloadedGMPFile);
|
|
||||||
let sourceBytes = getBinaryFileData(zipFile);
|
|
||||||
do_check_true(compareBinaryData(downloadedBytes, sourceBytes));
|
|
||||||
|
|
||||||
// Make sure the prefs are set correctly
|
// Make sure the prefs are set correctly
|
||||||
do_check_true(!!GMPScope.GMPPrefs.get(
|
do_check_true(!!GMPScope.GMPPrefs.get(
|
||||||
GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "", gmpAddon.id));
|
GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "", gmpAddon.id));
|
||||||
@ -499,16 +492,9 @@ function* test_checkForAddons_installAddon(id, includeSize, wantInstallReject) {
|
|||||||
extractedFile.parent.remove(true);
|
extractedFile.parent.remove(true);
|
||||||
zipFile.remove(false);
|
zipFile.remove(false);
|
||||||
httpServer.stop(function() {});
|
httpServer.stop(function() {});
|
||||||
do_print("Removing downloaded GMP file: " + downloadedGMPFile.path);
|
|
||||||
downloadedGMPFile.remove(false);
|
|
||||||
installManager.uninit();
|
installManager.uninit();
|
||||||
} catch(ex) {
|
} catch(ex) {
|
||||||
zipFile.remove(false);
|
zipFile.remove(false);
|
||||||
let downloadedGMPFile = FileUtils.getFile("TmpD",
|
|
||||||
[gmpAddon.id + ".zip"]);
|
|
||||||
do_print("Removing downloaded GMP file from exception handler: " +
|
|
||||||
downloadedGMPFile.path);
|
|
||||||
downloadedGMPFile.remove(false);
|
|
||||||
if (!wantInstallReject) {
|
if (!wantInstallReject) {
|
||||||
do_throw("install update should not reject");
|
do_throw("install update should not reject");
|
||||||
}
|
}
|
||||||
@ -799,45 +785,6 @@ function overrideXHR(status, response, options) {
|
|||||||
return overrideXHR.myxhr;
|
return overrideXHR.myxhr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Compares binary data of 2 arrays and returns true if they are the same
|
|
||||||
*
|
|
||||||
* @param arr1 The first array to compare
|
|
||||||
* @param arr2 The second array to compare
|
|
||||||
*/
|
|
||||||
function compareBinaryData(arr1, arr2) {
|
|
||||||
do_check_eq(arr1.length, arr2.length);
|
|
||||||
for (let i = 0; i < arr1.length; i++) {
|
|
||||||
if (arr1[i] != arr2[i]) {
|
|
||||||
do_print("Data differs at index " + i +
|
|
||||||
", arr1: " + arr1[i] + ", arr2: " + arr2[i]);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads a file's data and returns it
|
|
||||||
*
|
|
||||||
* @param file The file to read the data from
|
|
||||||
* @return array of bytes for the data in the file.
|
|
||||||
*/
|
|
||||||
function getBinaryFileData(file) {
|
|
||||||
let fileStream = Cc["@mozilla.org/network/file-input-stream;1"].
|
|
||||||
createInstance(Ci.nsIFileInputStream);
|
|
||||||
// Open as RD_ONLY with default permissions.
|
|
||||||
fileStream.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
|
|
||||||
|
|
||||||
// Check the returned size versus the expected size.
|
|
||||||
let stream = Cc["@mozilla.org/binaryinputstream;1"].
|
|
||||||
createInstance(Ci.nsIBinaryInputStream);
|
|
||||||
stream.setInputStream(fileStream);
|
|
||||||
let bytes = stream.readByteArray(stream.available());
|
|
||||||
fileStream.close();
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new zip file containing a file with the specified data
|
* Creates a new zip file containing a file with the specified data
|
||||||
* @param zipName The name of the zip file
|
* @param zipName The name of the zip file
|
||||||
|
322
toolkit/mozapps/extensions/internal/ProductAddonChecker.jsm
Normal file
322
toolkit/mozapps/extensions/internal/ProductAddonChecker.jsm
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = [ "ProductAddonChecker" ];
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/Task.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Log.jsm");
|
||||||
|
Cu.import("resource://gre/modules/CertUtils.jsm");
|
||||||
|
Cu.import("resource://gre/modules/FileUtils.jsm");
|
||||||
|
Cu.import("resource://gre/modules/NetUtil.jsm");
|
||||||
|
Cu.import("resource://gre/modules/osfile.jsm");
|
||||||
|
|
||||||
|
let logger = Log.repository.getLogger("addons.productaddons");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of milliseconds after which we need to cancel `downloadXML`.
|
||||||
|
*
|
||||||
|
* Bug 1087674 suggests that the XHR we use in `downloadXML` may
|
||||||
|
* never terminate in presence of network nuisances (e.g. strange
|
||||||
|
* antivirus behavior). This timeout is a defensive measure to ensure
|
||||||
|
* that we fail cleanly in such case.
|
||||||
|
*/
|
||||||
|
const TIMEOUT_DELAY_MS = 20000;
|
||||||
|
// Chunk size for the incremental downloader
|
||||||
|
const DOWNLOAD_CHUNK_BYTES_SIZE = 300000;
|
||||||
|
// Incremental downloader interval
|
||||||
|
const DOWNLOAD_INTERVAL = 0;
|
||||||
|
// How much of a file to read into memory at a time for hashing
|
||||||
|
const HASH_CHUNK_SIZE = 8192;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the status of an XMLHttpRequest either directly or from its underlying
|
||||||
|
* channel.
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* The XMLHttpRequest.
|
||||||
|
* @return an integer status value.
|
||||||
|
*/
|
||||||
|
function getRequestStatus(request) {
|
||||||
|
let status = null;
|
||||||
|
try {
|
||||||
|
status = request.status;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status != null) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
return request.channel.QueryInterface(Ci.nsIRequest).status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads an XML document from a URL optionally testing the SSL certificate
|
||||||
|
* for certain attributes.
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* The url to download from.
|
||||||
|
* @param allowNonBuiltIn
|
||||||
|
* Whether to trust SSL certificates without a built-in CA issuer.
|
||||||
|
* @param allowedCerts
|
||||||
|
* The list of certificate attributes to match the SSL certificate
|
||||||
|
* against or null to skip checks.
|
||||||
|
* @return a promise that resolves to the DOM document downloaded or rejects
|
||||||
|
* with a JS exception in case of error.
|
||||||
|
*/
|
||||||
|
function downloadXML(url, allowNonBuiltIn = false, allowedCerts = null) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
|
||||||
|
createInstance(Ci.nsISupports);
|
||||||
|
// This is here to let unit test code override XHR
|
||||||
|
if (request.wrappedJSObject) {
|
||||||
|
request = request.wrappedJSObject;
|
||||||
|
}
|
||||||
|
request.open("GET", url, true);
|
||||||
|
request.channel.notificationCallbacks = new BadCertHandler(allowNonBuiltIn);
|
||||||
|
// Prevent the request from reading from the cache.
|
||||||
|
request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
|
||||||
|
// Prevent the request from writing to the cache.
|
||||||
|
request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
|
||||||
|
request.timeout = TIMEOUT_DELAY_MS;
|
||||||
|
|
||||||
|
request.overrideMimeType("text/xml");
|
||||||
|
// The Cache-Control header is only interpreted by proxies and the
|
||||||
|
// final destination. It does not help if a resource is already
|
||||||
|
// cached locally.
|
||||||
|
request.setRequestHeader("Cache-Control", "no-cache");
|
||||||
|
// HTTP/1.0 servers might not implement Cache-Control and
|
||||||
|
// might only implement Pragma: no-cache
|
||||||
|
request.setRequestHeader("Pragma", "no-cache");
|
||||||
|
|
||||||
|
let fail = (event) => {
|
||||||
|
let request = event.target;
|
||||||
|
let status = getRequestStatus(request);
|
||||||
|
let message = "Failed downloading XML, status: " + status + ", reason: " + event.type;
|
||||||
|
logger.warn(message);
|
||||||
|
let ex = new Error(message);
|
||||||
|
ex.status = status;
|
||||||
|
reject(ex);
|
||||||
|
};
|
||||||
|
|
||||||
|
let success = (event) => {
|
||||||
|
logger.info("Completed downloading document");
|
||||||
|
let request = event.target;
|
||||||
|
|
||||||
|
try {
|
||||||
|
checkCert(request.channel, allowNonBuiltIn, allowedCerts);
|
||||||
|
} catch (ex) {
|
||||||
|
logger.error("Request failed certificate checks: " + ex);
|
||||||
|
ex.status = getRequestStatus(request);
|
||||||
|
reject(ex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(request.responseXML);
|
||||||
|
};
|
||||||
|
|
||||||
|
request.addEventListener("error", fail, false);
|
||||||
|
request.addEventListener("abort", fail, false);
|
||||||
|
request.addEventListener("timeout", fail, false);
|
||||||
|
request.addEventListener("load", success, false);
|
||||||
|
|
||||||
|
logger.info("sending request to: " + url);
|
||||||
|
request.send(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a list of add-ons from a DOM document.
|
||||||
|
*
|
||||||
|
* @param document
|
||||||
|
* The DOM document to parse.
|
||||||
|
* @return null if there is no <addons> element otherwise an array of the addons
|
||||||
|
* listed.
|
||||||
|
*/
|
||||||
|
function parseXML(document) {
|
||||||
|
// Check that the root element is correct
|
||||||
|
if (document.documentElement.localName != "updates") {
|
||||||
|
throw new Error("got node name: " + document.documentElement.localName +
|
||||||
|
", expected: updates");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are any addons elements in the updates element
|
||||||
|
let addons = document.querySelector("updates:root > addons");
|
||||||
|
if (!addons) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let results = [];
|
||||||
|
let addonList = document.querySelectorAll("updates:root > addons > addon");
|
||||||
|
for (let addonElement of addonList) {
|
||||||
|
let addon = {};
|
||||||
|
|
||||||
|
for (let name of ["id", "URL", "hashFunction", "hashValue", "version", "size"]) {
|
||||||
|
if (addonElement.hasAttribute(name)) {
|
||||||
|
addon[name] = addonElement.getAttribute(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addon.size = Number(addon.size) || undefined;
|
||||||
|
|
||||||
|
results.push(addon);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads file from a URL using the incremental file downloader.
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* The url to download from.
|
||||||
|
* @return a promise that resolves to the path of a temporary file or rejects
|
||||||
|
* with a JS exception in case of error.
|
||||||
|
*/
|
||||||
|
function downloadFile(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let observer = {
|
||||||
|
onStartRequest: function() {},
|
||||||
|
|
||||||
|
onStopRequest: function(request, context, status) {
|
||||||
|
if (!Components.isSuccessCode(status)) {
|
||||||
|
logger.warn("File download failed: 0x" + status.toString(16));
|
||||||
|
tmpFile.remove(true);
|
||||||
|
reject(Components.Exception("File download failed", status));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(tmpFile.path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let uri = NetUtil.newURI(url);
|
||||||
|
let request = Cc["@mozilla.org/network/incremental-download;1"].
|
||||||
|
createInstance(Ci.nsIIncrementalDownload);
|
||||||
|
let tmpFile = FileUtils.getFile("TmpD", ["tmpaddon"]);
|
||||||
|
tmpFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
|
||||||
|
|
||||||
|
logger.info("Downloading from " + uri.spec + " to " + tmpFile.path);
|
||||||
|
request.init(uri, tmpFile, DOWNLOAD_CHUNK_BYTES_SIZE, DOWNLOAD_INTERVAL);
|
||||||
|
request.start(observer, null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a string containing binary values to hex.
|
||||||
|
*/
|
||||||
|
function binaryToHex(input) {
|
||||||
|
let result = "";
|
||||||
|
for (let i = 0; i < input.length; ++i) {
|
||||||
|
let hex = input.charCodeAt(i).toString(16);
|
||||||
|
if (hex.length == 1) {
|
||||||
|
hex = "0" + hex;
|
||||||
|
}
|
||||||
|
result += hex;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the hash of a file.
|
||||||
|
*
|
||||||
|
* @param hashFunction
|
||||||
|
* The type of hash function to use, must be supported by nsICryptoHash.
|
||||||
|
* @param path
|
||||||
|
* The path of the file to hash.
|
||||||
|
* @return a promise that resolves to hash of the file or rejects with a JS
|
||||||
|
* exception in case of error.
|
||||||
|
*/
|
||||||
|
let computeHash = Task.async(function*(hashFunction, path) {
|
||||||
|
let file = yield OS.File.open(path, { existing: true, read: true });
|
||||||
|
try {
|
||||||
|
let hasher = Cc["@mozilla.org/security/hash;1"].
|
||||||
|
createInstance(Ci.nsICryptoHash);
|
||||||
|
hasher.initWithString(hashFunction);
|
||||||
|
|
||||||
|
let bytes;
|
||||||
|
do {
|
||||||
|
bytes = yield file.read(HASH_CHUNK_SIZE);
|
||||||
|
hasher.update(bytes, bytes.length);
|
||||||
|
} while (bytes.length == HASH_CHUNK_SIZE);
|
||||||
|
|
||||||
|
return binaryToHex(hasher.finish(false));
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
yield file.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that a downloaded file matches what was expected.
|
||||||
|
*
|
||||||
|
* @param properties
|
||||||
|
* The properties to check, `size` and `hashFunction` with `hashValue`
|
||||||
|
* are supported. Any properties missing won't be checked.
|
||||||
|
* @param path
|
||||||
|
* The path of the file to check.
|
||||||
|
* @return a promise that resolves if the file matched or rejects with a JS
|
||||||
|
* exception in case of error.
|
||||||
|
*/
|
||||||
|
let verifyFile = Task.async(function*(properties, path) {
|
||||||
|
if (properties.size !== undefined) {
|
||||||
|
let stat = yield OS.File.stat(path);
|
||||||
|
if (stat.size != properties.size) {
|
||||||
|
throw new Error("Downloaded file was " + stat.size + " bytes but expected " + properties.size + " bytes.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (properties.hashFunction !== undefined) {
|
||||||
|
let expectedDigest = properties.hashValue.toLowerCase();
|
||||||
|
let digest = yield computeHash(properties.hashFunction, path);
|
||||||
|
if (digest != expectedDigest) {
|
||||||
|
throw new Error("Hash was `" + digest + "` but expected `" + expectedDigest + "`.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const ProductAddonChecker = {
|
||||||
|
/**
|
||||||
|
* Downloads a list of add-ons from a URL optionally testing the SSL
|
||||||
|
* certificate for certain attributes.
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* The url to download from.
|
||||||
|
* @param allowNonBuiltIn
|
||||||
|
* Whether to trust SSL certificates without a built-in CA issuer.
|
||||||
|
* @param allowedCerts
|
||||||
|
* The list of certificate attributes to match the SSL certificate
|
||||||
|
* against or null to skip checks.
|
||||||
|
* @return a promise that resolves to the list of add-ons or rejects with a JS
|
||||||
|
* exception in case of error.
|
||||||
|
*/
|
||||||
|
getProductAddonList: function(url, allowNonBuiltIn = false, allowedCerts = null) {
|
||||||
|
return downloadXML(url, allowNonBuiltIn, allowedCerts).then(parseXML);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads an add-on to a local file and checks that it matches the expected
|
||||||
|
* file. The caller is responsible for deleting the temporary file returned.
|
||||||
|
*
|
||||||
|
* @param addon
|
||||||
|
* The addon to download.
|
||||||
|
* @return a promise that resolves to the temporary file downloaded or rejects
|
||||||
|
* with a JS exception in case of error.
|
||||||
|
*/
|
||||||
|
downloadAddon: Task.async(function*(addon) {
|
||||||
|
let path = yield downloadFile(addon.URL);
|
||||||
|
try {
|
||||||
|
yield verifyFile(addon, path);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
yield OS.File.remove(path);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -12,6 +12,7 @@ EXTRA_JS_MODULES.addons += [
|
|||||||
'Content.js',
|
'Content.js',
|
||||||
'GMPProvider.jsm',
|
'GMPProvider.jsm',
|
||||||
'LightweightThemeImageOptimizer.jsm',
|
'LightweightThemeImageOptimizer.jsm',
|
||||||
|
'ProductAddonChecker.jsm',
|
||||||
'SpellCheckDictionaryBootstrap.js',
|
'SpellCheckDictionaryBootstrap.js',
|
||||||
'WebExtensionBootstrap.js',
|
'WebExtensionBootstrap.js',
|
||||||
]
|
]
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
Not an xml file!
|
@ -0,0 +1,3 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<foobar></barfoo>
|
@ -0,0 +1,3 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<test></test>
|
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<updates>
|
||||||
|
<addons></addons>
|
||||||
|
</updates>
|
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<updates>
|
||||||
|
<addons>
|
||||||
|
<addon id="test1" URL="http://example.com/test1.xpi"/>
|
||||||
|
<addon id="test2" URL="http://example.com/test2.xpi" hashFunction="md5" hashValue="djhfgsjdhf"/>
|
||||||
|
<addon id="test3" URL="http://example.com/test3.xpi" version="1.0" size="45"/>
|
||||||
|
<addon id="test4"/>
|
||||||
|
<addon URL="http://example.com/test5.xpi"/>
|
||||||
|
</addons>
|
||||||
|
</updates>
|
@ -0,0 +1,3 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<updates></updates>
|
Binary file not shown.
@ -0,0 +1,264 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
Components.utils.import("resource://gre/modules/addons/ProductAddonChecker.jsm");
|
||||||
|
Components.utils.import("resource://testing-common/httpd.js");
|
||||||
|
Components.utils.import("resource://gre/modules/osfile.jsm");
|
||||||
|
|
||||||
|
const LocalFile = new Components.Constructor("@mozilla.org/file/local;1", AM_Ci.nsIFile, "initWithPath");
|
||||||
|
|
||||||
|
let testserver = new HttpServer();
|
||||||
|
testserver.registerDirectory("/data/", do_get_file("data/productaddons"));
|
||||||
|
testserver.start();
|
||||||
|
let root = testserver.identity.primaryScheme + "://" +
|
||||||
|
testserver.identity.primaryHost + ":" +
|
||||||
|
testserver.identity.primaryPort + "/data/"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares binary data of 2 arrays and returns true if they are the same
|
||||||
|
*
|
||||||
|
* @param arr1 The first array to compare
|
||||||
|
* @param arr2 The second array to compare
|
||||||
|
*/
|
||||||
|
function compareBinaryData(arr1, arr2) {
|
||||||
|
do_check_eq(arr1.length, arr2.length);
|
||||||
|
for (let i = 0; i < arr1.length; i++) {
|
||||||
|
if (arr1[i] != arr2[i]) {
|
||||||
|
do_print("Data differs at index " + i +
|
||||||
|
", arr1: " + arr1[i] + ", arr2: " + arr2[i]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a file's data and returns it
|
||||||
|
*
|
||||||
|
* @param file The file to read the data from
|
||||||
|
* @return array of bytes for the data in the file.
|
||||||
|
*/
|
||||||
|
function getBinaryFileData(file) {
|
||||||
|
let fileStream = AM_Cc["@mozilla.org/network/file-input-stream;1"].
|
||||||
|
createInstance(AM_Ci.nsIFileInputStream);
|
||||||
|
// Open as RD_ONLY with default permissions.
|
||||||
|
fileStream.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
|
||||||
|
|
||||||
|
let stream = AM_Cc["@mozilla.org/binaryinputstream;1"].
|
||||||
|
createInstance(AM_Ci.nsIBinaryInputStream);
|
||||||
|
stream.setInputStream(fileStream);
|
||||||
|
let bytes = stream.readByteArray(stream.available());
|
||||||
|
fileStream.close();
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares binary data of 2 files and returns true if they are the same
|
||||||
|
*
|
||||||
|
* @param file1 The first file to compare
|
||||||
|
* @param file2 The second file to compare
|
||||||
|
*/
|
||||||
|
function compareFiles(file1, file2) {
|
||||||
|
return compareBinaryData(getBinaryFileData(file1), getBinaryFileData(file2));
|
||||||
|
}
|
||||||
|
|
||||||
|
add_task(function* test_404() {
|
||||||
|
try {
|
||||||
|
let addons = yield ProductAddonChecker.getProductAddonList(root + "404.xml");
|
||||||
|
do_throw("Should not have returned anything");
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
do_check_true(true, "Expected to throw for a missing update file");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(function* test_not_xml() {
|
||||||
|
try {
|
||||||
|
let addons = yield ProductAddonChecker.getProductAddonList(root + "bad.txt");
|
||||||
|
do_throw("Should not have returned anything");
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
do_check_true(true, "Expected to throw for a non XML result");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(function* test_invalid_xml() {
|
||||||
|
try {
|
||||||
|
let addons = yield ProductAddonChecker.getProductAddonList(root + "bad.xml");
|
||||||
|
do_throw("Should not have returned anything");
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
do_check_true(true, "Expected to throw for invalid XML");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(function* test_wrong_xml() {
|
||||||
|
try {
|
||||||
|
let addons = yield ProductAddonChecker.getProductAddonList(root + "bad2.xml");
|
||||||
|
do_throw("Should not have returned anything");
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
do_check_true(true, "Expected to throw for a missing <updates> tag");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(function* test_missing() {
|
||||||
|
let addons = yield ProductAddonChecker.getProductAddonList(root + "missing.xml");
|
||||||
|
do_check_eq(addons, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(function* test_empty() {
|
||||||
|
let addons = yield ProductAddonChecker.getProductAddonList(root + "empty.xml");
|
||||||
|
do_check_true(Array.isArray(addons));
|
||||||
|
do_check_eq(addons.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(function* test_good_xml() {
|
||||||
|
let addons = yield ProductAddonChecker.getProductAddonList(root + "good.xml");
|
||||||
|
do_check_true(Array.isArray(addons));
|
||||||
|
|
||||||
|
// There are three valid entries in the XML
|
||||||
|
do_check_eq(addons.length, 5);
|
||||||
|
|
||||||
|
let addon = addons[0];
|
||||||
|
do_check_eq(addon.id, "test1");
|
||||||
|
do_check_eq(addon.URL, "http://example.com/test1.xpi");
|
||||||
|
do_check_eq(addon.hashFunction, undefined);
|
||||||
|
do_check_eq(addon.hashValue, undefined);
|
||||||
|
do_check_eq(addon.version, undefined);
|
||||||
|
do_check_eq(addon.size, undefined);
|
||||||
|
|
||||||
|
addon = addons[1];
|
||||||
|
do_check_eq(addon.id, "test2");
|
||||||
|
do_check_eq(addon.URL, "http://example.com/test2.xpi");
|
||||||
|
do_check_eq(addon.hashFunction, "md5");
|
||||||
|
do_check_eq(addon.hashValue, "djhfgsjdhf");
|
||||||
|
do_check_eq(addon.version, undefined);
|
||||||
|
do_check_eq(addon.size, undefined);
|
||||||
|
|
||||||
|
addon = addons[2];
|
||||||
|
do_check_eq(addon.id, "test3");
|
||||||
|
do_check_eq(addon.URL, "http://example.com/test3.xpi");
|
||||||
|
do_check_eq(addon.hashFunction, undefined);
|
||||||
|
do_check_eq(addon.hashValue, undefined);
|
||||||
|
do_check_eq(addon.version, "1.0");
|
||||||
|
do_check_eq(addon.size, 45);
|
||||||
|
|
||||||
|
addon = addons[3];
|
||||||
|
do_check_eq(addon.id, "test4");
|
||||||
|
do_check_eq(addon.URL, undefined);
|
||||||
|
do_check_eq(addon.hashFunction, undefined);
|
||||||
|
do_check_eq(addon.hashValue, undefined);
|
||||||
|
do_check_eq(addon.version, undefined);
|
||||||
|
do_check_eq(addon.size, undefined);
|
||||||
|
|
||||||
|
addon = addons[4];
|
||||||
|
do_check_eq(addon.id, undefined);
|
||||||
|
do_check_eq(addon.URL, "http://example.com/test5.xpi");
|
||||||
|
do_check_eq(addon.hashFunction, undefined);
|
||||||
|
do_check_eq(addon.hashValue, undefined);
|
||||||
|
do_check_eq(addon.version, undefined);
|
||||||
|
do_check_eq(addon.size, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(function* test_download_nourl() {
|
||||||
|
try {
|
||||||
|
let path = yield ProductAddonChecker.downloadAddon({});
|
||||||
|
|
||||||
|
yield OS.File.remove(path);
|
||||||
|
do_throw("Should not have downloaded a file with a missing url");
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
do_check_true(true, "Should have thrown when downloading a file with a missing url.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(function* test_download_missing() {
|
||||||
|
try {
|
||||||
|
let path = yield ProductAddonChecker.downloadAddon({
|
||||||
|
URL: root + "nofile.xpi",
|
||||||
|
});
|
||||||
|
|
||||||
|
yield OS.File.remove(path);
|
||||||
|
do_throw("Should not have downloaded a missing file");
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
do_check_true(true, "Should have thrown when downloading a missing file.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(function* test_download_noverify() {
|
||||||
|
let path = yield ProductAddonChecker.downloadAddon({
|
||||||
|
URL: root + "unsigned.xpi",
|
||||||
|
});
|
||||||
|
|
||||||
|
let stat = yield OS.File.stat(path);
|
||||||
|
do_check_false(stat.isDir);
|
||||||
|
do_check_eq(stat.size, 452)
|
||||||
|
|
||||||
|
do_check_true(compareFiles(do_get_file("data/productaddons/unsigned.xpi"), new LocalFile(path)));
|
||||||
|
|
||||||
|
yield OS.File.remove(path);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(function* test_download_badsize() {
|
||||||
|
try {
|
||||||
|
let path = yield ProductAddonChecker.downloadAddon({
|
||||||
|
URL: root + "unsigned.xpi",
|
||||||
|
size: 400,
|
||||||
|
});
|
||||||
|
|
||||||
|
yield OS.File.remove(path);
|
||||||
|
do_throw("Should not have downloaded a file with a bad size");
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
do_check_true(true, "Should have thrown when downloading a file with a bad size.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(function* test_download_badhashfn() {
|
||||||
|
try {
|
||||||
|
let path = yield ProductAddonChecker.downloadAddon({
|
||||||
|
URL: root + "unsigned.xpi",
|
||||||
|
hashFunction: "sha2567",
|
||||||
|
hashValue: "9b9abf7ddfc1a6d7ffc7e0247481dcc202363e4445ad3494fb22036f1698c7f3",
|
||||||
|
});
|
||||||
|
|
||||||
|
yield OS.File.remove(path);
|
||||||
|
do_throw("Should not have downloaded a file with a bad hash function");
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
do_check_true(true, "Should have thrown when downloading a file with a bad hash function.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(function* test_download_badhash() {
|
||||||
|
try {
|
||||||
|
let path = yield ProductAddonChecker.downloadAddon({
|
||||||
|
URL: root + "unsigned.xpi",
|
||||||
|
hashFunction: "sha256",
|
||||||
|
hashValue: "8b9abf7ddfc1a6d7ffc7e0247481dcc202363e4445ad3494fb22036f1698c7f3",
|
||||||
|
});
|
||||||
|
|
||||||
|
yield OS.File.remove(path);
|
||||||
|
do_throw("Should not have downloaded a file with a bad hash");
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
do_check_true(true, "Should have thrown when downloading a file with a bad hash.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(function* test_download_works() {
|
||||||
|
let path = yield ProductAddonChecker.downloadAddon({
|
||||||
|
URL: root + "unsigned.xpi",
|
||||||
|
size: 452,
|
||||||
|
hashFunction: "sha256",
|
||||||
|
hashValue: "9b9abf7ddfc1a6d7ffc7e0247481dcc202363e4445ad3494fb22036f1698c7f3",
|
||||||
|
});
|
||||||
|
|
||||||
|
let stat = yield OS.File.stat(path);
|
||||||
|
do_check_false(stat.isDir);
|
||||||
|
|
||||||
|
do_check_true(compareFiles(do_get_file("data/productaddons/unsigned.xpi"), new LocalFile(path)));
|
||||||
|
|
||||||
|
yield OS.File.remove(path);
|
||||||
|
});
|
@ -23,6 +23,7 @@ skip-if = appname != "firefox"
|
|||||||
[test_provider_shutdown.js]
|
[test_provider_shutdown.js]
|
||||||
[test_provider_unsafe_access_shutdown.js]
|
[test_provider_unsafe_access_shutdown.js]
|
||||||
[test_provider_unsafe_access_startup.js]
|
[test_provider_unsafe_access_startup.js]
|
||||||
|
[test_ProductAddonChecker.js]
|
||||||
[test_shutdown.js]
|
[test_shutdown.js]
|
||||||
[test_system_reset.js]
|
[test_system_reset.js]
|
||||||
[test_XPIcancel.js]
|
[test_XPIcancel.js]
|
||||||
|
Loading…
Reference in New Issue
Block a user