Bug 593535: Allow restarting extension downloads after failures. r=robstrong, a=blocks-betaN

This commit is contained in:
Dave Townsend 2010-11-09 18:52:06 -08:00
parent d1c59140ef
commit be8fb0186c
8 changed files with 441 additions and 45 deletions

View File

@ -4670,6 +4670,16 @@ var XPIDatabase = {
}
};
function getHashStringForCrypto(aCrypto) {
// return the two-digit hexadecimal code for a byte
function toHexString(charCode)
("0" + charCode.toString(16)).slice(-2);
// convert the binary hash data to a hex string.
let binary = aCrypto.finish(false);
return [toHexString(binary.charCodeAt(i)) for (i in binary)].join("").toLowerCase()
}
/**
* Instantiates an AddonInstall and passes the new object to a callback when
* it is complete.
@ -4707,7 +4717,14 @@ function AddonInstall(aCallback, aInstallLocation, aUrl, aHash, aName, aType,
this.installLocation = aInstallLocation;
this.sourceURI = aUrl;
this.releaseNotesURI = aReleaseNotesURI;
this.hash = aHash;
if (aHash) {
let hashSplit = aHash.toLowerCase().split(":");
this.originalHash = {
algorithm: hashSplit[0],
data: hashSplit[1]
};
}
this.hash = this.originalHash;
this.loadGroup = aLoadGroup;
this.listeners = [];
this.existingAddon = aExistingAddon;
@ -4736,13 +4753,25 @@ function AddonInstall(aCallback, aInstallLocation, aUrl, aHash, aName, aType,
if (this.hash) {
let crypto = Cc["@mozilla.org/security/hash;1"].
createInstance(Ci.nsICryptoHash);
try {
crypto.initWithString(this.hash.algorithm);
}
catch (e) {
WARN("Unknown hash algorithm " + this.hash.algorithm);
this.state = AddonManager.STATE_DOWNLOAD_FAILED;
this.error = AddonManager.ERROR_INCORRECT_HASH;
aCallback(this);
return;
}
let fis = Cc["@mozilla.org/network/file-input-stream;1"].
createInstance(Ci.nsIFileInputStream);
fis.init(this.file, -1, -1, false);
crypto.updateFromStream(fis, this.file.fileSize);
let hash = crypto.finish(true);
if (hash != this.hash) {
WARN("Hash mismatch");
let calculatedHash = getHashStringForCrypto(crypto);
if (calculatedHash != this.hash.data) {
WARN("File hash (" + calculatedHash + ") did not match provided hash (" +
this.hash.data + ")");
this.state = AddonManager.STATE_DOWNLOAD_FAILED;
this.error = AddonManager.ERROR_INCORRECT_HASH;
aCallback(this);
@ -4816,6 +4845,7 @@ AddonInstall.prototype = {
wrapper: null,
stream: null,
crypto: null,
originalHash: null,
hash: null,
loadGroup: null,
badCertHandler: null,
@ -4855,6 +4885,18 @@ AddonInstall.prototype = {
case AddonManager.STATE_DOWNLOADED:
this.startInstall();
break;
case AddonManager.STATE_DOWNLOAD_FAILED:
case AddonManager.STATE_INSTALL_FAILED:
case AddonManager.STATE_CANCELLED:
this.removeTemporaryFile();
this.state = AddonManager.STATE_AVAILABLE;
this.error = 0;
this.progress = 0;
this.maxProgress = -1;
this.hash = this.originalHash;
XPIProvider.installs.push(this);
this.startDownload();
break;
case AddonManager.STATE_DOWNLOADING:
case AddonManager.STATE_CHECKING:
case AddonManager.STATE_INSTALLING:
@ -5260,7 +5302,12 @@ AddonInstall.prototype = {
if (!this.hash && aOldChannel.originalURI.schemeIs("https") &&
aOldChannel instanceof Ci.nsIHttpChannel) {
try {
this.hash = aOldChannel.getResponseHeader("X-Target-Digest");
let hashStr = aOldChannel.getResponseHeader("X-Target-Digest");
let hashSplit = hashStr.toLowerCase().split(":");
this.hash = {
algorithm: hashSplit[0],
data: hashSplit[1]
};
}
catch (e) {
}
@ -5283,13 +5330,11 @@ AddonInstall.prototype = {
this.crypto = Cc["@mozilla.org/security/hash;1"].
createInstance(Ci.nsICryptoHash);
if (this.hash) {
[alg, this.hash] = this.hash.split(":", 2);
try {
this.crypto.initWithString(alg);
this.crypto.initWithString(this.hash.algorithm);
}
catch (e) {
WARN("Unknown hash algorithm " + alg);
WARN("Unknown hash algorithm " + this.hash.algorithm);
this.state = AddonManager.STATE_DOWNLOAD_FAILED;
this.error = AddonManager.ERROR_INCORRECT_HASH;
XPIProvider.removeActiveInstall(this);
@ -5347,18 +5392,13 @@ AddonInstall.prototype = {
}
}
// return the two-digit hexadecimal code for a byte
function toHexString(charCode)
("0" + charCode.toString(16)).slice(-2);
// convert the binary hash data to a hex string.
let binary = this.crypto.finish(false);
let hash = [toHexString(binary.charCodeAt(i)) for (i in binary)].join("")
let calculatedHash = getHashStringForCrypto(this.crypto);
this.crypto = null;
if (this.hash && hash.toLowerCase() != this.hash.toLowerCase()) {
if (this.hash && calculatedHash != this.hash.data) {
this.downloadFailed(AddonManager.ERROR_INCORRECT_HASH,
"Downloaded file hash (" + hash +
") did not match provided hash (" + this.hash + ")");
"Downloaded file hash (" + calculatedHash +
") did not match provided hash (" + this.hash.data + ")");
return;
}
try {
@ -5411,7 +5451,11 @@ AddonInstall.prototype = {
XPIProvider.removeActiveInstall(this);
AddonManagerPrivate.callInstallListeners("onDownloadFailed", this.listeners,
this.wrapper);
this.removeTemporaryFile();
// If the listener hasn't restarted the download then remove any temporary
// file
if (this.state == AddonManager.STATE_DOWNLOAD_FAILED)
this.removeTemporaryFile();
},
/**

View File

@ -52,6 +52,14 @@ Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/AddonManager.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
// Installation can begin from any of these states
const READY_STATES = [
AddonManager.STATE_AVAILABLE,
AddonManager.STATE_DOWNLOAD_FAILED,
AddonManager.STATE_INSTALL_FAILED,
AddonManager.STATE_CANCELLED
];
["LOG", "WARN", "ERROR"].forEach(function(aName) {
this.__defineGetter__(aName, function() {
Components.utils.import("resource://gre/modules/AddonLogging.jsm");
@ -95,7 +103,7 @@ function Installer(aWindow, aUrl, aInstalls) {
aInstall.addListener(this);
// Start downloading if it hasn't already begun
if (aInstall.state == AddonManager.STATE_AVAILABLE)
if (READY_STATES.indexOf(aInstall.state) != -1)
aInstall.install();
}, this);

View File

@ -119,6 +119,28 @@ function do_get_addon(aName) {
return do_get_file("addons/" + aName + ".xpi");
}
function do_get_addon_hash(aName, aAlgorithm) {
if (!aAlgorithm)
aAlgorithm = "sha1";
let file = do_get_addon(aName);
let crypto = AM_Cc["@mozilla.org/security/hash;1"].
createInstance(AM_Ci.nsICryptoHash);
crypto.initWithString(aAlgorithm);
let fis = AM_Cc["@mozilla.org/network/file-input-stream;1"].
createInstance(AM_Ci.nsIFileInputStream);
fis.init(file, -1, -1, false);
crypto.updateFromStream(fis, file.fileSize);
// return the two-digit hexadecimal code for a byte
function toHexString(charCode)
("0" + charCode.toString(16)).slice(-2);
let binary = crypto.finish(false);
return aAlgorithm + ":" + [toHexString(binary.charCodeAt(i)) for (i in binary)].join("")
}
/**
* Returns an extension uri spec
*

View File

@ -1287,7 +1287,185 @@ function check_test_21(aInstall) {
AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
do_check_eq(a2, null);
end_test();
run_test_22();
});
});
}
// Tests that an install can be restarted after being cancelled
function run_test_22() {
prepare_test({ }, [
"onNewInstall"
]);
let url = "http://localhost:4444/addons/test_install3.xpi";
AddonManager.getInstallForURL(url, function(aInstall) {
ensure_test_completed();
do_check_neq(aInstall, null);
do_check_eq(aInstall.state, AddonManager.STATE_AVAILABLE);
prepare_test({}, [
"onDownloadStarted",
"onDownloadEnded",
], check_test_22);
aInstall.install();
}, "application/x-xpinstall");
}
function check_test_22(aInstall) {
prepare_test({}, [
"onDownloadCancelled"
]);
aInstall.cancel();
ensure_test_completed();
prepare_test({
"addon3@tests.mozilla.org": [
"onInstalling"
]
}, [
"onDownloadStarted",
"onDownloadEnded",
"onInstallStarted",
"onInstallEnded"
], finish_test_22);
aInstall.install();
}
function finish_test_22(aInstall) {
prepare_test({
"addon3@tests.mozilla.org": [
"onOperationCancelled"
]
}, [
"onInstallCancelled"
]);
aInstall.cancel();
ensure_test_completed();
run_test_23();
}
// Tests that an install can be restarted after being cancelled when a hash
// was provided
function run_test_23() {
prepare_test({ }, [
"onNewInstall"
]);
let url = "http://localhost:4444/addons/test_install3.xpi";
AddonManager.getInstallForURL(url, function(aInstall) {
ensure_test_completed();
do_check_neq(aInstall, null);
do_check_eq(aInstall.state, AddonManager.STATE_AVAILABLE);
prepare_test({}, [
"onDownloadStarted",
"onDownloadEnded",
], check_test_23);
aInstall.install();
}, "application/x-xpinstall", do_get_addon_hash("test_install3"));
}
function check_test_23(aInstall) {
prepare_test({}, [
"onDownloadCancelled"
]);
aInstall.cancel();
ensure_test_completed();
prepare_test({
"addon3@tests.mozilla.org": [
"onInstalling"
]
}, [
"onDownloadStarted",
"onDownloadEnded",
"onInstallStarted",
"onInstallEnded"
], finish_test_23);
aInstall.install();
}
function finish_test_23(aInstall) {
prepare_test({
"addon3@tests.mozilla.org": [
"onOperationCancelled"
]
}, [
"onInstallCancelled"
]);
aInstall.cancel();
ensure_test_completed();
run_test_24();
}
// Tests that an install with a bad hash can be restarted after it fails, though
// it will only fail again
function run_test_24() {
prepare_test({ }, [
"onNewInstall"
]);
let url = "http://localhost:4444/addons/test_install3.xpi";
AddonManager.getInstallForURL(url, function(aInstall) {
ensure_test_completed();
do_check_neq(aInstall, null);
do_check_eq(aInstall.state, AddonManager.STATE_AVAILABLE);
prepare_test({}, [
"onDownloadStarted",
"onDownloadFailed",
], check_test_24);
aInstall.install();
}, "application/x-xpinstall", "sha1:foo");
}
function check_test_24(aInstall) {
prepare_test({ }, [
"onDownloadStarted",
"onDownloadFailed"
], run_test_25);
aInstall.install();
}
// Tests that installs with a hash for a local file work
function run_test_25() {
prepare_test({ }, [
"onNewInstall"
]);
let url = Services.io.newFileURI(do_get_addon("test_install3")).spec;
AddonManager.getInstallForURL(url, function(aInstall) {
ensure_test_completed();
do_check_neq(aInstall, null);
do_check_eq(aInstall.state, AddonManager.STATE_DOWNLOADED);
do_check_eq(aInstall.error, 0);
prepare_test({ }, [
"onDownloadCancelled"
]);
aInstall.cancel();
ensure_test_completed();
end_test();
}, "application/x-xpinstall", do_get_addon_hash("test_install3"));
}

View File

@ -92,6 +92,7 @@ _BROWSER_FILES = head.js \
browser_httphash3.js \
browser_httphash4.js \
browser_httphash5.js \
browser_httphash6.js \
browser_badargs.js \
unsigned.xpi \
signed.xpi \
@ -116,6 +117,7 @@ _BROWSER_FILES = head.js \
cookieRedirect.sjs \
hashRedirect.sjs \
bug540558.html \
redirect.sjs \
$(NULL)
libs:: $(_BROWSER_FILES)

View File

@ -0,0 +1,83 @@
// ----------------------------------------------------------------------------
// Tests that a new hash is accepted when restarting a failed download
// This verifies bug 593535
function setup_redirect(aSettings) {
var url = "https://example.com/browser/" + RELATIVE_DIR + "redirect.sjs?mode=setup";
for (var name in aSettings) {
url += "&" + name + "=" + aSettings[name];
}
var req = new XMLHttpRequest();
req.open("GET", url, false);
req.send(null);
}
var gInstall = null;
function test() {
Harness.downloadFailedCallback = download_failed;
Harness.installsCompletedCallback = finish_failed_download;
Harness.setup();
var pm = Services.perms;
pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false);
// Set up the redirect to give a bad hash
setup_redirect({
"X-Target-Digest": "sha1:foo",
"Location": "http://example.com/browser/" + RELATIVE_DIR + "unsigned.xpi"
});
var url = "https://example.com/browser/" + RELATIVE_DIR + "redirect.sjs?mode=redirect";
var triggers = encodeURIComponent(JSON.stringify({
"Unsigned XPI": {
URL: url,
toString: function() { return this.URL; }
}
}));
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
}
function download_failed(install) {
is(install.error, AddonManager.ERROR_INCORRECT_HASH, "Should have seen a hash failure");
// Stash the failed download while the harness cleans itself up
gInstall = install;
}
function finish_failed_download() {
// Setup to track the successful re-download
Harness.installEndedCallback = install_ended;
Harness.installsCompletedCallback = finish_test;
Harness.setup();
// Give it the right hash this time
setup_redirect({
"X-Target-Digest": "sha1:3d0dc22e1f394e159b08aaf5f0f97de4d5c65f4f",
"Location": "http://example.com/browser/" + RELATIVE_DIR + "unsigned.xpi"
});
// The harness expects onNewInstall events for all installs that are about to start
Harness.onNewInstall(gInstall);
// Restart the install as a regular webpage install so the harness tracks it
AddonManager.installAddonsFromWebpage("application/x-xpinstall",
gBrowser.contentWindow,
gBrowser.currentURI, [gInstall]);
}
function install_ended(install, addon) {
install.cancel();
}
function finish_test(count) {
is(count, 1, "1 Add-on should have been successfully installed");
Services.perms.remove("example.com", "install");
Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS);
gBrowser.removeCurrentTab();
Harness.finish();
}

View File

@ -72,39 +72,53 @@ var Harness = {
installCount: null,
runningInstalls: null,
waitingForFinish: false,
// Setup and tear down functions
setup: function() {
waitForExplicitFinish();
Services.prefs.setBoolPref(PREF_LOGGING_ENABLED, true);
Services.obs.addObserver(this, "addon-install-started", false);
Services.obs.addObserver(this, "addon-install-blocked", false);
Services.obs.addObserver(this, "addon-install-failed", false);
Services.obs.addObserver(this, "addon-install-complete", false);
Services.wm.addListener(this);
if (!this.waitingForFinish) {
waitForExplicitFinish();
this.waitingForFinish = true;
Services.prefs.setBoolPref(PREF_LOGGING_ENABLED, true);
Services.obs.addObserver(this, "addon-install-started", false);
Services.obs.addObserver(this, "addon-install-blocked", false);
Services.obs.addObserver(this, "addon-install-failed", false);
Services.obs.addObserver(this, "addon-install-complete", false);
AddonManager.addInstallListener(this);
Services.wm.addListener(this);
var self = this;
registerCleanupFunction(function() {
Services.prefs.clearUserPref(PREF_LOGGING_ENABLED);
Services.obs.removeObserver(self, "addon-install-started");
Services.obs.removeObserver(self, "addon-install-blocked");
Services.obs.removeObserver(self, "addon-install-failed");
Services.obs.removeObserver(self, "addon-install-complete");
AddonManager.removeInstallListener(self);
Services.wm.removeListener(self);
AddonManager.getAllInstalls(function(aInstalls) {
is(aInstalls.length, 0, "Should be no active installs at the end of the test");
installs.forEach(function(aInstall) {
info("Install for " + aInstall.sourceURI + " is in state " + aInstall.state);
aInstall.cancel();
});
});
});
}
AddonManager.addInstallListener(this);
this.installCount = 0;
this.pendingCount = 0;
this.runningInstalls = [];
var self = this;
registerCleanupFunction(function() {
Services.prefs.clearUserPref(PREF_LOGGING_ENABLED);
Services.obs.removeObserver(self, "addon-install-started");
Services.obs.removeObserver(self, "addon-install-blocked");
Services.obs.removeObserver(self, "addon-install-failed");
Services.obs.removeObserver(self, "addon-install-complete");
Services.wm.removeListener(self);
AddonManager.removeInstallListener(self);
});
},
finish: function() {
AddonManager.getAllInstalls(function(installs) {
is(installs.length, 0, "Should be no active installs at the end of the test");
finish();
});
finish();
},
endTest: function() {

View File

@ -0,0 +1,45 @@
// Script has two modes based on the query string. If the mode is "setup" then
// parameters from the query string configure the redirection. If the mode is
// "redirect" then a redirect is returned
function handleRequest(request, response)
{
let parts = request.queryString.split("&");
let settings = {};
parts.forEach(function(aString) {
let [k, v] = aString.split("=");
settings[k] = v;
})
if (settings.mode == "setup") {
delete settings.mode;
// Object states must be an nsISupports
var state = {
settings: settings,
QueryInterface: function(aIid) {
if (aIid.equals(Components.interfaces.nsISupports))
return settings;
throw Components.results.NS_ERROR_NO_INTERFACE;
}
}
state.wrappedJSObject = state;
setObjectState("xpinstall-redirect-settings", state);
response.setStatusLine(request.httpVersion, 200, "Ok");
response.setHeader("Content-Type", "text/plain");
response.write("Setup complete");
}
else if (settings.mode == "redirect") {
getObjectState("xpinstall-redirect-settings", function(aObject) {
settings = aObject.wrappedJSObject.settings;
});
response.setStatusLine(request.httpVersion, 302, "Found");
for (var name in settings) {
response.setHeader(name, settings[name]);
}
response.write("Done");
}
}