Bug 1214058: Part 1 - Add a simplified JSON-based add-on update protocol. r=Mossop

This commit is contained in:
Kris Maglione 2015-11-03 14:49:46 -08:00
parent 984587207a
commit 0ca0a616cf
10 changed files with 1190 additions and 308 deletions

View File

@ -4402,6 +4402,7 @@ pref("xpinstall.whitelist.required", true);
pref("xpinstall.signatures.required", false);
pref("extensions.alwaysUnpack", false);
pref("extensions.minCompatiblePlatformVersion", "2.0");
pref("extensions.webExtensionsMinPlatformVersion", "42.0a1");
pref("network.buffer.cache.count", 24);
pref("network.buffer.cache.size", 32768);

View File

@ -42,6 +42,8 @@ const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
const PREF_SELECTED_LOCALE = "general.useragent.locale";
const UNKNOWN_XPCOM_ABI = "unknownABI";
const PREF_MIN_WEBEXT_PLATFORM_VERSION = "extensions.webExtensionsMinPlatformVersion";
const UPDATE_REQUEST_VERSION = 2;
const CATEGORY_UPDATE_PARAMS = "extension-update-params";
@ -663,6 +665,7 @@ var gCheckUpdateSecurity = gCheckUpdateSecurityDefault;
var gUpdateEnabled = true;
var gAutoUpdateDefault = true;
var gHotfixID = null;
var gWebExtensionsMinPlatformVersion = null;
var gShutdownBarrier = null;
var gRepoShutdownState = "";
var gShutdownInProgress = false;
@ -947,6 +950,11 @@ var AddonManagerInternal = {
} catch (e) {}
Services.prefs.addObserver(PREF_EM_HOTFIX_ID, this, false);
try {
gWebExtensionsMinPlatformVersion = Services.prefs.getCharPref(PREF_MIN_WEBEXT_PLATFORM_VERSION);
} catch (e) {}
Services.prefs.addObserver(PREF_MIN_WEBEXT_PLATFORM_VERSION, this, false);
let defaultProvidersEnabled = true;
try {
defaultProvidersEnabled = Services.prefs.getBoolPref(PREF_DEFAULT_PROVIDERS_ENABLED);
@ -1377,6 +1385,10 @@ var AddonManagerInternal = {
}
break;
}
case PREF_MIN_WEBEXT_PLATFORM_VERSION: {
gWebExtensionsMinPlatformVersion = Services.prefs.getCharPref(PREF_MIN_WEBEXT_PLATFORM_VERSION);
break;
}
}
},
@ -2894,6 +2906,10 @@ this.AddonManagerPrivate = {
safeCall(listener.onUpdateFinished.bind(listener), addon);
}
},
get webExtensionsMinPlatformVersion() {
return gWebExtensionsMinPlatformVersion;
},
};
/**

View File

@ -31,6 +31,8 @@ Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
"resource://gre/modules/addons/AddonRepository.jsm");
@ -217,6 +219,48 @@ RDFSerializer.prototype = {
}
}
/**
* Sanitizes the update URL in an update item, as returned by
* parseRDFManifest and parseJSONManifest. Ensures that:
*
* - The URL is secure, or secured by a strong enough hash.
* - The security principal of the update manifest has permission to
* load the URL.
*
* @param aUpdate
* The update item to sanitize.
* @param aRequest
* The XMLHttpRequest used to load the manifest.
* @param aHashPattern
* The regular expression used to validate the update hash.
* @param aHashString
* The human-readable string specifying which hash functions
* are accepted.
*/
function sanitizeUpdateURL(aUpdate, aRequest, aHashPattern, aHashString) {
if (aUpdate.updateURL) {
let scriptSecurity = Services.scriptSecurityManager;
let principal = scriptSecurity.getChannelURIPrincipal(aRequest.channel);
try {
// This logs an error on failure, so no need to log it a second time
scriptSecurity.checkLoadURIStrWithPrincipal(principal, aUpdate.updateURL,
scriptSecurity.DISALLOW_SCRIPT);
} catch (e) {
delete aUpdate.updateURL;
return;
}
if (AddonManager.checkUpdateSecurity &&
!aUpdate.updateURL.startsWith("https:") &&
!aHashPattern.test(aUpdate.updateHash)) {
logger.warn(`Update link ${aUpdate.updateURL} is not secure and is not verified ` +
`by a strong enough hash (needs to be ${aHashString}).`);
delete aUpdate.updateURL;
delete aUpdate.updateHash;
}
}
}
/**
* Parses an RDF style update manifest into an array of update objects.
*
@ -226,10 +270,17 @@ RDFSerializer.prototype = {
* An optional update key for the add-on
* @param aRequest
* The XMLHttpRequest that has retrieved the update manifest
* @param aManifestData
* The pre-parsed manifest, as a bare XML DOM document
* @return an array of update objects
* @throws if the update manifest is invalid in any way
*/
function parseRDFManifest(aId, aUpdateKey, aRequest) {
function parseRDFManifest(aId, aUpdateKey, aRequest, aManifestData) {
if (aManifestData.documentElement.namespaceURI != PREFIX_NS_RDF) {
throw Components.Exception("Update manifest had an unrecognised namespace: " + xml.documentElement.namespaceURI);
return;
}
function EM_R(aProp) {
return gRDF.GetResource(PREFIX_NS_EM + aProp);
}
@ -366,20 +417,136 @@ function parseRDFManifest(aId, aUpdateKey, aRequest) {
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;
}
// The JSON update protocol requires an SHA-2 hash. RDF still
// supports SHA-1, for compatibility reasons.
sanitizeUpdateURL(result, aRequest, /^sha/, "sha1 or stronger");
results.push(result);
}
}
return results;
}
/**
* Parses an JSON 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
* @param aManifestData
* The pre-parsed manifest, as a JSON object tree
* @return an array of update objects
* @throws if the update manifest is invalid in any way
*/
function parseJSONManifest(aId, aUpdateKey, aRequest, aManifestData) {
if (aUpdateKey)
throw Components.Exception("Update keys are not supported for JSON update manifests");
let TYPE_CHECK = {
"array": val => Array.isArray(val),
"object": val => val && typeof val == "object" && !Array.isArray(val),
};
function getProperty(aObj, aProperty, aType, aDefault = undefined) {
if (!(aProperty in aObj))
return aDefault;
let value = aObj[aProperty];
let matchesType = aType in TYPE_CHECK ? TYPE_CHECK[aType](value) : typeof value == aType;
if (!matchesType)
throw Components.Exception(`Update manifest property '${aProperty}' has incorrect type (expected ${aType})`);
return value;
}
function getRequiredProperty(aObj, aProperty, aType) {
let value = getProperty(aObj, aProperty, aType);
if (value === undefined)
throw Components.Exception(`Update manifest is missing a required ${aProperty} property.`);
return value;
}
let manifest = aManifestData;
if (!TYPE_CHECK["object"](manifest))
throw Components.Exception("Root element of update manifest must be a JSON object literal");
// The set of add-ons this manifest has updates for
let addons = getRequiredProperty(manifest, "addons", "object");
// The entry for this particular add-on
let addon = getProperty(addons, aId, "object");
// A missing entry doesn't count as a failure, just as no avialable update
// information
if (!addon) {
logger.warn("Update manifest did not contain an entry for " + aId);
return [];
}
// The list of available updates
let updates = getProperty(addon, "updates", "array", []);
let results = [];
for (let update of updates) {
let version = getRequiredProperty(update, "version", "string");
logger.debug(`Found an update entry for ${aId} version ${version}`);
let applications = getProperty(update, "applications", "object",
{ gecko: {} });
// "gecko" is currently the only supported application entry. If
// it's missing, skip this update.
if (!("gecko" in applications))
continue;
let app = getProperty(applications, "gecko", "object");
let appEntry = {
id: TOOLKIT_ID,
minVersion: getProperty(app, "strict_min_version", "string",
AddonManagerPrivate.webExtensionsMinPlatformVersion),
maxVersion: "*",
};
let result = {
id: aId,
version: version,
multiprocessCompatible: getProperty(update, "multiprocess_compatible", "boolean", true),
updateURL: getProperty(update, "update_link", "string"),
updateHash: getProperty(update, "update_hash", "string"),
updateInfoURL: getProperty(update, "update_info_url", "string"),
strictCompatibility: false,
targetApplications: [appEntry],
};
if ("strict_max_version" in app) {
if ("advisory_max_version" in app) {
logger.warn("Ignoring 'advisory_max_version' update manifest property for " +
aId + " property since 'strict_max_version' also present");
}
appEntry.maxVersion = getProperty(app, "strict_max_version", "string");
result.strictCompatibility = appEntry.maxVersion != "*";
} else if ("advisory_max_version" in app) {
appEntry.maxVersion = getProperty(app, "advisory_max_version", "string");
}
// The JSON update protocol requires an SHA-2 hash. RDF still
// supports SHA-1, for compatibility reasons.
sanitizeUpdateURL(result, aRequest, /^sha(256|512):/, "sha256 or sha512");
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
@ -415,7 +582,7 @@ function UpdateParser(aId, aUpdateKey, aUrl, aObserver) {
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.overrideMimeType("text/plain");
this.request.setRequestHeader("Moz-XPI-Update", "1", true);
this.request.timeout = TIMEOUT;
var self = this;
@ -474,41 +641,50 @@ UpdateParser.prototype = {
return;
}
let xml = request.responseXML;
if (!xml || xml.documentElement.namespaceURI == XMLURI_PARSE_ERROR) {
logger.warn("Update manifest was not valid XML");
// Detect the manifest type by first attempting to parse it as
// JSON, and falling back to parsing it as XML if that fails.
let parser;
try {
try {
let json = JSON.parse(request.responseText);
parser = () => parseJSONManifest(this.id, this.updateKey, request, json);
} catch (e if e instanceof SyntaxError) {
let domParser = Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
let xml = domParser.parseFromString(request.responseText, "text/xml");
if (xml.documentElement.namespaceURI == XMLURI_PARSE_ERROR)
throw new Error("Update manifest was not valid XML or JSON");
parser = () => parseRDFManifest(this.id, this.updateKey, request, xml);
}
} catch (e) {
logger.warn("onUpdateCheckComplete failed to determine manifest type");
this.notifyError(AddonUpdateChecker.ERROR_UNKNOWN_FORMAT);
return;
}
let results;
try {
results = parser();
}
catch (e) {
logger.warn("onUpdateCheckComplete failed to parse update manifest", e);
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;
if ("onUpdateCheckComplete" in this.observer) {
try {
results = parseRDFManifest(this.id, this.updateKey, request);
this.observer.onUpdateCheckComplete(results);
}
catch (e) {
logger.warn("onUpdateCheckComplete failed to parse RDF manifest", e);
this.notifyError(AddonUpdateChecker.ERROR_PARSE_ERROR);
return;
logger.warn("onUpdateCheckComplete notification failed", e);
}
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);
else {
logger.warn("onUpdateCheckComplete may not properly cancel", new Error("stack marker"));
}
},
/**

View File

@ -906,7 +906,7 @@ function loadManifestFromWebManifest(aStream) {
addon.targetApplications = [{
id: TOOLKIT_ID,
minVersion: "42a1",
minVersion: AddonManagerPrivate.webExtensionsMinPlatformVersion,
maxVersion: "*",
}];

View File

@ -0,0 +1,327 @@
{
"addons": {
"updatecheck1@tests.mozilla.org": {
"updates": [
{
"version": "1.0",
"update_link": "https://localhost:4444/addons/test1.xpi",
"applications": {
"gecko": {
"strict_min_version": "1",
"strict_max_version": "1"
}
}
},
{
"_comment_": "This update is incompatible and so should not be considered a valid update",
"version": "2.0",
"update_link": "https://localhost:4444/addons/test2.xpi",
"applications": {
"gecko": {
"strict_min_version": "2",
"strict_max_version": "2"
}
}
},
{
"version": "3.0",
"update_link": "https://localhost:4444/addons/test3.xpi",
"applications": {
"gecko": {
"strict_min_version": "1",
"strict_max_version": "1"
}
}
},
{
"version": "2.0",
"update_link": "https://localhost:4444/addons/test2.xpi",
"applications": {
"gecko": {
"strict_min_version": "1",
"strict_max_version": "2"
}
}
},
{
"_comment_": "This update is incompatible and so should not be considered a valid update",
"version": "4.0",
"update_link": "https://localhost:4444/addons/test4.xpi",
"applications": {
"gecko": {
"strict_min_version": "2",
"strict_max_version": "2"
}
}
}
]
},
"test_bug378216_5@tests.mozilla.org": {
"_comment_": "An update which expects a signature. It will fail since signatures are ",
"_comment_": "supported in this format.",
"_comment_": "The updateLink will also be ignored since it is not secure and there ",
"_comment_": "is no updateHash.",
"updates": [
{
"version": "2.0",
"update_link": "http://localhost:4444/broken.xpi",
"applications": {
"gecko": {
"strict_min_version": "1",
"strict_max_version": "1"
}
}
}
]
},
"test_bug378216_5@tests.mozilla.org": {
"updates": [
{
"version": "2.0",
"update_link": "http://localhost:4444/broken.xpi",
"applications": {
"gecko": {
"strict_min_version": "1",
"strict_max_version": "1"
}
}
}
]
},
"test_bug378216_7@tests.mozilla.org": {
"_comment_": "An update which expects a signature. It will fail since signatures are ",
"_comment_": "supported in this format.",
"_comment_": "The updateLink will also be ignored since it is not secure ",
"_comment_": "and there is no updateHash.",
"updates": [
{
"version": "2.0",
"update_link": "http://localhost:4444/broken.xpi",
"applications": {
"gecko": {
"strict_min_version": "1",
"strict_max_version": "2"
}
}
}
]
},
"test_bug378216_8@tests.mozilla.org": {
"_comment_": "The updateLink will be ignored since it is not secure and ",
"_comment_": "there is no updateHash.",
"updates": [
{
"version": "2.0",
"update_link": "http://localhost:4444/broken.xpi",
"applications": {
"gecko": {
"strict_min_version": "1",
"strict_max_version": "1"
}
}
}
]
},
"test_bug378216_9@tests.mozilla.org": {
"_comment_": "The updateLink will used since there is an updateHash to verify it.",
"updates": [
{
"version": "2.0",
"update_link": "http://localhost:4444/broken.xpi",
"update_hash": "sha256:78fc1d2887eda35b4ad2e3a0b60120ca271ce6e6",
"applications": {
"gecko": {
"strict_min_version": "1",
"strict_max_version": "1"
}
}
}
]
},
"test_bug378216_10@tests.mozilla.org": {
"_comment_": "The updateLink will used since it is a secure URL.",
"updates": [
{
"version": "2.0",
"update_link": "https://localhost:4444/broken.xpi",
"applications": {
"gecko": {
"strict_min_version": "1",
"strict_max_version": "1"
}
}
}
]
},
"test_bug378216_11@tests.mozilla.org": {
"_comment_": "The updateLink will used since it is a secure URL.",
"updates": [
{
"version": "2.0",
"update_link": "https://localhost:4444/broken.xpi",
"applications": {
"gecko": {
"strict_min_version": "1",
"strict_max_version": "1"
}
}
}
]
},
"test_bug378216_12@tests.mozilla.org": {
"_comment_": "The updateLink will not be used since the updateHash ",
"_comment_": "verifying it is not strong enough.",
"updates": [
{
"version": "2.0",
"update_link": "http://localhost:4444/broken.xpi",
"update_hash": "sha1:78fc1d2887eda35b4ad2e3a0b60120ca271ce6e6",
"applications": {
"gecko": {
"strict_min_version": "1",
"strict_max_version": "1"
}
}
}
]
},
"test_bug378216_13@tests.mozilla.org": {
"_comment_": "An update with a weak hash. The updateLink will used since it is ",
"_comment_": "a secure URL.",
"updates": [
{
"version": "2.0",
"update_link": "https://localhost:4444/broken.xpi",
"update_hash": "sha1:78fc1d2887eda35b4ad2e3a0b60120ca271ce6e6",
"applications": {
"gecko": {
"strict_min_version": "1",
"strict_max_version": "1"
}
}
}
]
},
"_comment_": "There should be no information present for test_bug378216_14",
"test_bug378216_15@tests.mozilla.org": {
"_comment_": "Invalid update JSON",
"updates": "foo"
},
"ignore-compat@tests.mozilla.org": {
"_comment_": "Various updates available - one is not compatible, but compatibility checking is disabled",
"updates": [
{
"version": "1.0",
"update_link": "https://localhost:4444/addons/test1.xpi",
"applications": {
"gecko": {
"strict_min_version": "0.1",
"advisory_max_version": "0.2"
}
}
},
{
"version": "2.0",
"update_link": "https://localhost:4444/addons/test2.xpi",
"applications": {
"gecko": {
"strict_min_version": "0.5",
"advisory_max_version": "0.6"
}
}
},
{
"_comment_": "Update for future app versions - should never be compatible",
"version": "3.0",
"update_link": "https://localhost:4444/addons/test3.xpi",
"applications": {
"gecko": {
"strict_min_version": "2",
"advisory_max_version": "3"
}
}
}
]
},
"compat-override@tests.mozilla.org": {
"_comment_": "Various updates available - one is not compatible, but compatibility checking is disabled",
"updates": [
{
"_comment_": "Has compatibility override, but it doesn't match this app version",
"version": "1.0",
"update_link": "https://localhost:4444/addons/test1.xpi",
"applications": {
"gecko": {
"strict_min_version": "0.1",
"advisory_max_version": "0.2"
}
}
},
{
"_comment_": "Has compatibility override, so is incompaible",
"version": "2.0",
"update_link": "https://localhost:4444/addons/test2.xpi",
"applications": {
"gecko": {
"strict_min_version": "0.5",
"advisory_max_version": "0.6"
}
}
},
{
"_comment_": "Update for future app versions - should never be compatible",
"version": "3.0",
"update_link": "https://localhost:4444/addons/test3.xpi",
"applications": {
"gecko": {
"strict_min_version": "2",
"advisory_max_version": "3"
}
}
}
]
},
"compat-strict-optin@tests.mozilla.org": {
"_comment_": "Opt-in to strict compatibility checking",
"updates": [
{
"version": "1.0",
"update_link": "https://localhost:4444/addons/test1.xpi",
"_comment_": "strictCompatibility: true",
"applications": {
"gecko": {
"strict_min_version": "0.1",
"strict_max_version": "0.2"
}
}
}
]
}
}
}

View File

@ -236,7 +236,7 @@
A90eF5zy</em:signature>
</RDF:Description>
<!-- An update with a valid signature. The updateLink will used since the
<!-- An update with a valid signature. The updateLink will not be used since the
updateHash verifying it is not strong enough. -->
<RDF:Description about="urn:mozilla:extension:test_bug378216_12@tests.mozilla.org">
<em:updates>

View File

@ -1542,7 +1542,7 @@ if ("nsIWindowsRegKey" in AM_Ci) {
* This is a mock nsIWindowsRegistry implementation. It only implements the
* methods that the extension manager requires.
*/
function MockWindowsRegKey() {
var MockWindowsRegKey = function MockWindowsRegKey() {
}
MockWindowsRegKey.prototype = {
@ -1723,6 +1723,30 @@ do_register_cleanup(function addon_cleanup() {
} catch (e) {}
});
/**
* Creates a new HttpServer for testing, and begins listening on the
* specified port. Automatically shuts down the server when the test
* unit ends.
*
* @param port
* The port to listen on. If omitted, listen on a random
* port. The latter is the preferred behavior.
*
* @return HttpServer
*/
function createHttpServer(port = -1) {
let server = new HttpServer();
server.start(port);
do_register_cleanup(() => {
return new Promise(resolve => {
server.stop(resolve);
});
});
return server;
}
/**
* Handler function that responds with the interpolated
* static file associated to the URL specified by request.path.
@ -1912,3 +1936,43 @@ function promiseFindAddonUpdates(addon, reason = AddonManager.UPDATE_WHEN_PERIOD
}, reason);
});
}
/**
* Monitors console output for the duration of a task, and returns a promise
* which resolves to a tuple containing a list of all console messages
* generated during the task's execution, and the result of the task itself.
*
* @param {function} aTask
* The task to run while monitoring console output. May be
* either a generator function, per Task.jsm, or an ordinary
* function which returns promose.
* @return {Promise<[Array<nsIConsoleMessage>, *]>}
*/
var promiseConsoleOutput = Task.async(function*(aTask) {
const DONE = "=== xpcshell test console listener done ===";
let listener, messages = [];
let awaitListener = new Promise(resolve => {
listener = msg => {
if (msg == DONE) {
resolve();
} else {
msg instanceof Components.interfaces.nsIScriptError;
messages.push(msg);
}
}
});
Services.console.registerListener(listener);
try {
let result = yield aTask();
Services.console.logStringMessage(DONE);
yield awaitListener;
return { messages, result };
}
finally {
Services.console.unregisterListener(listener);
}
});

View File

@ -0,0 +1,373 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
// This verifies that AddonUpdateChecker works correctly for JSON
// update manifests, particularly for behavior which does not
// cleanly overlap with RDF manifests.
const TOOLKIT_ID = "toolkit@mozilla.org";
const TOOLKIT_MINVERSION = "42.0a1";
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42.0a2", "42.0a2");
Components.utils.import("resource://gre/modules/addons/AddonUpdateChecker.jsm");
Components.utils.import("resource://testing-common/httpd.js");
let testserver = createHttpServer();
gPort = testserver.identity.primaryPort;
let gUpdateManifests = {};
function mapManifest(aPath, aManifestData) {
gUpdateManifests[aPath] = aManifestData;
testserver.registerPathHandler(aPath, serveManifest);
}
function serveManifest(request, response) {
let manifest = gUpdateManifests[request.path];
response.setHeader("Content-Type", manifest.contentType, false);
response.write(manifest.data);
}
const extensionsDir = gProfD.clone();
extensionsDir.append("extensions");
function checkUpdates(aData) {
// Registers JSON update manifest for it with the testing server,
// checks for updates, and yields the list of updates on
// success.
let extension = aData.manifestExtension || "json";
let path = `/updates/${aData.id}.${extension}`;
let updateUrl = `http://localhost:${gPort}${path}`
let addonData = {};
if ("updates" in aData)
addonData.updates = aData.updates;
let manifestJSON = {
"addons": {
[aData.id]: addonData
}
};
mapManifest(path.replace(/\?.*/, ""),
{ data: JSON.stringify(manifestJSON),
contentType: aData.contentType || "application/json" });
return new Promise((resolve, reject) => {
AddonUpdateChecker.checkForUpdates(aData.id, aData.updateKey, updateUrl, {
onUpdateCheckComplete: resolve,
onUpdateCheckError: function(status) {
reject(new Error("Update check failed with status " + status));
}
});
});
}
add_task(function* test_default_values() {
// Checks that the appropriate defaults are used for omitted values.
startupManager();
let updates = yield checkUpdates({
id: "updatecheck-defaults@tests.mozilla.org",
version: "0.1",
updates: [{
version: "0.2"
}]
});
equal(updates.length, 1);
let update = updates[0];
equal(update.targetApplications.length, 1);
let targetApp = update.targetApplications[0];
equal(targetApp.id, TOOLKIT_ID);
equal(targetApp.minVersion, TOOLKIT_MINVERSION);
equal(targetApp.maxVersion, "*");
equal(update.version, "0.2");
equal(update.multiprocessCompatible, true, "multiprocess_compatible flag");
equal(update.strictCompatibility, false, "inferred strictConpatibility flag");
equal(update.updateURL, null, "updateURL");
equal(update.updateHash, null, "updateHash");
equal(update.updateInfoURL, null, "updateInfoURL");
// If there's no applications property, we default to using one
// containing "gecko". If there is an applications property, but
// it doesn't contain "gecko", the update is skipped.
updates = yield checkUpdates({
id: "updatecheck-defaults@tests.mozilla.org",
version: "0.1",
updates: [{
version: "0.2",
applications: { "foo": {} }
}]
});
equal(updates.length, 0);
// Updates property is also optional. No updates, but also no error.
updates = yield checkUpdates({
id: "updatecheck-defaults@tests.mozilla.org",
version: "0.1",
});
equal(updates.length, 0);
});
add_task(function* test_explicit_values() {
// Checks that the appropriate explicit values are used when
// provided.
let updates = yield checkUpdates({
id: "updatecheck-explicit@tests.mozilla.org",
version: "0.1",
updates: [{
version: "0.2",
update_link: "https://example.com/foo.xpi",
update_hash: "sha256:0",
update_info_url: "https://example.com/update_info.html",
multiprocess_compatible: false,
applications: {
gecko: {
strict_min_version: "42.0a2.xpcshell",
strict_max_version: "43.xpcshell"
}
}
}]
});
equal(updates.length, 1);
let update = updates[0];
equal(update.targetApplications.length, 1);
let targetApp = update.targetApplications[0];
equal(targetApp.id, TOOLKIT_ID);
equal(targetApp.minVersion, "42.0a2.xpcshell");
equal(targetApp.maxVersion, "43.xpcshell");
equal(update.version, "0.2");
equal(update.multiprocessCompatible, false, "multiprocess_compatible flag");
equal(update.strictCompatibility, true, "inferred strictCompatibility flag");
equal(update.updateURL, "https://example.com/foo.xpi", "updateURL");
equal(update.updateHash, "sha256:0", "updateHash");
equal(update.updateInfoURL, "https://example.com/update_info.html", "updateInfoURL");
});
add_task(function* test_secure_hashes() {
// Checks that only secure hash functions are accepted for
// non-secure update URLs.
let hashFunctions = ["sha512",
"sha256",
"sha1",
"md5",
"md4",
"xxx"];
let updateItems = hashFunctions.map((hash, idx) => ({
version: `0.${idx}`,
update_link: `http://localhost:${gPort}/updates/${idx}-${hash}.xpi`,
update_hash: `${hash}:08ac852190ecd81f40a514ea9299fe9143d9ab5e296b97e73fb2a314de49648a`,
}));
let { messages, result: updates } = yield promiseConsoleOutput(() => {
return checkUpdates({
id: "updatecheck-hashes@tests.mozilla.org",
version: "0.1",
updates: updateItems
});
});
equal(updates.length, hashFunctions.length);
updates = updates.filter(update => update.updateHash || update.updateURL);
equal(updates.length, 2, "expected number of update hashes were accepted");
ok(updates[0].updateHash.startsWith("sha512:"), "sha512 hash is present");
ok(updates[0].updateURL);
ok(updates[1].updateHash.startsWith("sha256:"), "sha256 hash is present");
ok(updates[1].updateURL);
messages = messages.filter(msg => /Update link.*not secure.*strong enough hash \(needs to be sha256 or sha512\)/.test(msg.message));
equal(messages.length, hashFunctions.length - 2, "insecure hashes generated the expected warning");
});
add_task(function* test_strict_compat() {
// Checks that strict compatibility is enabled for strict max
// versions other than "*", but not for advisory max versions.
// Also, ensure that strict max versions take precedence over
// advisory versions.
let { messages, result: updates } = yield promiseConsoleOutput(() => {
return checkUpdates({
id: "updatecheck-strict@tests.mozilla.org",
version: "0.1",
updates: [
{ version: "0.2",
applications: { gecko: { strict_max_version: "*" } } },
{ version: "0.3",
applications: { gecko: { strict_max_version: "43" } } },
{ version: "0.4",
applications: { gecko: { advisory_max_version: "43" } } },
{ version: "0.5",
applications: { gecko: { advisory_max_version: "43",
strict_max_version: "44" } } },
]
});
});
equal(updates.length, 4, "all update items accepted");
equal(updates[0].targetApplications[0].maxVersion, "*");
equal(updates[0].strictCompatibility, false);
equal(updates[1].targetApplications[0].maxVersion, "43");
equal(updates[1].strictCompatibility, true);
equal(updates[2].targetApplications[0].maxVersion, "43");
equal(updates[2].strictCompatibility, false);
equal(updates[3].targetApplications[0].maxVersion, "44");
equal(updates[3].strictCompatibility, true);
messages = messages.filter(msg => /Ignoring 'advisory_max_version'.*'strict_max_version' also present/.test(msg.message));
equal(messages.length, 1, "mix of advisory_max_version and strict_max_version generated the expected warning");
});
add_task(function* test_update_url_security() {
// Checks that update links to privileged URLs are not accepted.
let { messages, result: updates } = yield promiseConsoleOutput(() => {
return checkUpdates({
id: "updatecheck-security@tests.mozilla.org",
version: "0.1",
updates: [
{ version: "0.2",
update_link: "chrome://browser/content/browser.xul",
update_hash: "sha256:08ac852190ecd81f40a514ea9299fe9143d9ab5e296b97e73fb2a314de49648a" },
{ version: "0.3",
update_link: "http://example.com/update.xpi",
update_hash: "sha256:18ac852190ecd81f40a514ea9299fe9143d9ab5e296b97e73fb2a314de49648a" },
]
});
});
equal(updates.length, 2, "both updates were processed");
equal(updates[0].updateURL, null, "privileged update URL was removed");
equal(updates[1].updateURL, "http://example.com/update.xpi", "safe update URL was accepted");
messages = messages.filter(msg => /http:\/\/localhost.*\/updates\/.*may not load or link to chrome:/.test(msg.message));
equal(messages.length, 1, "privileged upate URL generated the expected console message");
});
add_task(function* test_no_update_key() {
// Checks that updates fail when an update key has been specified.
let { messages } = yield promiseConsoleOutput(function* () {
yield Assert.rejects(
checkUpdates({
id: "updatecheck-updatekey@tests.mozilla.org",
version: "0.1",
updateKey: "ayzzx=",
updates: [
{ version: "0.2" },
{ version: "0.3" },
]
}),
null, "updated expected to fail");
});
messages = messages.filter(msg => /Update keys are not supported for JSON update manifests/.test(msg.message));
equal(messages.length, 1, "got expected update-key-unsupported error");
});
add_task(function* test_type_detection() {
// Checks that JSON update manifests are detected correctly
// regardless of extension or MIME type.
let tests = [
{ contentType: "application/json",
extension: "json",
valid: true },
{ contentType: "application/json",
extension: "php",
valid: true },
{ contentType: "text/plain",
extension: "json",
valid: true },
{ contentType: "application/octet-stream",
extension: "json",
valid: true },
{ contentType: "text/plain",
extension: "json?foo=bar",
valid: true },
{ contentType: "text/plain",
extension: "php",
valid: true },
{ contentType: "text/plain",
extension: "rdf",
valid: true },
{ contentType: "application/json",
extension: "rdf",
valid: true },
{ contentType: "text/xml",
extension: "json",
valid: true },
{ contentType: "application/rdf+xml",
extension: "json",
valid: true },
];
for (let [i, test] of tests.entries()) {
let { messages } = yield promiseConsoleOutput(function *() {
let id = `updatecheck-typedetection-${i}@tests.mozilla.org`;
let updates;
try {
updates = yield checkUpdates({
id: id,
version: "0.1",
contentType: test.contentType,
manifestExtension: test.extension,
updates: [{ version: "0.2" }]
});
} catch (e) {
ok(!test.valid, "update manifest correctly detected as RDF");
return;
}
ok(test.valid, "update manifest correctly detected as JSON");
equal(updates.length, 1, "correct number of updates");
equal(updates[0].id, id, "update is for correct extension");
});
if (test.valid) {
// Make sure we don't get any XML parsing errors from the
// XMLHttpRequest machinery.
ok(!messages.some(msg => /not well-formed/.test(msg.message)),
"expect XMLHttpRequest not to attempt XML parsing");
}
messages = messages.filter(msg => /Update manifest was not valid XML/.test(msg.message));
equal(messages.length, !test.valid, "expected number of XML parsing errors");
}
});

View File

@ -7,52 +7,47 @@
Components.utils.import("resource://gre/modules/addons/AddonUpdateChecker.jsm");
Components.utils.import("resource://testing-common/httpd.js");
var testserver;
function run_test() {
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
var testserver = createHttpServer(4444);
testserver.registerDirectory("/data/", do_get_file("data"));
// Create and configure the HTTP server.
testserver = new HttpServer();
testserver.registerDirectory("/data/", do_get_file("data"));
testserver.start(4444);
function checkUpdates(aId, aUpdateKey, aUpdateFile) {
return new Promise((resolve, reject) => {
AddonUpdateChecker.checkForUpdates(aId, aUpdateKey, `http://localhost:4444/data/${aUpdateFile}`, {
onUpdateCheckComplete: resolve,
do_test_pending();
run_test_1();
}
function end_test() {
testserver.stop(do_test_finished);
}
// Test that a basic update check returns the expected available updates
function run_test_1() {
AddonUpdateChecker.checkForUpdates("updatecheck1@tests.mozilla.org", null,
"http://localhost:4444/data/test_updatecheck.rdf", {
onUpdateCheckComplete: function(updates) {
check_test_1(updates);
},
onUpdateCheckError: function(status) {
do_throw("Update check failed with status " + status);
}
onUpdateCheckError: function(status) {
let error = new Error("Update check failed with status " + status);
error.status = status;
reject(error);
}
});
});
}
function check_test_1(updates) {
do_check_eq(updates.length, 5);
let update = AddonUpdateChecker.getNewestCompatibleUpdate(updates);
do_check_neq(update, null);
do_check_eq(update.version, 3);
update = AddonUpdateChecker.getCompatibilityUpdate(updates, "2");
do_check_neq(update, null);
do_check_eq(update.version, 2);
do_check_eq(update.targetApplications[0].minVersion, 1);
do_check_eq(update.targetApplications[0].maxVersion, 2);
function run_test() {
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
run_test_2();
run_next_test();
}
// Test that a basic update check returns the expected available updates
add_task(function* () {
for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) {
let updates = yield checkUpdates("updatecheck1@tests.mozilla.org", null, file);
equal(updates.length, 5);
let update = AddonUpdateChecker.getNewestCompatibleUpdate(updates);
notEqual(update, null);
equal(update.version, "3.0");
update = AddonUpdateChecker.getCompatibilityUpdate(updates, "2");
notEqual(update, null);
equal(update.version, "2.0");
equal(update.targetApplications[0].minVersion, "1");
equal(update.targetApplications[0].maxVersion, "2");
}
});
/*
* Tests that the security checks are applied correctly
*
@ -73,240 +68,169 @@ var updateKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDK426erD/H3XtsjvaB5+PJqbh
"NyeP6i4LuUYjTURnn7Yw/IgzyIJ2oKsYa32RuxAyteqAWqPT/J63wBixIeCxmysf" +
"awB/zH4KaPiY3vnrzQIDAQAB";
function run_test_2() {
AddonUpdateChecker.checkForUpdates("test_bug378216_5@tests.mozilla.org",
updateKey,
"http://localhost:4444/data/test_updatecheck.rdf", {
onUpdateCheckComplete: function(updates) {
do_throw("Expected the update check to fail");
},
add_task(function* () {
for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) {
try {
yield checkUpdates("test_bug378216_5@tests.mozilla.org",
updateKey, file);
throw "Expected the update check to fail";
} catch (e) {}
}
});
onUpdateCheckError: function(status) {
run_test_3();
add_task(function* () {
for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) {
try {
yield checkUpdates("test_bug378216_7@tests.mozilla.org",
updateKey, file);
throw "Expected the update check to fail";
} catch (e) {}
}
});
add_task(function* () {
// Make sure that the JSON manifest is rejected when an update key is
// required, but perform the remaining tests which aren't expected to fail
// because of the update key, without requiring one for the JSON variant.
try {
let updates = yield checkUpdates("test_bug378216_8@tests.mozilla.org",
updateKey, "test_updatecheck.json");
throw "Expected the update check to fail";
} catch(e) {}
for (let [file, key] of [["test_updatecheck.rdf", updateKey],
["test_updatecheck.json", null]]) {
let updates = yield checkUpdates("test_bug378216_8@tests.mozilla.org",
key, file);
equal(updates.length, 1);
ok(!("updateURL" in updates[0]));
}
});
add_task(function* () {
for (let [file, key] of [["test_updatecheck.rdf", updateKey],
["test_updatecheck.json", null]]) {
let updates = yield checkUpdates("test_bug378216_9@tests.mozilla.org",
key, file);
equal(updates.length, 1);
equal(updates[0].version, "2.0");
ok("updateURL" in updates[0]);
}
});
add_task(function* () {
for (let [file, key] of [["test_updatecheck.rdf", updateKey],
["test_updatecheck.json", null]]) {
let updates = yield checkUpdates("test_bug378216_10@tests.mozilla.org",
key, file);
equal(updates.length, 1);
equal(updates[0].version, "2.0");
ok("updateURL" in updates[0]);
}
});
add_task(function* () {
for (let [file, key] of [["test_updatecheck.rdf", updateKey],
["test_updatecheck.json", null]]) {
let updates = yield checkUpdates("test_bug378216_11@tests.mozilla.org",
key, file);
equal(updates.length, 1);
equal(updates[0].version, "2.0");
ok("updateURL" in updates[0]);
}
});
add_task(function* () {
for (let [file, key] of [["test_updatecheck.rdf", updateKey],
["test_updatecheck.json", null]]) {
let updates = yield checkUpdates("test_bug378216_12@tests.mozilla.org",
key, file);
equal(updates.length, 1);
do_check_false("updateURL" in updates[0]);
}
});
add_task(function* () {
for (let [file, key] of [["test_updatecheck.rdf", updateKey],
["test_updatecheck.json", null]]) {
let updates = yield checkUpdates("test_bug378216_13@tests.mozilla.org",
key, file);
equal(updates.length, 1);
equal(updates[0].version, "2.0");
ok("updateURL" in updates[0]);
}
});
add_task(function* () {
for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) {
let updates = yield checkUpdates("test_bug378216_14@tests.mozilla.org",
null, file);
equal(updates.length, 0);
}
});
add_task(function* () {
for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) {
try {
yield checkUpdates("test_bug378216_15@tests.mozilla.org",
null, file);
throw "Update check should have failed";
} catch (e) {
equal(e.status, AddonUpdateChecker.ERROR_PARSE_ERROR);
}
});
}
}
});
function run_test_3() {
AddonUpdateChecker.checkForUpdates("test_bug378216_7@tests.mozilla.org",
updateKey,
"http://localhost:4444/data/test_updatecheck.rdf", {
onUpdateCheckComplete: function(updates) {
do_throw("Expected the update check to fail");
},
add_task(function* () {
for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) {
let updates = yield checkUpdates("ignore-compat@tests.mozilla.org",
null, file);
equal(updates.length, 3);
let update = AddonUpdateChecker.getNewestCompatibleUpdate(
updates, null, null, true);
notEqual(update, null);
equal(update.version, 2);
}
});
onUpdateCheckError: function(status) {
run_test_4();
}
});
}
add_task(function* () {
for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) {
let updates = yield checkUpdates("compat-override@tests.mozilla.org",
null, file);
equal(updates.length, 3);
let overrides = [{
type: "incompatible",
minVersion: 1,
maxVersion: 2,
appID: "xpcshell@tests.mozilla.org",
appMinVersion: 0.1,
appMaxVersion: 0.2
}, {
type: "incompatible",
minVersion: 2,
maxVersion: 2,
appID: "xpcshell@tests.mozilla.org",
appMinVersion: 1,
appMaxVersion: 2
}];
let update = AddonUpdateChecker.getNewestCompatibleUpdate(
updates, null, null, true, false, overrides);
notEqual(update, null);
equal(update.version, 1);
}
});
function run_test_4() {
AddonUpdateChecker.checkForUpdates("test_bug378216_8@tests.mozilla.org",
updateKey,
"http://localhost:4444/data/test_updatecheck.rdf", {
onUpdateCheckComplete: function(updates) {
do_check_eq(updates.length, 1);
do_check_false("updateURL" in updates[0]);
run_test_5();
},
onUpdateCheckError: function(status) {
do_throw("Update check failed with status " + status);
}
});
}
function run_test_5() {
AddonUpdateChecker.checkForUpdates("test_bug378216_9@tests.mozilla.org",
updateKey,
"http://localhost:4444/data/test_updatecheck.rdf", {
onUpdateCheckComplete: function(updates) {
do_check_eq(updates.length, 1);
do_check_eq(updates[0].version, "2.0");
do_check_true("updateURL" in updates[0]);
run_test_6();
},
onUpdateCheckError: function(status) {
do_throw("Update check failed with status " + status);
}
});
}
function run_test_6() {
AddonUpdateChecker.checkForUpdates("test_bug378216_10@tests.mozilla.org",
updateKey,
"http://localhost:4444/data/test_updatecheck.rdf", {
onUpdateCheckComplete: function(updates) {
do_check_eq(updates.length, 1);
do_check_eq(updates[0].version, "2.0");
do_check_true("updateURL" in updates[0]);
run_test_7();
},
onUpdateCheckError: function(status) {
do_throw("Update check failed with status " + status);
}
});
}
function run_test_7() {
AddonUpdateChecker.checkForUpdates("test_bug378216_11@tests.mozilla.org",
updateKey,
"http://localhost:4444/data/test_updatecheck.rdf", {
onUpdateCheckComplete: function(updates) {
do_check_eq(updates.length, 1);
do_check_eq(updates[0].version, "2.0");
do_check_true("updateURL" in updates[0]);
run_test_8();
},
onUpdateCheckError: function(status) {
do_throw("Update check failed with status " + status);
}
});
}
function run_test_8() {
AddonUpdateChecker.checkForUpdates("test_bug378216_12@tests.mozilla.org",
updateKey,
"http://localhost:4444/data/test_updatecheck.rdf", {
onUpdateCheckComplete: function(updates) {
do_check_eq(updates.length, 1);
do_check_false("updateURL" in updates[0]);
run_test_9();
},
onUpdateCheckError: function(status) {
do_throw("Update check failed with status " + status);
}
});
}
function run_test_9() {
AddonUpdateChecker.checkForUpdates("test_bug378216_13@tests.mozilla.org",
updateKey,
"http://localhost:4444/data/test_updatecheck.rdf", {
onUpdateCheckComplete: function(updates) {
do_check_eq(updates.length, 1);
do_check_eq(updates[0].version, "2.0");
do_check_true("updateURL" in updates[0]);
run_test_10();
},
onUpdateCheckError: function(status) {
do_throw("Update check failed with status " + status);
}
});
}
function run_test_10() {
AddonUpdateChecker.checkForUpdates("test_bug378216_14@tests.mozilla.org",
null,
"http://localhost:4444/data/test_updatecheck.rdf", {
onUpdateCheckComplete: function(updates) {
do_check_eq(updates.length, 0);
run_test_11();
},
onUpdateCheckError: function(status) {
do_throw("Update check failed with status " + status);
}
});
}
function run_test_11() {
AddonUpdateChecker.checkForUpdates("test_bug378216_15@tests.mozilla.org",
null,
"http://localhost:4444/data/test_updatecheck.rdf", {
onUpdateCheckComplete: function(updates) {
do_throw("Update check should have failed");
},
onUpdateCheckError: function(status) {
do_check_eq(status, AddonUpdateChecker.ERROR_PARSE_ERROR);
run_test_12();
}
});
}
function run_test_12() {
AddonUpdateChecker.checkForUpdates("ignore-compat@tests.mozilla.org",
null,
"http://localhost:4444/data/test_updatecheck.rdf", {
onUpdateCheckComplete: function(updates) {
do_check_eq(updates.length, 3);
let update = AddonUpdateChecker.getNewestCompatibleUpdate(updates,
null,
null,
true);
do_check_neq(update, null);
do_check_eq(update.version, 2);
run_test_13();
},
onUpdateCheckError: function(status) {
do_throw("Update check failed with status " + status);
}
});
}
function run_test_13() {
AddonUpdateChecker.checkForUpdates("compat-override@tests.mozilla.org",
null,
"http://localhost:4444/data/test_updatecheck.rdf", {
onUpdateCheckComplete: function(updates) {
do_check_eq(updates.length, 3);
let overrides = [{
type: "incompatible",
minVersion: 1,
maxVersion: 2,
appID: "xpcshell@tests.mozilla.org",
appMinVersion: 0.1,
appMaxVersion: 0.2
}, {
type: "incompatible",
minVersion: 2,
maxVersion: 2,
appID: "xpcshell@tests.mozilla.org",
appMinVersion: 1,
appMaxVersion: 2
}];
let update = AddonUpdateChecker.getNewestCompatibleUpdate(updates,
null,
null,
true,
false,
overrides);
do_check_neq(update, null);
do_check_eq(update.version, 1);
run_test_14();
},
onUpdateCheckError: function(status) {
do_throw("Update check failed with status " + status);
}
});
}
function run_test_14() {
AddonUpdateChecker.checkForUpdates("compat-strict-optin@tests.mozilla.org",
null,
"http://localhost:4444/data/test_updatecheck.rdf", {
onUpdateCheckComplete: function(updates) {
do_check_eq(updates.length, 1);
let update = AddonUpdateChecker.getNewestCompatibleUpdate(updates,
null,
null,
true,
false);
do_check_eq(update, null);
end_test();
},
onUpdateCheckError: function(status) {
do_throw("Update check failed with status " + status);
}
});
}
add_task(function* () {
for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) {
let updates = yield checkUpdates("compat-strict-optin@tests.mozilla.org",
null, file);
equal(updates.length, 1);
let update = AddonUpdateChecker.getNewestCompatibleUpdate(
updates, null, null, true, false);
equal(update, null);
}
});

View File

@ -279,6 +279,7 @@ skip-if = os == "android"
# Bug 676992: test consistently hangs on Android
skip-if = os == "android"
run-sequentially = Uses hardcoded ports in xpi files.
[test_json_updatecheck.js]
[test_updateid.js]
# Bug 676992: test consistently hangs on Android
skip-if = os == "android"