gecko/toolkit/modules/GMPInstallManager.jsm
David Rajchenbach-Teller 53e4b14b6f Bug 1087674 - Handle XHR abort()/timeout and certificate errors more gracefully in GMPInstallmanager. r=gfritzsche
Prior to this patch, a Man in the Middle (MITM) attack on SSL could cause GMPInstallManager to fail during the check for updates, which in turn would cause a crash during shutdown. This was observed in the wild by users of recent versions of Avast, which performs such attacks on SSL as part of its "HTTPS scanning" feature. With this patch, errors are handled more gracefully. The attack still prevents any update (including the install of OpenH264) but at least it does not cause a crash anymore.
2014-10-30 11:27:02 +01:00

1002 lines
34 KiB
JavaScript

/* 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 = "gmp-gmpopenh264";
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", "OPEN_H264_ID"];
var gLocale = null;
const PARENT_LOGGER_ID = "GMPInstallManager";
// Used to determine if logging should be enabled
XPCOMUtils.defineLazyGetter(this, "gLogEnabled", function() {
return Preferences.get("media.gmp-manager.log", false);
});
// Setup the parent logger with dump logging. It'll only be used if logging is
// enabled though. We don't actually have any fatal logging errors, so setting
// the log level to fatal effectively disables it.
let parentLogger = Log.repository.getLogger(PARENT_LOGGER_ID);
parentLogger.level = gLogEnabled ? Log.Level.Debug : Log.Level.Fatal;
let appender = new Log.DumpAppender();
parentLogger.addAppender(appender);
// 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");
/**
* Number of milliseconds after which we need to cancel `checkForAddons`.
*
* Bug 1087674 suggests that the XHR we use in `checkForAddons` may
* never terminate in presence of network nuisances (e.g. strange
* antivirus behavior). This timeout is a defensive measure to ensure
* that we fail cleanly in such case.
*/
const CHECK_FOR_ADDONS_TIMEOUT_DELAY_MS = 20000;
function getScopedLogger(prefix) {
// `PARENT_LOGGER_ID.` being passed here effectively links this logger
// to the parentLogger.
return Log.repository.getLogger(PARENT_LOGGER_ID + "." + prefix);
}
/**
* 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_VERSION: "media.{0}.version",
KEY_ADDON_AUTOUPDATE: "media.{0}.autoupdate",
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.char16_t;
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("_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("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.timeout = CHECK_FOR_ADDONS_TIMEOUT_DELAY_MS;
this._request.addEventListener("error", event => this.onFailXML("onErrorXML", event), false);
this._request.addEventListener("abort", event => this.onFailXML("onAbortXML", event), false);
this._request.addEventListener("timeout", event => this.onFailXML("onTimeoutXML", event), false);
this._request.addEventListener("load", event => this.onLoadXML(event), false);
log.info("sending request to: " + url);
this._request.send(null);
return this._deferred.promise;
},
/**
* 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.
* This will only install/update the OpenH264 plugin
*/
simpleCheckAndInstall: function() {
let log = getScopedLogger("simpleCheckAndInstall");
let autoUpdate = GMPPrefs.get(GMPPrefs.KEY_ADDON_AUTOUPDATE,
OPEN_H264_ID, true);
if (!autoUpdate) {
log.info("Auto-update is off for openh264, aborting check.");
return Promise.resolve({status: "check-disabled"});
}
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({status: "too-frequent-no-check"});
}
let deferred = Promise.defer();
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 deferred.resolve({status: "nothing-new-to-install"});
}
// Only 1 addon will be returned because of the gmpAddon.isOpenH264
// check above.
addonsToInstall.forEach(gmpAddon => {
promise = this.installAddon(gmpAddon);
promise.then(extractedPaths => {
// installed!
log.info("Addon installed successfully: " + gmpAddon.toString());
return deferred.resolve({status: "addon-install"});
}, () => {
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());
deferred.reject();
});
});
}, () => {
log.error("Could not check for addons");
deferred.reject();
});
return deferred.promise;
},
/**
* 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("onLoadXML");
try {
log.info("request completed downloading document");
let certs = null;
if (!Services.prefs.prefHasUserValue(GMPPrefs.KEY_URL_OVERRIDE) &&
GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS, 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();
} catch (ex) {
log.error("could not load xml: " + ex);
this._deferred.reject({
target: event.target,
status: this._getChannelStatus(event.target),
message: "" + ex,
});
delete this._deferred;
}
},
/**
* Returns the status code for the XMLHttpRequest
*/
_getChannelStatus: function(request) {
let log = getScopedLogger("_getChannelStatus");
let status = null;
try {
status = request.status;
log.info("request.status is: " + request.status);
}
catch (e) {
}
if (status == null) {
status = request.channel.QueryInterface(Ci.nsIRequest).status;
}
return status;
},
/**
* There was an error of some kind during the XMLHttpRequest. This
* error may have been caused by external factors (e.g. network
* issues) or internally (by a timeout).
*
* @param event The nsIDOMEvent for the error
*/
onFailXML: function(failure, event) {
let log = getScopedLogger(failure);
let request = event.target;
let status = this._getChannelStatus(request);
let message = "request.status: " + status + "(" + event.type + ")";
log.warn(message);
this._deferred.reject({
target: request,
status: status,
message: message
});
delete this._deferred;
},
/**
* Returns an array of GMPAddon objects discovered by the update check.
* Or returns an empty array if there were any problems with parsing.
* If there's an error, it will be logged if logging is enabled.
*/
parseResponseXML: function() {
try {
let log = getScopedLogger("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,
gmpAddon.version);
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);
// Setting the version pref signals installation completion to consumers,
// if you need to set other prefs etc. do it before this.
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;
}