Bug 1009816 - Firefox desktop: openh264 updates: check, download, install. r=rstrong, gfritzsche a=kwierso

This commit is contained in:
Brian R. Bondy 2014-07-17 21:46:10 -04:00
parent fce4d7a57d
commit db8a57e7d8
7 changed files with 1734 additions and 0 deletions

View File

@ -139,6 +139,8 @@ pref("app.update.cert.maxErrors", 5);
// when the |app.update.cert.checkAttributes| preference is set to false. Also,
// the |app.update.url.override| preference should ONLY be used for testing.
// IMPORTANT! metro.js should also be updated for updates to certs.X.issuerName
// IMPORTANT! media.gmp-manager.certs.* prefs should also be updated if these
// are updated.
// Non-release builds (Nightly, Aurora, etc.) have been switched over to aus4.mozilla.org.
// This condition protects us against accidentally using it for release builds.
@ -1590,6 +1592,45 @@ pref("ui.key.menuAccessKeyFocuses", true);
// Encrypted media extensions.
pref("media.eme.enabled", false);
// GMPInstallManager prefs
// Enables some extra logging (can reduce performance)
pref("media.gmp-manager.log", false);
// User-settable override to media.gmp-manager.url for testing purposes.
//pref("media.gmp-manager.url.override", "");
// Update service URL for GMP install/updates:
pref("media.gmp-manager.url", "https://aus4.mozilla.org/update/3/GMP/%VERSION%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/update.xml");
// When |media.gmp-manager.cert.requireBuiltIn| is true or not specified the
// final certificate and all certificates the connection is redirected to before
// the final certificate for the url specified in the |media.gmp-manager.url|
// preference must be built-in.
pref("media.gmp-manager.cert.requireBuiltIn", true);
// The |media.gmp-manager.certs.| preference branch contains branches that are
// sequentially numbered starting at 1 that contain attribute name / value
// pairs for the certificate used by the server that hosts the update xml file
// as specified in the |media.gmp-manager.url| preference. When these preferences are
// present the following conditions apply for a successful update check:
// 1. the uri scheme must be https
// 2. the preference name must exist as an attribute name on the certificate and
// the value for the name must be the same as the value for the attribute name
// on the certificate.
// If these conditions aren't met it will be treated the same as when there is
// no update available. This validation will not be performed when the
// |media.gmp-manager.url.override| user preference has been set for testing updates or
// when the |media.gmp-manager.cert.checkAttributes| preference is set to false. Also,
// the |media.gmp-manager.url.override| preference should ONLY be used for testing.
// IMPORTANT! app.update.certs.* prefs should also be updated if these
// are updated.
pref("media.gmp-manager.cert.checkAttributes", true);
pref("media.gmp-manager.certs.1.issuerName", "CN=DigiCert Secure Server CA,O=DigiCert Inc,C=US");
pref("media.gmp-manager.certs.1.commonName", "aus4.mozilla.org");
pref("media.gmp-manager.certs.2.issuerName", "CN=Thawte SSL CA,O=\"Thawte, Inc.\",C=US");
pref("media.gmp-manager.certs.2.commonName", "aus4.mozilla.org");
// Delete HTTP cache v2 data of users that didn't opt-in manually
pref("browser.cache.auto_delete_cache_version", 1);
// Play with different values of the decay time and get telemetry,

View File

@ -21,6 +21,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu",
"resource://gre/modules/CharsetMenu.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
"resource://gre/modules/ShortcutUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "GMPInstallManager",
"resource://gre/modules/GMPInstallManager.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "gDNSService",
"@mozilla.org/network/dns-service;1",
@ -1316,6 +1318,14 @@ var gBrowserInit = {
WindowsPrefSync.init();
}
// Delay this a minute because there's no rush
setTimeout(() => {
this.gmpInstallManager = new GMPInstallManager();
// We don't really care about the results, if somenoe is interested they
// can check the log.
this.gmpInstallManager.simpleCheckAndInstall();
}, 1000 * 60);
SessionStore.promiseInitialized.then(() => {
// Bail out if the window has been closed in the meantime.
if (window.closed) {
@ -1465,6 +1475,9 @@ var gBrowserInit = {
if (typeof WindowsPrefSync !== 'undefined') {
WindowsPrefSync.uninit();
}
if (this.gmpInstallManager) {
this.gmpInstallManager.uninit();
}
BrowserOffline.uninit();
OfflineApps.uninit();

View File

@ -40,6 +40,8 @@ user_pref("media.volume_scale", "0.01");
user_pref("security.warn_viewing_mixed", false);
user_pref("app.update.enabled", false);
user_pref("app.update.staging.enabled", false);
// Make sure GMPInstallManager won't hit the network.
user_pref("media.gmp-manager.url", "https://%(server)s/dummy.xml");
user_pref("browser.panorama.experienced_first_run", true); // Assume experienced
user_pref("dom.w3c_touch_events.enabled", 1);
user_pref("dom.undo_manager.enabled", true);

View File

@ -0,0 +1,951 @@
/* 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";
this.EXPORTED_SYMBOLS = [];
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu, manager: Cm} =
Components;
// Chunk size for the incremental downloader
const DOWNLOAD_CHUNK_BYTES_SIZE = 300000;
// Incremental downloader interval
const DOWNLOAD_INTERVAL = 0;
// 1 day default
const DEFAULT_SECONDS_BETWEEN_CHECKS = 60 * 60 * 24;
const OPEN_H264_ID = "openh264-plugin@cisco.com";
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/ctypes.jsm");
this.EXPORTED_SYMBOLS = ["GMPInstallManager", "GMPExtractor", "GMPDownloader",
"GMPAddon", "GMPPrefs"];
var gLocale = null;
// Shared code for suppressing bad cert dialogs
XPCOMUtils.defineLazyGetter(this, "gCertUtils", function() {
let temp = { };
Cu.import("resource://gre/modules/CertUtils.jsm", temp);
return temp;
});
XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
"resource://gre/modules/UpdateChannel.jsm");
// Used to determine if logging should be enabled
XPCOMUtils.defineLazyGetter(this, "gLogEnabled", function() {
return GMPPrefs.get(GMPPrefs.KEY_LOG_ENABLED);
});
function getScopedLogger(prefix) {
let logger = Log.repository.getLogger(prefix);
if (gLogEnabled) {
logger.level = Log.Level.Debug;
let appender = new Log.DumpAppender();
logger.addAppender(appender);
}
return logger;
}
/**
* Manages preferences for GMP addons
*/
let GMPPrefs = {
/**
* Obtains the specified preference in relation to the specified addon
* @param key The GMPPrefs key value to use
* @param addon The addon to scope the preference to
* @param defaultValue The default value if no preference exists
* @return The obtained preference value, or the defaultVlaue if none exists
*/
get: function(key, addon, defaultValue) {
if (key === GMPPrefs.KEY_APP_DISTRIBUTION ||
key === GMPPrefs.KEY_APP_DISTRIBUTION_VERSION) {
let prefValue = "default";
try {
prefValue = Services.prefs.getDefaultBranch(null).getCharPref(key);
} catch (e) {
// use default when pref not found
}
return prefValue;
}
return Preferences.get(this._getPrefKey(key, addon), defaultValue);
},
/**
* Sets the specified preference in relation to the specified addon
* @param key The GMPPrefs key value to use
* @param val The value to set
* @param addon The addon to scope the preference to
*/
set: function(key, val, addon) {
let log = getScopedLogger("GMPPrefs.set");
log.info("Setting pref: " + this._getPrefKey(key, addon) +
" to value: " + val);
return Preferences.set(this._getPrefKey(key, addon), val);
},
_getPrefKey: function(key, addon) {
return key.replace("{0}", addon || "");
},
/**
* List of keys which can be used in get and set
*/
KEY_LOG_ENABLED: "media.gmp-manager.log",
KEY_ADDON_LAST_UPDATE: "media.{0}.lastUpdate",
KEY_ADDON_PATH: "media.{0}.path",
KEY_ADDON_VERSION: "media.{0}.version",
KEY_URL: "media.gmp-manager.url",
KEY_URL_OVERRIDE: "media.gmp-manager.url.override",
KEY_CERT_CHECKATTRS: "media.gmp-manager.cert.checkAttributes",
KEY_CERT_REQUIREBUILTIN: "media.gmp-manager.cert.requireBuiltIn",
KEY_UPDATE_LAST_CHECK: "media.gmp-manager.lastCheck",
KEY_UPDATE_SECONDS_BETWEEN_CHECKS: "media.gmp-manager.secondsBetweenChecks",
KEY_APP_DISTRIBUTION: "distribution.id",
KEY_APP_DISTRIBUTION_VERSION: "distribution.version",
CERTS_BRANCH: "media.gmp-manager.certs."
};
// This is copied directly from nsUpdateService.js
// It is used for calculating the URL string w/ var replacement.
// TODO: refactor this out somewhere else
XPCOMUtils.defineLazyGetter(this, "gOSVersion", function aus_gOSVersion() {
let osVersion;
let sysInfo = Cc["@mozilla.org/system-info;1"].
getService(Ci.nsIPropertyBag2);
try {
osVersion = sysInfo.getProperty("name") + " " + sysInfo.getProperty("version");
}
catch (e) {
LOG("gOSVersion - OS Version unknown: updates are not possible.");
}
if (osVersion) {
#ifdef XP_WIN
const BYTE = ctypes.uint8_t;
const WORD = ctypes.uint16_t;
const DWORD = ctypes.uint32_t;
const WCHAR = ctypes.jschar;
const BOOL = ctypes.int;
// This structure is described at:
// http://msdn.microsoft.com/en-us/library/ms724833%28v=vs.85%29.aspx
const SZCSDVERSIONLENGTH = 128;
const OSVERSIONINFOEXW = new ctypes.StructType('OSVERSIONINFOEXW',
[
{dwOSVersionInfoSize: DWORD},
{dwMajorVersion: DWORD},
{dwMinorVersion: DWORD},
{dwBuildNumber: DWORD},
{dwPlatformId: DWORD},
{szCSDVersion: ctypes.ArrayType(WCHAR, SZCSDVERSIONLENGTH)},
{wServicePackMajor: WORD},
{wServicePackMinor: WORD},
{wSuiteMask: WORD},
{wProductType: BYTE},
{wReserved: BYTE}
]);
// This structure is described at:
// http://msdn.microsoft.com/en-us/library/ms724958%28v=vs.85%29.aspx
const SYSTEM_INFO = new ctypes.StructType('SYSTEM_INFO',
[
{wProcessorArchitecture: WORD},
{wReserved: WORD},
{dwPageSize: DWORD},
{lpMinimumApplicationAddress: ctypes.voidptr_t},
{lpMaximumApplicationAddress: ctypes.voidptr_t},
{dwActiveProcessorMask: DWORD.ptr},
{dwNumberOfProcessors: DWORD},
{dwProcessorType: DWORD},
{dwAllocationGranularity: DWORD},
{wProcessorLevel: WORD},
{wProcessorRevision: WORD}
]);
let kernel32 = false;
try {
kernel32 = ctypes.open("Kernel32");
} catch (e) {
LOG("gOSVersion - Unable to open kernel32! " + e);
osVersion += ".unknown (unknown)";
}
if(kernel32) {
try {
// Get Service pack info
try {
let GetVersionEx = kernel32.declare("GetVersionExW",
ctypes.default_abi,
BOOL,
OSVERSIONINFOEXW.ptr);
let winVer = OSVERSIONINFOEXW();
winVer.dwOSVersionInfoSize = OSVERSIONINFOEXW.size;
if(0 !== GetVersionEx(winVer.address())) {
osVersion += "." + winVer.wServicePackMajor
+ "." + winVer.wServicePackMinor;
} else {
LOG("gOSVersion - Unknown failure in GetVersionEX (returned 0)");
osVersion += ".unknown";
}
} catch (e) {
LOG("gOSVersion - error getting service pack information. Exception: " + e);
osVersion += ".unknown";
}
// Get processor architecture
let arch = "unknown";
try {
let GetNativeSystemInfo = kernel32.declare("GetNativeSystemInfo",
ctypes.default_abi,
ctypes.void_t,
SYSTEM_INFO.ptr);
let sysInfo = SYSTEM_INFO();
// Default to unknown
sysInfo.wProcessorArchitecture = 0xffff;
GetNativeSystemInfo(sysInfo.address());
switch(sysInfo.wProcessorArchitecture) {
case 9:
arch = "x64";
break;
case 6:
arch = "IA64";
break;
case 0:
arch = "x86";
break;
}
} catch (e) {
LOG("gOSVersion - error getting processor architecture. Exception: " + e);
} finally {
osVersion += " (" + arch + ")";
}
} finally {
kernel32.close();
}
}
#endif
try {
osVersion += " (" + sysInfo.getProperty("secondaryLibrary") + ")";
}
catch (e) {
// Not all platforms have a secondary widget library, so an error is nothing to worry about.
}
osVersion = encodeURIComponent(osVersion);
}
return osVersion;
});
// This is copied directly from nsUpdateService.js
// It is used for calculating the URL string w/ var replacement.
// TODO: refactor this out somewhere else
XPCOMUtils.defineLazyGetter(this, "gABI", function aus_gABI() {
let abi = null;
try {
abi = Services.appinfo.XPCOMABI;
}
catch (e) {
LOG("gABI - XPCOM ABI unknown: updates are not possible.");
}
#ifdef XP_MACOSX
// Mac universal build should report a different ABI than either macppc
// or mactel.
let macutils = Cc["@mozilla.org/xpcom/mac-utils;1"].
getService(Ci.nsIMacUtils);
if (macutils.isUniversalBinary)
abi += "-u-" + macutils.architecturesInBinary;
#ifdef MOZ_SHARK
// Disambiguate optimised and shark nightlies
abi += "-shark"
#endif
#endif
return abi;
});
/**
* Provides an easy API for downloading and installing GMP Addons
*/
function GMPInstallManager() {
}
/**
* Temp file name used for downloading
*/
GMPInstallManager.prototype = {
/**
* Obtains a URL with replacement of vars
*/
_getURL: function() {
let log = getScopedLogger("GMPInstallManager._getURL");
// Use the override URL if it is specified. The override URL is just like
// the normal URL but it does not check the cert.
let url = GMPPrefs.get(GMPPrefs.KEY_URL_OVERRIDE);
if (url) {
log.info("Using override url: " + url);
} else {
url = GMPPrefs.get(GMPPrefs.KEY_URL);
log.info("Using url: " + url);
}
url =
url.replace(/%PRODUCT%/g, Services.appinfo.name)
.replace(/%VERSION%/g, Services.appinfo.version)
.replace(/%BUILD_ID%/g, Services.appinfo.appBuildID)
.replace(/%BUILD_TARGET%/g, Services.appinfo.OS + "_" + gABI)
.replace(/%OS_VERSION%/g, gOSVersion);
if (/%LOCALE%/.test(url)) {
// TODO: Get the real local, does it actually matter for GMP plugins?
url = url.replace(/%LOCALE%/g, "en-US");
}
url =
url.replace(/%CHANNEL%/g, UpdateChannel.get())
.replace(/%PLATFORM_VERSION%/g, Services.appinfo.platformVersion)
.replace(/%DISTRIBUTION%/g,
GMPPrefs.get(GMPPrefs.KEY_APP_DISTRIBUTION))
.replace(/%DISTRIBUTION_VERSION%/g,
GMPPrefs.get(GMPPrefs.KEY_APP_DISTRIBUTION_VERSION))
.replace(/\+/g, "%2B");
log.info("Using url (with replacement): " + url);
return url;
},
/**
* Performs an addon check.
* @return a promise which will be resolved or rejected.
* The promise is resolved with an array of GMPAddons
* The promise is rejected with an object with properties:
* target: The XHR request object
* status: The HTTP status code
* type: Sometimes specifies type of rejection
*/
checkForAddons: function() {
let log = getScopedLogger("GMPInstallManager.checkForAddons");
if (this._deferred) {
log.error("checkForAddons already called");
return Promise.reject({type: "alreadycalled"});
}
this._deferred = Promise.defer();
let url = this._getURL();
this._request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsISupports);
// This is here to let unit test code override XHR
if (this._request.wrappedJSObject) {
this._request = this._request.wrappedJSObject;
}
this._request.open("GET", url, true);
let allowNonBuiltIn = !GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS,
undefined, 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");
// The Cache-Control header is only interpreted by proxies and the
// final destination. It does not help if a resource is already
// cached locally.
this._request.setRequestHeader("Cache-Control", "no-cache");
// HTTP/1.0 servers might not implement Cache-Control and
// might only implement Pragma: no-cache
this._request.setRequestHeader("Pragma", "no-cache");
this._request.addEventListener("error", this.onErrorXML.bind(this) ,false);
this._request.addEventListener("load", this.onLoadXML.bind(this), false);
log.info("sending request to: " + url);
this._request.send(null);
return this._deferred.promise;
},
/**
* Installs the specified addon and calls a callback when done.
* @param gmpAddon The GMPAddon object to install
* @return a promise which will be resolved or rejected
* The promise will resolve with an array of paths that were extracted
* The promise will reject with an error object:
* target: The XHR request object
* status: The HTTP status code
* type: A string to represent the type of error
* downloaderr, or verifyerr
*/
installAddon: function(gmpAddon) {
if (this._deferred) {
log.error("checkForAddons already called");
return Promise.reject({type: "alreadycalled"});
}
this.gmpDownloader = new GMPDownloader(gmpAddon);
return this.gmpDownloader.start();
},
_getTimeSinceLastCheck: function() {
let now = Math.round(Date.now() / 1000);
// Default to 0 here because `now - 0` will be returned later if that case
// is hit. We want a large value so a check will occur.
let lastCheck = GMPPrefs.get(GMPPrefs.KEY_UPDATE_LAST_CHECK,
undefined, 0);
// Handle clock jumps, return now since we want it to represent
// a lot of time has passed since the last check.
if (now < lastCheck) {
return now;
}
return now - lastCheck;
},
_updateLastCheck: function() {
let now = Math.round(Date.now() / 1000);
GMPPrefs.set(GMPPrefs.KEY_UPDATE_LAST_CHECK, now);
},
/**
* Wrapper for checkForAddons and installaddon.
* Will only install if not already installed and will log the results.
*/
simpleCheckAndInstall: function() {
let log = getScopedLogger("GMPInstallManager.simpleCheckAndInstall");
let secondsBetweenChecks =
GMPPrefs.get(GMPPrefs.KEY_UPDATE_SECONDS_BETWEEN_CHECKS, undefined,
DEFAULT_SECONDS_BETWEEN_CHECKS)
let secondsSinceLast = this._getTimeSinceLastCheck();
log.info("Last check was: " + secondsSinceLast +
" seconds ago, minimum seconds: " + secondsBetweenChecks);
if (secondsBetweenChecks > secondsSinceLast) {
log.info("Will not check for updates.");
return Promise.resolve();
}
let promise = this.checkForAddons();
promise.then(gmpAddons => {
this._updateLastCheck();
log.info("Found " + gmpAddons.length + " addons advertised.");
let addonsToInstall = gmpAddons.filter(gmpAddon => {
log.info("Found addon: " + gmpAddon.toString());
return gmpAddon.isValid && gmpAddon.isOpenH264 &&
!gmpAddon.isInstalled
});
if (!addonsToInstall.length) {
log.info("No new addons to install, returning");
return;
}
addonsToInstall.forEach(gmpAddon => {
promise = this.installAddon(gmpAddon);
promise.then(extractedPaths => {
// installed!
log.info("Addon installed successfully: " + gmpAddon.toString());
}, () => {
if (!GMPPrefs.get(GMPPrefs.KEY_LOG_ENABLED)) {
Cu.reportError(gmpAddon.toString() + " could not be installed. Enable " +
GMPPrefs.KEY_LOG_ENABLED + " for details!");
}
log.error("Could not install addon: " + gmpAddon.toString());
});
});
});
},
/**
* Makes sure everything is cleaned up
*/
uninit: function() {
let log = getScopedLogger("GMPDownloader.uninit");
if (this._request) {
log.info("Aborting request");
this._request.abort();
}
if (this._deferred) {
log.info("Rejecting deferred");
this._deferred.reject({type: "uninitialized"});
}
log.info("Done cleanup");
},
/**
* If set to true, specifies to leave the temporary downloaded zip file.
* This is useful for tests.
*/
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");
log.info("request completed downloading document");
let certs = null;
if (!Services.prefs.prefHasUserValue(GMPPrefs.KEY_URL_OVERRIDE) &&
GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS, undefined, true)) {
certs = gCertUtils.readCertPrefs(GMPPrefs.CERTS_BRANCH);
}
let allowNonBuiltIn = !GMPPrefs.get(GMPPrefs.KEY_CERT_REQUIREBUILTIN,
undefined, true);
log.info("allowNonBuiltIn: " + allowNonBuiltIn);
gCertUtils.checkCert(this._request.channel, allowNonBuiltIn, certs);
this.parseResponseXML();
},
/**
* Returns the status code for the XMLHttpRequest
*/
_getChannelStatus: function(request) {
let log = getScopedLogger("GMPInstallManager._getChannelStatus");
let status = 0;
try {
status = request.status;
log.info("request.status is: " + request.status);
}
catch (e) {
}
if (status == 0) {
status = request.channel.QueryInterface(Ci.nsIRequest).status;
}
return status;
},
/**
* There was an error of some kind during the XMLHttpRequest
* @param event The nsIDOMEvent for the error
*/
onErrorXML: function(event) {
let log = getScopedLogger("GMPInstallManager.onErrorXML");
let request = event.target;
let status = this._getChannelStatus(request);
let message = "request.status: " + status;
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;
}
},
};
/**
* Used to construct a single GMP addon
* GMPAddon objects are returns from GMPInstallManager.checkForAddons
* GMPAddon objects can also be used in calls to GMPInstallManager.installAddon
*
* @param gmpAddon The AUS response XML's DOM element `addon`
*/
function GMPAddon(gmpAddon) {
let log = getScopedLogger("GMPAddon.constructor");
gmpAddon.QueryInterface(Ci.nsIDOMElement);
["id", "URL", "hashFunction",
"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());
}
/**
* 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 = {
/**
* Returns a string representation of the addon
*/
toString: function() {
return this.id + " (" +
"isValid: " + this.isValid +
", isInstalled: " + this.isInstalled +
", isOpenH264: " + this.isOpenH264 +
", hashFunction: " + this.hashFunction+
", hashValue: " + this.hashValue +
(this.size !== undefined ? ", size: " + this.size : "" ) +
")";
},
/**
* If all the fields aren't specified don't consider this addon valid
* @return true if the addon is parsed and valid
*/
get isValid() {
return this.id && this.URL && this.version &&
this.hashFunction && !!this.hashValue;
},
/**
* Open H264 has special handling.
* @return true if the plugin is the openh264 plugin
*/
get isOpenH264() {
return this.id === OPEN_H264_ID;
},
get isInstalled() {
return this.version &&
GMPPrefs.get(GMPPrefs.KEY_ADDON_VERSION, this.id) === this.version;
}
};
/**
* Constructs a GMPExtractor object which is used to extract a GMP zip
* into the specified location. (Which typically leties per platform)
* @param zipPath The path on disk of the zip file to extract
*/
function GMPExtractor(zipPath, installToDirPath) {
this.zipPath = zipPath;
this.installToDirPath = installToDirPath;
}
GMPExtractor.prototype = {
/**
* Obtains a list of all the entries in a zipfile in the format of *.*.
* This also includes files inside directories.
*
* @param zipReader the nsIZipReader to check
* @return An array of string name entries which can be used
* in nsIZipReader.extract
*/
_getZipEntries: function(zipReader) {
let entries = [];
let enumerator = zipReader.findEntries("*.*");
while (enumerator.hasMore()) {
entries.push(enumerator.getNext());
}
return entries;
},
/**
* Installs the this.zipPath contents into the directory used to store GMP
* addons for the current platform.
*
* @return a promise which will be resolved or rejected
* See GMPInstallManager.installAddon for resolve/rejected info
*/
install: function() {
try {
let log = getScopedLogger("GMPExtractor.install");
this._deferred = Promise.defer();
log.info("Installing " + this.zipPath + "...");
// Get the input zip file
let zipFile = Cc["@mozilla.org/file/local;1"].
createInstance(Ci.nsIFile);
zipFile.initWithPath(this.zipPath);
// Initialize a zipReader and obtain the entries
var zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
createInstance(Ci.nsIZipReader);
zipReader.open(zipFile)
let entries = this._getZipEntries(zipReader);
let extractedPaths = [];
// Extract each of the entries
entries.forEach(entry => {
// We don't need these types of files
if (entry.contains("__MACOSX")) {
return;
}
let outFile = Cc["@mozilla.org/file/local;1"].
createInstance(Ci.nsILocalFile);
outFile.initWithPath(this.installToDirPath);
outFile.appendRelativePath(entry);
// Make sure the directory hierarchy exists
if(!outFile.parent.exists()) {
outFile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
}
zipReader.extract(entry, outFile);
extractedPaths.push(outFile.path);
log.info(entry + " was successfully extracted to: " +
outFile.path);
});
zipReader.close();
if (!GMPInstallManager.overrideLeaveDownloadedZip) {
zipFile.remove(false);
}
log.info(this.zipPath + " was installed successfully");
this._deferred.resolve(extractedPaths);
} catch (e) {
if (zipReader) {
zipReader.close();
}
this._deferred.reject({
target: this,
status: e,
type: "exception"
});
}
return this._deferred.promise;
}
};
/**
* Constructs an object which downloads and initiates an install of
* the specified GMPAddon object.
* @param gmpAddon The addon to install.
*/
function GMPDownloader(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 = {
/**
* Starts the download process for an addon.
* @return a promise which will be resolved or rejected
* See GMPInstallManager.installAddon for resolve/rejected info
*/
start: function() {
let log = getScopedLogger("GMPDownloader.start");
this._deferred = Promise.defer();
if (!this._gmpAddon.isValid) {
log.info("gmpAddon is not valid, will not continue");
return Promise.reject({
target: this,
status: status,
type: "downloaderr"
});
}
let uri = Services.io.newURI(this._gmpAddon.URL, null, null);
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, gmpAddon.id);
installToDirPath.initWithPath(path);
log.info("install to directory path: " + installToDirPath.path);
let gmpInstaller = new GMPExtractor(zipPath, installToDirPath.path);
let installPromise = gmpInstaller.install();
installPromise.then(extractedPaths => {
// Success, set the prefs
let now = Math.round(Date.now() / 1000);
GMPPrefs.set(GMPPrefs.KEY_ADDON_LAST_UPDATE, now, gmpAddon.id);
GMPPrefs.set(GMPPrefs.KEY_ADDON_PATH,
installToDirPath.path, gmpAddon.id);
GMPPrefs.set(GMPPrefs.KEY_ADDON_VERSION, gmpAddon.version,
gmpAddon.id);
this._deferred.resolve(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;
}

View File

@ -60,6 +60,7 @@ EXTRA_JS_MODULES += [
EXTRA_PP_JS_MODULES += [
'CertUtils.jsm',
'GMPInstallManager.jsm',
'ResetProfile.jsm',
'Services.jsm',
'Troubleshoot.jsm',

View File

@ -0,0 +1,725 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu, manager: Cm} = Components;
const URL_HOST = "http://localhost";
Cu.import("resource://gre/modules/GMPInstallManager.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://testing-common/httpd.js");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Preferences.jsm")
do_get_profile();
function run_test() {Cu.import("resource://gre/modules/Preferences.jsm")
Preferences.set("media.gmp-manager.log", true);
run_next_test();
}
/**
* Tests that the helper used for preferences works correctly
*/
add_test(function test_prefs() {
let addon1 = "addon1", addon2 = "addon2";
GMPPrefs.set(GMPPrefs.KEY_LOG_ENABLED, true);
GMPPrefs.set(GMPPrefs.KEY_URL, "http://not-really-used");
GMPPrefs.set(GMPPrefs.KEY_URL_OVERRIDE, "http://not-really-used-2");
GMPPrefs.set(GMPPrefs.KEY_ADDON_LAST_UPDATE, "1", addon1);
GMPPrefs.set(GMPPrefs.KEY_ADDON_PATH, "2", addon1);
GMPPrefs.set(GMPPrefs.KEY_ADDON_VERSION, "3", addon1);
GMPPrefs.set(GMPPrefs.KEY_ADDON_LAST_UPDATE, "4", addon2);
GMPPrefs.set(GMPPrefs.KEY_ADDON_PATH, "5", addon2);
GMPPrefs.set(GMPPrefs.KEY_ADDON_VERSION, "6", addon2);
GMPPrefs.set(GMPPrefs.KEY_CERT_CHECKATTRS, true);
do_check_true(GMPPrefs.get(GMPPrefs.KEY_LOG_ENABLED));
do_check_eq(GMPPrefs.get(GMPPrefs.KEY_URL), "http://not-really-used");
do_check_eq(GMPPrefs.get(GMPPrefs.KEY_URL_OVERRIDE), "http://not-really-used-2");
do_check_eq(GMPPrefs.get(GMPPrefs.KEY_ADDON_LAST_UPDATE, addon1), "1");
do_check_eq(GMPPrefs.get(GMPPrefs.KEY_ADDON_PATH, addon1), "2");
do_check_eq(GMPPrefs.get(GMPPrefs.KEY_ADDON_VERSION, addon1), "3");
do_check_eq(GMPPrefs.get(GMPPrefs.KEY_ADDON_LAST_UPDATE, addon2), "4");
do_check_eq(GMPPrefs.get(GMPPrefs.KEY_ADDON_PATH, addon2), "5");
do_check_eq(GMPPrefs.get(GMPPrefs.KEY_ADDON_VERSION, addon2), "6");
do_check_true(GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS));
run_next_test();
});
/**
* Tests that an uninit without a check works fine
*/
add_test(function test_checkForAddons_noResponse() {
let installManager = new GMPInstallManager();
installManager.uninit();
run_next_test();
});
/**
* Tests that an uninit without an install works fine
*/
add_test(function test_checkForAddons_noResponse() {
overrideXHR(200, "");
let installManager = new GMPInstallManager();
let promise = installManager.checkForAddons();
promise.then(function(gmpAddons) {
do_throw("no repsonse should reject");
}, function(err) {
installManager.uninit();
});
run_next_test();
});
/**
* Tests that no response returned rejects
*/
add_test(function test_checkForAddons_noResponse() {
overrideXHR(200, "");
let installManager = new GMPInstallManager();
let promise = installManager.checkForAddons();
promise.then(function(gmpAddons) {
do_throw("no repsonse should reject");
}, function(err) {
do_check_true(!!err);
installManager.uninit();
run_next_test();
});
});
/**
* Tests that no addons element returned resolves with no addons
*/
add_task(function test_checkForAddons_noAddonsElement() {
overrideXHR(200, "<updates></updates>");
let installManager = new GMPInstallManager();
let gmpAddons = yield installManager.checkForAddons();
do_check_eq(gmpAddons.length, 0);
installManager.uninit();
});
/**
* Tests that empty addons element returned resolves with no addons
*/
add_task(function test_checkForAddons_noAddonsElement() {
overrideXHR(200, "<updates><addons/></updates>");
let installManager = new GMPInstallManager();
let gmpAddons = yield installManager.checkForAddons();
do_check_eq(gmpAddons.length, 0);
installManager.uninit();
});
/**
* Tests that a response with the wrong root element rejects
*/
add_test(function test_checkForAddons_wrongResponseXML() {
overrideXHR(200, "<digits_of_pi>3.141592653589793....</digits_of_pi>");
let installManager = new GMPInstallManager();
let promise = installManager.checkForAddons();
promise.then(function(err, gmpAddons) {
do_throw("response with the wrong root element should reject");
}, function(err) {
do_check_true(!!err);
installManager.uninit();
run_next_test();
});
});
/**
* Tests that a 404 error works as expected
*/
add_test(function test_checkForAddons_404Error() {
overrideXHR(404, "");
let installManager = new GMPInstallManager();
let promise = installManager.checkForAddons();
promise.then(function(gmpAddons) {
do_throw("404 response should reject");
}, function(err) {
do_check_true(!!err);
do_check_eq(err.status, 404);
installManager.uninit();
run_next_test();
});
});
/**
* Tests that gettinga a funky non XML response works as expected
*/
add_test(function test_checkForAddons_notXML() {
overrideXHR(200, "3.141592653589793....");
let installManager = new GMPInstallManager();
let promise = installManager.checkForAddons();
promise.then(function(gmpAddons) {
do_throw("non XML response should reject");
}, function(err) {
do_check_true(!!err);
installManager.uninit();
run_next_test();
});
});
/**
* Tests that getting a response with a single addon works as expected
*/
add_test(function test_checkForAddons_singleAddonNoUpdates() {
let responseXML =
"<?xml version=\"1.0\"?>" +
"<updates>" +
" <addons>" +
" <addon id=\"openh264-plugin@cisco.com\"" +
" URL=\"http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip\"" +
" hashFunction=\"sha256\"" +
" hashValue=\"1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
" version=\"1.1\"/>" +
" </addons>" +
"</updates>"
overrideXHR(200, responseXML);
let installManager = new GMPInstallManager();
let promise = installManager.checkForAddons();
promise.then(function(gmpAddons) {
do_check_eq(gmpAddons.length, 1);
let gmpAddon= gmpAddons[0];
do_check_eq(gmpAddon.id, "openh264-plugin@cisco.com");
do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip");
do_check_eq(gmpAddon.hashFunction, "sha256");
do_check_eq(gmpAddon.hashValue, "1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee");
do_check_eq(gmpAddon.version, "1.1");
do_check_eq(gmpAddon.size, undefined);
do_check_true(gmpAddon.isValid);
do_check_true(gmpAddon.isOpenH264);
do_check_false(gmpAddon.isInstalled);
installManager.uninit();
run_next_test();
}, function(err) {
do_throw("1 addon found should not reject");
});
});
/**
* Tests that getting a response with a single addon with the optional size
* attribute parses as expected.
*/
add_test(function test_checkForAddons_singleAddonNoUpdates() {
let responseXML =
"<?xml version=\"1.0\"?>" +
"<updates>" +
" <addons>" +
" <addon id=\"openh264-plugin-no-at-symbol\"" +
" URL=\"http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip\"" +
" hashFunction=\"sha256\"" +
" size=\"42\"" +
" hashValue=\"1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
" version=\"1.1\"/>" +
" </addons>" +
"</updates>"
overrideXHR(200, responseXML);
let installManager = new GMPInstallManager();
let promise = installManager.checkForAddons();
promise.then(function(gmpAddons) {
do_check_eq(gmpAddons.length, 1);
let gmpAddon= gmpAddons[0];
do_check_eq(gmpAddon.id, "openh264-plugin-no-at-symbol");
do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip");
do_check_eq(gmpAddon.hashFunction, "sha256");
do_check_eq(gmpAddon.hashValue, "1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee");
do_check_eq(gmpAddon.size, 42);
do_check_eq(gmpAddon.version, "1.1");
do_check_true(gmpAddon.isValid);
do_check_false(gmpAddon.isOpenH264);
do_check_false(gmpAddon.isInstalled);
installManager.uninit();
run_next_test();
}, function(err) {
do_throw("1 addon found should not reject");
});
});
/**
* Tests that checking for multiple addons work correctly.
* Also tests that invalid addons work correctly.
*/
add_test(function test_checkForAddons_multipleAddonNoUpdatesSomeInvalid() {
let responseXML =
"<?xml version=\"1.0\"?>" +
"<updates>" +
" <addons>" +
// valid openh264
" <addon id=\"openh264-plugin@cisco.com\"" +
" URL=\"http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip\"" +
" hashFunction=\"sha256\"" +
" hashValue=\"1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
" version=\"1.1\"/>" +
// valid not openh264
" <addon id=\"NOT-openh264-plugin@cisco.com\"" +
" URL=\"http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip\"" +
" hashFunction=\"sha512\"" +
" hashValue=\"141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
" version=\"9.1\"/>" +
// noid
" <addon notid=\"NOT-openh264-plugin@cisco.com\"" +
" URL=\"http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip\"" +
" hashFunction=\"sha512\"" +
" hashValue=\"141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
" version=\"9.1\"/>" +
// no URL
" <addon id=\"NOT-openh264-plugin@cisco.com\"" +
" notURL=\"http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip\"" +
" hashFunction=\"sha512\"" +
" hashValue=\"141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
" version=\"9.1\"/>" +
// no hash function
" <addon id=\"NOT-openh264-plugin@cisco.com\"" +
" URL=\"http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip\"" +
" nothashFunction=\"sha512\"" +
" hashValue=\"141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
" version=\"9.1\"/>" +
// no hash function
" <addon id=\"NOT-openh264-plugin@cisco.com\"" +
" URL=\"http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip\"" +
" hashFunction=\"sha512\"" +
" nothashValue=\"141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
" version=\"9.1\"/>" +
// not version
" <addon id=\"NOT-openh264-plugin@cisco.com\"" +
" URL=\"http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip\"" +
" hashFunction=\"sha512\"" +
" hashValue=\"141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
" notversion=\"9.1\"/>" +
" </addons>" +
"</updates>"
overrideXHR(200, responseXML);
let installManager = new GMPInstallManager();
let promise = installManager.checkForAddons();
promise.then(function(gmpAddons) {
do_check_eq(gmpAddons.length, 7);
let gmpAddon= gmpAddons[0];
do_check_eq(gmpAddon.id, "openh264-plugin@cisco.com");
do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip");
do_check_eq(gmpAddon.hashFunction, "sha256");
do_check_eq(gmpAddon.hashValue, "1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee");
do_check_eq(gmpAddon.version, "1.1");
do_check_true(gmpAddon.isValid);
do_check_true(gmpAddon.isOpenH264);
do_check_false(gmpAddon.isInstalled);
gmpAddon= gmpAddons[1];
do_check_eq(gmpAddon.id, "NOT-openh264-plugin@cisco.com");
do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip");
do_check_eq(gmpAddon.hashFunction, "sha512");
do_check_eq(gmpAddon.hashValue, "141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee");
do_check_eq(gmpAddon.version, "9.1");
do_check_true(gmpAddon.isValid);
do_check_false(gmpAddon.isOpenH264);
do_check_false(gmpAddon.isInstalled);
for (let i = 2; i < gmpAddons.length; i++) {
do_check_false(gmpAddons[i].isValid);
do_check_false(gmpAddons[i].isInstalled);
}
installManager.uninit();
run_next_test();
}, function(err) {
do_throw("multiple addons found should not reject");
});
});
/**
* Tests that checking for addons when there are also updates available
* works as expected.
*/
add_test(function test_checkForAddons_updatesWithAddons() {
let responseXML =
"<?xml version=\"1.0\"?>" +
" <updates>" +
" <update type=\"minor\" displayVersion=\"33.0a1\" appVersion=\"33.0a1\" platformVersion=\"33.0a1\" buildID=\"20140628030201\">" +
" <patch type=\"complete\" URL=\"http://ftp.mozilla.org/pub/mozilla.org/firefox/nightly/2014/06/2014-06-28-03-02-01-mozilla-central/firefox-33.0a1.en-US.mac.complete.mar\" hashFunction=\"sha512\" hashValue=\"f3f90d71dff03ae81def80e64bba3e4569da99c9e15269f731c2b167c4fc30b3aed9f5fee81c19614120230ca333e73a5e7def1b8e45d03135b2069c26736219\" size=\"85249896\"/>" +
" </update>" +
" <addons>" +
" <addon id=\"openh264-plugin@cisco.com\"" +
" URL=\"http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip\"" +
" hashFunction=\"sha256\"" +
" hashValue=\"1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
" version=\"1.1\"/>" +
" </addons>" +
"</updates>"
overrideXHR(200, responseXML);
let installManager = new GMPInstallManager();
let promise = installManager.checkForAddons();
promise.then(function(gmpAddons) {
do_check_eq(gmpAddons.length, 1);
let gmpAddon= gmpAddons[0];
do_check_eq(gmpAddon.id, "openh264-plugin@cisco.com");
do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip");
do_check_eq(gmpAddon.hashFunction, "sha256");
do_check_eq(gmpAddon.hashValue, "1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee");
do_check_eq(gmpAddon.version, "1.1");
do_check_true(gmpAddon.isValid);
do_check_true(gmpAddon.isOpenH264);
do_check_false(gmpAddon.isInstalled);
installManager.uninit();
run_next_test();
}, function(err) {
do_throw("updates with addons should not reject");
});
});
/**
* Tests that installing found addons works as expected
*/
function test_checkForAddons_installAddon(id, includeSize,wantInstallReject) {
do_print("Running installAddon for includeSize: " + includeSize +
" and wantInstallReject: " + wantInstallReject);
let httpServer = new HttpServer();
let dir = FileUtils.getDir("TmpD", [], true);
httpServer.registerDirectory("/", dir);
httpServer.start(-1);
let testserverPort = httpServer.identity.primaryPort;
let zipFileName = "test_" + id + "_GMP.zip";
let zipURL = URL_HOST + ":" + testserverPort + "/" + zipFileName;
do_print("zipURL: " + zipURL);
let data = "e~=0.5772156649";
let zipFile = createNewZipFile(zipFileName, data);
let hashFunc = "sha256";
let expectedDigest = yield GMPDownloader.computeHash(hashFunc, zipFile);
let fileSize = zipFile.size;
if (wantInstallReject) {
fileSize = 1;
}
let responseXML =
"<?xml version=\"1.0\"?>" +
"<updates>" +
" <addons>" +
" <addon id=\"" + id + "-openh264-plugin@cisco.com\"" +
" URL=\"" + zipURL + "\"" +
" hashFunction=\"" + hashFunc + "\"" +
" hashValue=\"" + expectedDigest + "\"" +
(includeSize ? " size=\"" + fileSize + "\"" : "") +
" version=\"1.1\"/>" +
" </addons>" +
"</updates>"
overrideXHR(200, responseXML);
let installManager = new GMPInstallManager();
let checkPromise = installManager.checkForAddons();
checkPromise.then(function(gmpAddons) {
do_check_eq(gmpAddons.length, 1);
let gmpAddon = gmpAddons[0];
do_check_false(gmpAddon.isInstalled);
GMPInstallManager.overrideLeaveDownloadedZip = true;
let installPromise = installManager.installAddon(gmpAddon);
installPromise.then(function(extractedPaths) {
if (wantInstallReject) {
do_throw("install update should reject");
}
do_check_eq(extractedPaths.length, 1);
let extractedPath = extractedPaths[0];
do_print("Extracted path: " + extractedPath);
let extractedFile = Cc["@mozilla.org/file/local;1"].
createInstance(Ci.nsIFile);
extractedFile.initWithPath(extractedPath);
do_check_true(extractedFile.exists());
let readData = readStringFromFile(extractedFile);
do_check_eq(readData, data);
// Check that the downloaded zip mathces 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
do_check_true(!!GMPPrefs.get(GMPPrefs.KEY_ADDON_LAST_UPDATE,
gmpAddon.id, ""));
do_check_eq(GMPPrefs.get(GMPPrefs.KEY_ADDON_PATH, gmpAddon.id, ""),
extractedFile.parent.path);
do_check_eq(GMPPrefs.get(GMPPrefs.KEY_ADDON_VERSION, gmpAddon.id, ""),
"1.1");
// Make sure it reports as being installed
do_check_true(gmpAddon.isInstalled);
// Cleanup
extractedFile.parent.remove(true);
zipFile.remove(false);
httpServer.stop(function() {});
do_print("Removing downloaded GMP file: " + downloadedGMPFile.path);
downloadedGMPFile.remove(false);
installManager.uninit();
}, function(err) {
zipFile.remove(false);
let downloadedGMPFile = FileUtils.getFile("TmpD",
[gmpAddon.id + ".zip"]);
do_print("Removing from err downloaded GMP file: " +
downloadedGMPFile.path);
downloadedGMPFile.remove(false);
if (!wantInstallReject) {
do_throw("install update should not reject");
}
});
}, function(err) {
do_throw("checking updates to install them should not reject");
});
}
add_task(test_checkForAddons_installAddon.bind(null, "1", true, false));
add_task(test_checkForAddons_installAddon.bind(null, "2", false, false));
add_task(test_checkForAddons_installAddon.bind(null, "3", true, true));
/**
* Tests that installing addons when there is no server works as expected
*/
add_test(function test_installAddon_noServer() {
let dir = FileUtils.getDir("TmpD", [], true);
let zipFileName = "test_GMP.zip";
let zipURL = URL_HOST + ":0/" + zipFileName;
let data = "e~=0.5772156649";
let zipFile = createNewZipFile(zipFileName, data);
let responseXML =
"<?xml version=\"1.0\"?>" +
"<updates>" +
" <addons>" +
" <addon id=\"openh264-plugin@cisco.com\"" +
" URL=\"" + zipURL + "\"" +
" hashFunction=\"sha256\"" +
" hashValue=\"11221cbda000347b054028b527a60e578f919cb10f322ef8077d3491c6fcb474\"" +
" version=\"1.1\"/>" +
" </addons>" +
"</updates>"
overrideXHR(200, responseXML);
let installManager = new GMPInstallManager();
let checkPromise = installManager.checkForAddons();
checkPromise.then(function(gmpAddons) {
do_check_eq(gmpAddons.length, 1);
let gmpAddon= gmpAddons[0];
GMPInstallManager.overrideLeaveDownloadedZip = true;
let installPromise = installManager.installAddon(gmpAddon);
installPromise.then(function(extractedPaths) {
do_throw("No server for install should reject");
}, function(err) {
do_check_true(!!err);
installManager.uninit();
run_next_test();
});
}, function(err) {
do_throw("check should not reject for installn o server");
});
});
/**
* Returns the read stream into a string
*/
function readStringFromInputStream(inputStream) {
let sis = Cc["@mozilla.org/scriptableinputstream;1"].
createInstance(Ci.nsIScriptableInputStream);
sis.init(inputStream);
let text = sis.read(sis.available());
sis.close();
return text;
}
/**
* Reads a string of text from a file.
* This function only works with ASCII text.
*/
function readStringFromFile(file) {
if (!file.exists()) {
do_print("readStringFromFile - file doesn't exist: " + file.path);
return null;
}
let fis = Cc["@mozilla.org/network/file-input-stream;1"].
createInstance(Ci.nsIFileInputStream);
fis.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
return readStringFromInputStream(fis);
}
/**
* Bare bones XMLHttpRequest implementation for testing onprogress, onerror,
* and onload nsIDomEventListener handleEvent.
*/
function makeHandler(aVal) {
if (typeof aVal == "function")
return { handleEvent: aVal };
return aVal;
}
/**
* Constructs a mock xhr which is used for testing different aspects
* of responses.
*/
function xhr(inputStatus, inputResponse) {
this.inputStatus = inputStatus;
this.inputResponse = inputResponse;
}
xhr.prototype = {
overrideMimeType: function(aMimetype) { },
setRequestHeader: function(aHeader, aValue) { },
status: null,
channel: { set notificationCallbacks(aVal) { } },
_url: null,
_method: null,
open: function(aMethod, aUrl) {
this.channel.originalURI = Services.io.newURI(aUrl, null, null);
this._method = aMethod; this._url = aUrl;
},
abort: function() {
},
responseXML: null,
responseText: null,
send: function(aBody) {
let self = this;
do_execute_soon(function() {
self.status = self.inputStatus;
self.responseText = self.inputResponse;
try {
let parser = Cc["@mozilla.org/xmlextras/domparser;1"].
createInstance(Ci.nsIDOMParser);
self.responseXML = parser.parseFromString(self.inputResponse,
"application/xml");
} catch (e) {
self.responseXML = null;
}
let e = { target: self };
if (self.inputStatus === 200) {
self.onload(e);
} else {
self.onerror(e);
}
});
},
_onprogress: null,
set onprogress(aValue) { this._onprogress = makeHandler(aValue); },
get onprogress() { return this._onprogress; },
_onerror: null,
set onerror(aValue) { this._onerror = makeHandler(aValue); },
get onerror() { return this._onerror; },
_onload: null,
set onload(aValue) { this._onload = makeHandler(aValue); },
get onload() { return this._onload; },
addEventListener: function(aEvent, aValue, aCapturing) {
eval("this._on" + aEvent + " = aValue");
},
flags: Ci.nsIClassInfo.SINGLETON,
implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,
getHelperForLanguage: function(aLanguage) null,
getInterfaces: function(aCount) {
let interfaces = [Ci.nsISupports];
aCount.value = interfaces.length;
return interfaces;
},
classDescription: "XMLHttpRequest",
contractID: "@mozilla.org/xmlextras/xmlhttprequest;1",
classID: Components.ID("{c9b37f43-4278-4304-a5e0-600991ab08cb}"),
createInstance: function(aOuter, aIID) {
if (aOuter == null)
return this.QueryInterface(aIID);
throw Cr.NS_ERROR_NO_AGGREGATION;
},
QueryInterface: function(aIID) {
if (aIID.equals(Ci.nsIClassInfo) ||
aIID.equals(Ci.nsISupports))
return this;
throw Cr.NS_ERROR_NO_INTERFACE;
},
get wrappedJSObject() { return this; }
};
/**
* Helper used to overrideXHR requests (no matter to what URL) with the
* specified status and response.
* @param status The status you want to get back when an XHR request is made
* @param response The response you want to get back when an XHR request is made
*/
function overrideXHR(status, response) {
let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
if (overrideXHR.myxhr) {
registrar.unregisterFactory(overrideXHR.myxhr.classID, overrideXHR.myxhr);
}
overrideXHR.myxhr = new xhr(status, response);
registrar.registerFactory(overrideXHR.myxhr.classID,
overrideXHR.myxhr.classDescription,
overrideXHR.myxhr.contractID,
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
* @param zipName The name of the zip file
* @param data The data to go inside the zip for the filename entry1.info
*/
function createNewZipFile(zipName, data) {
// Create a zip file which will be used for extracting
let stream = Cc["@mozilla.org/io/string-input-stream;1"].
createInstance(Ci.nsIStringInputStream);
stream.setData(data, data.length);
let zipWriter = Cc["@mozilla.org/zipwriter;1"].
createInstance(Components.interfaces.nsIZipWriter);
let zipFile = FileUtils.getFile("TmpD", [zipName]);
if (zipFile.exists()) {
zipFile.remove(false);
}
// From prio.h
const PR_RDWR = 0x04;
const PR_CREATE_FILE = 0x08;
const PR_TRUNCATE = 0x20;
zipWriter.open(zipFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE);
zipWriter.addEntryStream("entry1.info", Date.now(),
Ci.nsIZipWriter.COMPRESSION_BEST, stream, false);
zipWriter.close();
stream.close();
do_print("zip file created on disk at: " + zipFile.path);
return zipFile;
}

View File

@ -13,6 +13,7 @@ support-files =
[test_dict.js]
[test_DirectoryLinksProvider.js]
[test_FileUtils.js]
[test_GMPInstallManager.js]
[test_Http.js]
[test_Log.js]
[test_NewTabUtils.js]