diff options
author | Moonchild <mcwerewolf@wolfbeast.com> | 2019-03-13 07:49:07 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-03-13 07:49:07 +0100 |
commit | bf0413359245579e9509146d42cd5547e35da695 (patch) | |
tree | 8218d4f60d9eccacbf42df8cb88094a082d401b4 /toolkit/mozapps/extensions/GMPInstallManager.jsm | |
parent | 51b821b3fdc5a7eab2369cb6a6680598a6264b08 (diff) | |
parent | 709bc24e9110eba12f94cfcb8db00a8338ac4098 (diff) | |
download | UXP-bf0413359245579e9509146d42cd5547e35da695.tar UXP-bf0413359245579e9509146d42cd5547e35da695.tar.gz UXP-bf0413359245579e9509146d42cd5547e35da695.tar.lz UXP-bf0413359245579e9509146d42cd5547e35da695.tar.xz UXP-bf0413359245579e9509146d42cd5547e35da695.zip |
Merge pull request #998 from MoonchildProductions/master
Merge master into Sync-weave
Diffstat (limited to 'toolkit/mozapps/extensions/GMPInstallManager.jsm')
-rw-r--r-- | toolkit/mozapps/extensions/GMPInstallManager.jsm | 961 |
1 files changed, 961 insertions, 0 deletions
diff --git a/toolkit/mozapps/extensions/GMPInstallManager.jsm b/toolkit/mozapps/extensions/GMPInstallManager.jsm new file mode 100644 index 000000000..b9ebe5d7e --- /dev/null +++ b/toolkit/mozapps/extensions/GMPInstallManager.jsm @@ -0,0 +1,961 @@ +/* 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; + +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"); +Cu.import("resource://gre/modules/GMPUtils.jsm"); + +this.EXPORTED_SYMBOLS = ["GMPInstallManager", "GMPExtractor", "GMPDownloader", + "GMPAddon"]; + +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"); + +/** + * 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.getLoggerWithMessagePrefix("Toolkit.GMP", prefix + " "); +} + +// 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("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, 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, verifyerr or previouserrorencountered + */ + installAddon: function(gmpAddon) { + if (this._deferred) { + log.error("previous error encountered"); + return Promise.reject({type: "previouserrorencountered"}); + } + 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, 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; + }, + get _isEMEEnabled() { + return GMPPrefs.get(GMPPrefs.KEY_EME_ENABLED, true); + }, + _isAddonUpdateEnabled: function(aAddon) { + return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_ENABLED, true, aAddon) && + GMPPrefs.get(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, aAddon); + }, + _updateLastCheck: function() { + let now = Math.round(Date.now() / 1000); + GMPPrefs.set(GMPPrefs.KEY_UPDATE_LAST_CHECK, now); + }, + _versionchangeOccurred: function() { + let savedBuildID = GMPPrefs.get(GMPPrefs.KEY_BUILDID, null); + let buildID = Services.appinfo.platformBuildID; + if (savedBuildID == buildID) { + return false; + } + GMPPrefs.set(GMPPrefs.KEY_BUILDID, buildID); + return true; + }, + /** + * Wrapper for checkForAddons and installAddon. + * Will only install if not already installed and will log the results. + * This will only install/update the OpenH264 and EME plugins + * @return a promise which will be resolved if all addons could be installed + * successfully, rejected otherwise. + */ + simpleCheckAndInstall: Task.async(function*() { + let log = getScopedLogger("GMPInstallManager.simpleCheckAndInstall"); + + if (this._versionchangeOccurred()) { + log.info("A version change occurred. Ignoring " + + "media.gmp-manager.lastCheck to check immediately for " + + "new or updated GMPs."); + } else { + let secondsBetweenChecks = + GMPPrefs.get(GMPPrefs.KEY_SECONDS_BETWEEN_CHECKS, + 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 {status: "too-frequent-no-check"}; + } + } + + try { + let gmpAddons = yield this.checkForAddons(); + this._updateLastCheck(); + log.info("Found " + gmpAddons.length + " addons advertised."); + let addonsToInstall = gmpAddons.filter(function(gmpAddon) { + log.info("Found addon: " + gmpAddon.toString()); + + if (!gmpAddon.isValid || GMPUtils.isPluginHidden(gmpAddon) || + gmpAddon.isInstalled) { + log.info("Addon invalid, hidden or already installed."); + return false; + } + + let addonUpdateEnabled = false; + if (GMP_PLUGIN_IDS.indexOf(gmpAddon.id) >= 0) { + addonUpdateEnabled = this._isAddonUpdateEnabled(gmpAddon.id); + if (!addonUpdateEnabled) { + log.info("Auto-update is off for " + gmpAddon.id + + ", skipping check."); + } + } else { + // Currently, we only support installs of OpenH264 and EME plugins. + log.info("Auto-update is off for unknown plugin '" + gmpAddon.id + + "', skipping check."); + } + + return addonUpdateEnabled; + }, this); + + if (!addonsToInstall.length) { + log.info("No new addons to install, returning"); + return {status: "nothing-new-to-install"}; + } + + let installResults = []; + let failureEncountered = false; + for (let addon of addonsToInstall) { + try { + yield this.installAddon(addon); + installResults.push({ + id: addon.id, + result: "succeeded", + }); + } catch (e) { + failureEncountered = true; + installResults.push({ + id: addon.id, + result: "failed", + }); + } + } + if (failureEncountered) { + throw {status: "failed", + results: installResults}; + } + return {status: "succeeded", + results: installResults}; + } catch(e) { + log.error("Could not check for addons", e); + throw e; + } + }), + + /** + * Makes sure everything is cleaned up + */ + uninit: function() { + let log = getScopedLogger("GMPInstallManager.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"); + try { + log.info("request completed downloading document"); + let certs = null; + if (!Services.prefs.prefHasUserValue(GMPPrefs.KEY_URL_OVERRIDE) && + GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS, true)) { + certs = gCertUtils.readCertPrefs(GMPPrefs.KEY_CERTS_BRANCH); + } + + let allowNonBuiltIn = !GMPPrefs.get(GMPPrefs.KEY_CERT_REQUIREBUILTIN, + true); + log.info("allowNonBuiltIn: " + allowNonBuiltIn); + + gCertUtils.checkCert(this._request.channel, allowNonBuiltIn, certs); + + this.parseResponseXML(); + } catch (ex) { + log.error("could not load xml: " + ex); + this._deferred.reject({ + target: event.target, + status: this._getChannelStatus(event.target), + message: "" + ex, + }); + delete this._deferred; + } + }, + + /** + * Returns the status code for the XMLHttpRequest + */ + _getChannelStatus: function(request) { + let log = getScopedLogger("GMPInstallManager._getChannelStatus"); + let status = null; + try { + status = request.status; + log.info("request.status is: " + request.status); + } + catch (e) { + } + + if (status == null) { + status = request.channel.QueryInterface(Ci.nsIRequest).status; + } + return status; + }, + + /** + * There was an error of some kind during the XMLHttpRequest. This + * error may have been caused by external factors (e.g. network + * issues) or internally (by a timeout). + * + * @param event The nsIDOMEvent for the error + */ + onFailXML: function(failure, event) { + let log = getScopedLogger("GMPInstallManager.onFailXML " + failure); + let request = event.target; + let status = this._getChannelStatus(request); + let message = "request.status: " + status + " (" + event.type + ")"; + log.warn(message); + this._deferred.reject({ + target: request, + status: status, + message: message + }); + delete this._deferred; + }, + + /** + * Returns an array of GMPAddon objects discovered by the update check. + * Or returns an empty array if there were any problems with parsing. + * If there's an error, it will be logged if logging is enabled. + */ + parseResponseXML: function() { + try { + let log = getScopedLogger("GMPInstallManager.parseResponseXML"); + let updatesElement = this._request.responseXML.documentElement; + if (!updatesElement) { + let message = "empty updates document"; + log.warn(message); + this._deferred.reject({ + target: this._request, + message: message + }); + delete this._deferred; + return; + } + + if (updatesElement.nodeName != "updates") { + let message = "got node name: " + updatesElement.nodeName + + ", expected: updates"; + log.warn(message); + this._deferred.reject({ + target: this._request, + message: message + }); + delete this._deferred; + return; + } + + const ELEMENT_NODE = Ci.nsIDOMNode.ELEMENT_NODE; + let gmpResults = []; + for (let i = 0; i < updatesElement.childNodes.length; ++i) { + let updatesChildElement = updatesElement.childNodes.item(i); + if (updatesChildElement.nodeType != ELEMENT_NODE) { + continue; + } + if (updatesChildElement.localName == "addons") { + gmpResults = GMPAddon.parseGMPAddonsNode(updatesChildElement); + } + } + this._deferred.resolve(gmpResults); + delete this._deferred; + } catch (e) { + this._deferred.reject({ + target: this._request, + message: e + }); + delete this._deferred; + } + }, +}; + +/** + * 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 + + ", 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; + }, + get isInstalled() { + return this.version && + GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VERSION, "", this.id) === this.version; + }, + get isEME() { + return this.id == "gmp-widevinecdm" || this.id.indexOf("gmp-eme-") == 0; + }, +}; +/** + * 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.includes("__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_PLUGIN_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_PLUGIN_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; +} |