diff options
Diffstat (limited to 'toolkit/modules/GMPInstallManager.jsm')
-rw-r--r-- | toolkit/modules/GMPInstallManager.jsm | 620 |
1 files changed, 620 insertions, 0 deletions
diff --git a/toolkit/modules/GMPInstallManager.jsm b/toolkit/modules/GMPInstallManager.jsm new file mode 100644 index 0000000000..4b1e3ff6c3 --- /dev/null +++ b/toolkit/modules/GMPInstallManager.jsm @@ -0,0 +1,620 @@ +/* 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"; + +// 1 day default +const DEFAULT_SECONDS_BETWEEN_CHECKS = 60 * 60 * 24; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { PromiseUtils } = ChromeUtils.import( + "resource://gre/modules/PromiseUtils.jsm" +); +const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm"); +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); +// These symbols are, unfortunately, accessed via the module global from +// tests, and therefore cannot be lexical definitions. +var { GMPPrefs, GMPUtils, GMP_PLUGIN_IDS } = ChromeUtils.import( + "resource://gre/modules/GMPUtils.jsm" +); +const { ProductAddonChecker } = ChromeUtils.import( + "resource://gre/modules/addons/ProductAddonChecker.jsm" +); + +var EXPORTED_SYMBOLS = [ + "GMPInstallManager", + "GMPExtractor", + "GMPDownloader", + "GMPAddon", +]; + +ChromeUtils.defineModuleGetter( + this, + "CertUtils", + "resource://gre/modules/CertUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "FileUtils", + "resource://gre/modules/FileUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "UpdateUtils", + "resource://gre/modules/UpdateUtils.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "ServiceRequest", + "resource://gre/modules/ServiceRequest.jsm" +); + +function getScopedLogger(prefix) { + // `PARENT_LOGGER_ID.` being passed here effectively links this logger + // to the parentLogger. + return Log.repository.getLoggerWithMessagePrefix("Toolkit.GMP", prefix + " "); +} + +const LOCAL_GMP_SOURCES = [ + { + id: "gmp-gmpopenh264", + src: "chrome://global/content/gmp-sources/openh264.json", + }, + { + id: "gmp-widevinecdm", + src: "chrome://global/content/gmp-sources/widevinecdm.json", + }, +]; + +function downloadJSON(uri) { + let log = getScopedLogger("GMPInstallManager.checkForAddons"); + log.info("fetching config from: " + uri); + return new Promise((resolve, reject) => { + let xmlHttp = new ServiceRequest({ mozAnon: true }); + + xmlHttp.onload = function(aResponse) { + resolve(JSON.parse(this.responseText)); + }; + + xmlHttp.onerror = function(e) { + reject("Fetching " + uri + " results in error code: " + e.target.status); + }; + + xmlHttp.open("GET", uri); + xmlHttp.overrideMimeType("application/json"); + xmlHttp.send(); + }); +} + +/** + * If downloading from the network fails (AUS server is down), + * load the sources from local build configuration. + */ +function downloadLocalConfig() { + let log = getScopedLogger("GMPInstallManager.checkForAddons"); + return Promise.all( + LOCAL_GMP_SOURCES.map(conf => { + return downloadJSON(conf.src).then(addons => { + let platforms = addons.vendors[conf.id].platforms; + let target = Services.appinfo.OS + "_" + UpdateUtils.ABI; + let details = null; + + while (!details) { + if (!(target in platforms)) { + // There was no matching platform so return false, this addon + // will be filtered from the results below + log.info("no details found for: " + target); + return false; + } + // Field either has the details of the binary or is an alias + // to another build target key that does + if (platforms[target].alias) { + target = platforms[target].alias; + } else { + details = platforms[target]; + } + } + + log.info("found plugin: " + conf.id); + return { + id: conf.id, + URL: details.fileUrl, + hashFunction: addons.hashFunction, + hashValue: details.hashValue, + version: addons.vendors[conf.id].version, + size: details.filesize, + }; + }); + }) + ).then(addons => { + // Some filters may not match this platform so + // filter those out + addons = addons.filter(x => x !== false); + + return { + usedFallback: true, + addons, + }; + }); +} + +/** + * 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 + */ + async _getURL() { + 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.getString(GMPPrefs.KEY_URL_OVERRIDE, ""); + if (url) { + log.info("Using override url: " + url); + } else { + url = GMPPrefs.getString(GMPPrefs.KEY_URL); + log.info("Using url: " + url); + } + + url = await UpdateUtils.formatUpdateURL(url); + + 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 object with properties: + * addons: array of addons + * usedFallback: whether the data was collected from online or + * from fallback data within the build + * 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 + */ + async checkForAddons() { + let log = getScopedLogger("GMPInstallManager.checkForAddons"); + if (this._deferred) { + log.error("checkForAddons already called"); + return Promise.reject({ type: "alreadycalled" }); + } + + if (!GMPPrefs.getBool(GMPPrefs.KEY_UPDATE_ENABLED, true)) { + log.info("Updates are disabled via media.gmp-manager.updateEnabled"); + return { usedFallback: true, addons: [] }; + } + + this._deferred = PromiseUtils.defer(); + + let allowNonBuiltIn = true; + let certs = null; + if (!Services.prefs.prefHasUserValue(GMPPrefs.KEY_URL_OVERRIDE)) { + allowNonBuiltIn = !GMPPrefs.getString( + GMPPrefs.KEY_CERT_REQUIREBUILTIN, + true + ); + if (GMPPrefs.getBool(GMPPrefs.KEY_CERT_CHECKATTRS, true)) { + certs = CertUtils.readCertPrefs(GMPPrefs.KEY_CERTS_BRANCH); + } + } + + let url = await this._getURL(); + + let addonPromise = ProductAddonChecker.getProductAddonList( + url, + allowNonBuiltIn, + certs + ).catch(downloadLocalConfig); + + addonPromise.then( + res => { + if (!res || !res.addons) { + this._deferred.resolve({ addons: [] }); + } else { + res.addons = res.addons.map(a => new GMPAddon(a)); + this._deferred.resolve(res); + } + delete this._deferred; + }, + ex => { + this._deferred.reject(ex); + delete this._deferred; + } + ); + 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(gmpAddon) { + if (this._deferred) { + let log = getScopedLogger("GMPInstallManager.installAddon"); + log.error("previous error encountered"); + return Promise.reject({ type: "previouserrorencountered" }); + } + this.gmpDownloader = new GMPDownloader(gmpAddon); + return this.gmpDownloader.start(); + }, + _getTimeSinceLastCheck() { + 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.getInt(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.getBool(GMPPrefs.KEY_EME_ENABLED, true); + }, + _isAddonEnabled(aAddon) { + return GMPPrefs.getBool(GMPPrefs.KEY_PLUGIN_ENABLED, true, aAddon); + }, + _isAddonUpdateEnabled(aAddon) { + return ( + this._isAddonEnabled(aAddon) && + GMPPrefs.getBool(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, aAddon) + ); + }, + _updateLastCheck() { + let now = Math.round(Date.now() / 1000); + GMPPrefs.setInt(GMPPrefs.KEY_UPDATE_LAST_CHECK, now); + }, + _versionchangeOccurred() { + let savedBuildID = GMPPrefs.getString(GMPPrefs.KEY_BUILDID, ""); + let buildID = Services.appinfo.platformBuildID || ""; + if (savedBuildID == buildID) { + return false; + } + GMPPrefs.setString(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. + */ + async simpleCheckAndInstall() { + 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.getInt( + 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 { usedFallback, addons } = await this.checkForAddons(); + this._updateLastCheck(); + log.info("Found " + addons.length + " addons advertised."); + let addonsToInstall = addons.filter(function(gmpAddon) { + log.info("Found addon: " + gmpAddon.toString()); + + if (!gmpAddon.isValid) { + log.info("Addon |" + gmpAddon.id + "| is invalid."); + return false; + } + + if (GMPUtils.isPluginHidden(gmpAddon)) { + log.info("Addon |" + gmpAddon.id + "| has been hidden."); + return false; + } + + if (gmpAddon.isInstalled) { + log.info("Addon |" + gmpAddon.id + "| already installed."); + return false; + } + + // Do not install from fallback if already installed as it + // may be a downgrade + if (usedFallback && gmpAddon.isUpdate) { + log.info( + "Addon |" + + gmpAddon.id + + "| not installing updates based " + + "on fallback." + ); + return false; + } + + let addonUpdateEnabled = false; + if (GMP_PLUGIN_IDS.includes(gmpAddon.id)) { + if (!this._isAddonEnabled(gmpAddon.id)) { + log.info( + "GMP |" + gmpAddon.id + "| has been disabled; skipping check." + ); + } else if (!this._isAddonUpdateEnabled(gmpAddon.id)) { + log.info( + "Auto-update is off for " + gmpAddon.id + ", skipping check." + ); + } else { + addonUpdateEnabled = true; + } + } 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 { + await this.installAddon(addon); + installResults.push({ + id: addon.id, + result: "succeeded", + }); + } catch (e) { + failureEncountered = true; + installResults.push({ + id: addon.id, + result: "failed", + }); + } + } + if (failureEncountered) { + // eslint-disable-next-line no-throw-literal + 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() { + 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, +}; + +/** + * 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 addon The ProductAddonChecker `addon` object + */ +function GMPAddon(addon) { + let log = getScopedLogger("GMPAddon.constructor"); + for (let name of Object.keys(addon)) { + this[name] = addon[name]; + } + log.info("Created new addon: " + this.toString()); +} + +GMPAddon.prototype = { + /** + * Returns a string representation of the addon + */ + toString() { + 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.getString(GMPPrefs.KEY_PLUGIN_VERSION, "", this.id) === + this.version + ); + }, + get isEME() { + return this.id == "gmp-widevinecdm" || this.id.indexOf("gmp-eme-") == 0; + }, + get isOpenH264() { + return this.id == "gmp-gmpopenh264"; + }, + /** + * @return true if the addon has been previously installed and this is + * a new version, if this is a fresh install return false + */ + get isUpdate() { + return ( + this.version && + GMPPrefs.getBool(GMPPrefs.KEY_PLUGIN_VERSION, false, this.id) + ); + }, +}; +/** + * Constructs a GMPExtractor object which is used to extract a GMP zip + * into the specified location. + * @param zipPath The path on disk of the zip file to extract + * @param relativePath The relative path inside the profile directory to + * extract the zip to. + */ +function GMPExtractor(zipPath, relativeInstallPath) { + this.zipPath = zipPath; + this.relativeInstallPath = relativeInstallPath; +} +GMPExtractor.prototype = { + /** + * 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() { + this._deferred = PromiseUtils.defer(); + let deferredPromise = this._deferred; + let { zipPath, relativeInstallPath } = this; + // Escape the zip path since the worker will use it as a URI + let zipFile = new FileUtils.File(zipPath); + let zipURI = Services.io.newFileURI(zipFile).spec; + let worker = new ChromeWorker( + "resource://gre/modules/GMPExtractorWorker.js" + ); + worker.onmessage = function(msg) { + let log = getScopedLogger("GMPExtractor"); + worker.terminate(); + if (msg.data.result != "success") { + log.error("Failed to extract zip file: " + zipURI); + return deferredPromise.reject({ + target: this, + status: msg.data.exception, + type: "exception", + }); + } + log.info("Successfully extracted zip file: " + zipURI); + return deferredPromise.resolve(msg.data.extractedPaths); + }; + worker.postMessage({ zipURI, relativeInstallPath }); + 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; +} + +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() { + let log = getScopedLogger("GMPDownloader"); + let gmpAddon = this._gmpAddon; + + if (!gmpAddon.isValid) { + log.info("gmpAddon is not valid, will not continue"); + return Promise.reject({ + target: this, + type: "downloaderr", + }); + } + // If the HTTPS-Only Mode is enabled, every insecure request gets upgraded + // by default. This upgrade has to be prevented for openh264 downloads since + // the server doesn't support https:// + const downloadOptions = { + httpsOnlyNoUpgrade: gmpAddon.isOpenH264, + }; + return ProductAddonChecker.downloadAddon(gmpAddon, downloadOptions).then( + zipPath => { + let relativePath = OS.Path.join(gmpAddon.id, gmpAddon.version); + log.info("install to directory path: " + relativePath); + let gmpInstaller = new GMPExtractor(zipPath, relativePath); + let installPromise = gmpInstaller.install(); + return installPromise.then(extractedPaths => { + // Success, set the prefs + let now = Math.round(Date.now() / 1000); + GMPPrefs.setInt(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, now, gmpAddon.id); + // Remember our ABI, so that if the profile is migrated to another + // platform or from 32 -> 64 bit, we notice and don't try to load the + // unexecutable plugin library. + let abi = GMPUtils._expectedABI(gmpAddon); + log.info("Setting ABI to '" + abi + "' for " + gmpAddon.id); + GMPPrefs.setString(GMPPrefs.KEY_PLUGIN_ABI, abi, gmpAddon.id); + // Setting the version pref signals installation completion to consumers, + // if you need to set other prefs etc. do it before this. + GMPPrefs.setString( + GMPPrefs.KEY_PLUGIN_VERSION, + gmpAddon.version, + gmpAddon.id + ); + return extractedPaths; + }); + } + ); + }, +}; |