forked from mirrors/gecko-dev
		
	Depends on D179819 Differential Revision: https://phabricator.services.mozilla.com/D179820
		
			
				
	
	
		
			803 lines
		
	
	
	
		
			26 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			803 lines
		
	
	
	
		
			26 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/. */
 | 
						|
 | 
						|
// 1 day default
 | 
						|
const DEFAULT_SECONDS_BETWEEN_CHECKS = 60 * 60 * 24;
 | 
						|
 | 
						|
import { PromiseUtils } from "resource://gre/modules/PromiseUtils.sys.mjs";
 | 
						|
 | 
						|
import { Log } from "resource://gre/modules/Log.sys.mjs";
 | 
						|
import {
 | 
						|
  GMPPrefs,
 | 
						|
  GMPUtils,
 | 
						|
  GMP_PLUGIN_IDS,
 | 
						|
  WIDEVINE_ID,
 | 
						|
} from "resource://gre/modules/GMPUtils.sys.mjs";
 | 
						|
 | 
						|
import { ProductAddonChecker } from "resource://gre/modules/addons/ProductAddonChecker.sys.mjs";
 | 
						|
 | 
						|
const lazy = {};
 | 
						|
 | 
						|
ChromeUtils.defineESModuleGetters(lazy, {
 | 
						|
  CertUtils: "resource://gre/modules/CertUtils.sys.mjs",
 | 
						|
  FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
 | 
						|
  ServiceRequest: "resource://gre/modules/ServiceRequest.sys.mjs",
 | 
						|
  UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
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 lazy.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.downloadLocalConfig");
 | 
						|
  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 + "_" + lazy.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
 | 
						|
 */
 | 
						|
export 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 lazy.UpdateUtils.formatUpdateURL(url);
 | 
						|
 | 
						|
    log.info("Using url (with replacement): " + url);
 | 
						|
    return url;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Records telemetry results on if fetching update.xml from Balrog succeeded
 | 
						|
   * when content signature was used to verify the response from Balrog.
 | 
						|
   * @param didGetAddonList
 | 
						|
   *        A boolean indicating if an update.xml containing the addon list was
 | 
						|
   *        successfully fetched (true) or not (false).
 | 
						|
   * @param err
 | 
						|
   *        The error that was thrown (if it exists) for the failure case. This
 | 
						|
   *        is expected to have a addonCheckerErr member which provides further
 | 
						|
   *        information on why the addon checker failed.
 | 
						|
   */
 | 
						|
  recordUpdateXmlTelemetryForContentSignature(didGetAddonList, err = null) {
 | 
						|
    let log = getScopedLogger(
 | 
						|
      "GMPInstallManager.recordUpdateXmlTelemetryForContentSignature"
 | 
						|
    );
 | 
						|
    try {
 | 
						|
      let updateResultHistogram = Services.telemetry.getHistogramById(
 | 
						|
        "MEDIA_GMP_UPDATE_XML_FETCH_RESULT"
 | 
						|
      );
 | 
						|
 | 
						|
      // The non-glean telemetry used here will be removed in future and just
 | 
						|
      // the glean data will be gathered.
 | 
						|
      if (didGetAddonList) {
 | 
						|
        updateResultHistogram.add("content_sig_ok");
 | 
						|
        Glean.gmp.updateXmlFetchResult.content_sig_success.add(1);
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      // All remaining cases are failure cases.
 | 
						|
      updateResultHistogram.add("content_sig_fail");
 | 
						|
      if (!err?.addonCheckerErr) {
 | 
						|
        // Unknown error case. If this is happening we should audit error paths
 | 
						|
        // to identify why we're not getting an error, or not getting it
 | 
						|
        // labelled.
 | 
						|
        Glean.gmp.updateXmlFetchResult.content_sig_unknown_error.add(1);
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      const errorToHistogramMap = {
 | 
						|
        [ProductAddonChecker.NETWORK_REQUEST_ERR]:
 | 
						|
          "content_sig_net_request_error",
 | 
						|
        [ProductAddonChecker.NETWORK_TIMEOUT_ERR]: "content_sig_net_timeout",
 | 
						|
        [ProductAddonChecker.ABORT_ERR]: "content_sig_abort",
 | 
						|
        [ProductAddonChecker.VERIFICATION_MISSING_DATA_ERR]:
 | 
						|
          "content_sig_missing_data",
 | 
						|
        [ProductAddonChecker.VERIFICATION_FAILED_ERR]: "content_sig_failed",
 | 
						|
        [ProductAddonChecker.VERIFICATION_INVALID_ERR]: "content_sig_invalid",
 | 
						|
        [ProductAddonChecker.XML_PARSE_ERR]: "content_sig_xml_parse_error",
 | 
						|
      };
 | 
						|
      let metricID =
 | 
						|
        errorToHistogramMap[err.addonCheckerErr] ?? "content_sig_unknown_error";
 | 
						|
      let metric = Glean.gmp.updateXmlFetchResult[metricID];
 | 
						|
      metric.add(1);
 | 
						|
    } catch (e) {
 | 
						|
      // We don't expect this path to be hit, but we don't want telemetry
 | 
						|
      // failures to break GMP updates, so catch any issues here and let the
 | 
						|
      // update machinery continue.
 | 
						|
      log.error(
 | 
						|
        `Failed to record telemetry result of getProductAddonList, got error: ${e}`
 | 
						|
      );
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Records telemetry results on if fetching update.xml from Balrog succeeded
 | 
						|
   * when cert pinning was used to verify the response from Balrog. This
 | 
						|
   * should be removed once we're no longer using cert pinning.
 | 
						|
   * @param didGetAddonList
 | 
						|
   *        A boolean indicating if an update.xml containing the addon list was
 | 
						|
   *        successfully fetched (true) or not (false).
 | 
						|
   * @param err
 | 
						|
   *        The error that was thrown (if it exists) for the failure case. This
 | 
						|
   *        is expected to have a addonCheckerErr member which provides further
 | 
						|
   *        information on why the addon checker failed.
 | 
						|
   */
 | 
						|
  recordUpdateXmlTelemetryForCertPinning(didGetAddonList, err = null) {
 | 
						|
    let log = getScopedLogger(
 | 
						|
      "GMPInstallManager.recordUpdateXmlTelemetryForCertPinning"
 | 
						|
    );
 | 
						|
    try {
 | 
						|
      let updateResultHistogram = Services.telemetry.getHistogramById(
 | 
						|
        "MEDIA_GMP_UPDATE_XML_FETCH_RESULT"
 | 
						|
      );
 | 
						|
 | 
						|
      // The non-glean telemetry used here will be removed in future and just
 | 
						|
      // the glean data will be gathered.
 | 
						|
      if (didGetAddonList) {
 | 
						|
        updateResultHistogram.add("cert_pinning_ok");
 | 
						|
        Glean.gmp.updateXmlFetchResult.cert_pin_success.add(1);
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      // All remaining cases are failure cases.
 | 
						|
      updateResultHistogram.add("cert_pinning_fail");
 | 
						|
      if (!err?.addonCheckerErr) {
 | 
						|
        // Unknown error case. If this is happening we should audit error paths
 | 
						|
        // to identify why we're not getting an error, or not getting it
 | 
						|
        // labelled.
 | 
						|
        Glean.gmp.updateXmlFetchResult.cert_pin_unknown_error.add(1);
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      const errorToHistogramMap = {
 | 
						|
        [ProductAddonChecker.NETWORK_REQUEST_ERR]: "cert_pin_net_request_error",
 | 
						|
        [ProductAddonChecker.NETWORK_TIMEOUT_ERR]: "cert_pin_net_timeout",
 | 
						|
        [ProductAddonChecker.ABORT_ERR]: "cert_pin_abort",
 | 
						|
        [ProductAddonChecker.VERIFICATION_MISSING_DATA_ERR]:
 | 
						|
          "cert_pin_missing_data",
 | 
						|
        [ProductAddonChecker.VERIFICATION_FAILED_ERR]: "cert_pin_failed",
 | 
						|
        [ProductAddonChecker.VERIFICATION_INVALID_ERR]: "cert_pin_invalid",
 | 
						|
        [ProductAddonChecker.XML_PARSE_ERR]: "cert_pin_xml_parse_error",
 | 
						|
      };
 | 
						|
      let metricID =
 | 
						|
        errorToHistogramMap[err.addonCheckerErr] ?? "cert_pin_unknown_error";
 | 
						|
      let metric = Glean.gmp.updateXmlFetchResult[metricID];
 | 
						|
      metric.add(1);
 | 
						|
    } catch (e) {
 | 
						|
      // We don't expect this path to be hit, but we don't want telemetry
 | 
						|
      // failures to break GMP updates, so catch any issues here and let the
 | 
						|
      // update machinery continue.
 | 
						|
      log.error(
 | 
						|
        `Failed to record telemetry result of getProductAddonList, got error: ${e}`
 | 
						|
      );
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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();
 | 
						|
 | 
						|
    // Should content signature checking of Balrog replies be used? If so this
 | 
						|
    // will be done instead of the older cert pinning method.
 | 
						|
    let checkContentSignature = GMPPrefs.getBool(
 | 
						|
      GMPPrefs.KEY_CHECK_CONTENT_SIGNATURE,
 | 
						|
      true
 | 
						|
    );
 | 
						|
 | 
						|
    let allowNonBuiltIn = true;
 | 
						|
    let certs = null;
 | 
						|
    // Only check certificates if we're not using a custom URL, and only if
 | 
						|
    // we're not checking a content signature.
 | 
						|
    if (
 | 
						|
      !Services.prefs.prefHasUserValue(GMPPrefs.KEY_URL_OVERRIDE) &&
 | 
						|
      !checkContentSignature
 | 
						|
    ) {
 | 
						|
      allowNonBuiltIn = !GMPPrefs.getString(
 | 
						|
        GMPPrefs.KEY_CERT_REQUIREBUILTIN,
 | 
						|
        true
 | 
						|
      );
 | 
						|
      if (GMPPrefs.getBool(GMPPrefs.KEY_CERT_CHECKATTRS, true)) {
 | 
						|
        certs = lazy.CertUtils.readCertPrefs(GMPPrefs.KEY_CERTS_BRANCH);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    let url = await this._getURL();
 | 
						|
 | 
						|
    log.info(
 | 
						|
      `Fetching product addon list url=${url}, allowNonBuiltIn=${allowNonBuiltIn}, certs=${certs}, checkContentSignature=${checkContentSignature}`
 | 
						|
    );
 | 
						|
    let addonPromise = ProductAddonChecker.getProductAddonList(
 | 
						|
      url,
 | 
						|
      allowNonBuiltIn,
 | 
						|
      certs,
 | 
						|
      checkContentSignature
 | 
						|
    )
 | 
						|
      .then(res => {
 | 
						|
        if (checkContentSignature) {
 | 
						|
          this.recordUpdateXmlTelemetryForContentSignature(true);
 | 
						|
        } else {
 | 
						|
          this.recordUpdateXmlTelemetryForCertPinning(true);
 | 
						|
        }
 | 
						|
        return res;
 | 
						|
      })
 | 
						|
      .catch(err => {
 | 
						|
        if (checkContentSignature) {
 | 
						|
          this.recordUpdateXmlTelemetryForContentSignature(false, err);
 | 
						|
        } else {
 | 
						|
          this.recordUpdateXmlTelemetryForCertPinning(false, err);
 | 
						|
        }
 | 
						|
        return 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) {
 | 
						|
        let now = Math.round(Date.now() / 1000);
 | 
						|
        GMPPrefs.setInt(GMPPrefs.KEY_UPDATE_LAST_EMPTY_CHECK, now);
 | 
						|
        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
 | 
						|
 */
 | 
						|
export 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 &&
 | 
						|
      !!this.hashValue &&
 | 
						|
      GMPPrefs.getString(GMPPrefs.KEY_PLUGIN_VERSION, "", this.id) ===
 | 
						|
        this.version &&
 | 
						|
      GMPPrefs.getString(GMPPrefs.KEY_PLUGIN_HASHVALUE, "", this.id) ===
 | 
						|
        this.hashValue
 | 
						|
    );
 | 
						|
  },
 | 
						|
  get isEME() {
 | 
						|
    return this.id == WIDEVINE_ID;
 | 
						|
  },
 | 
						|
  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 components inside the profile directory
 | 
						|
 *                     to extract the zip to.
 | 
						|
 */
 | 
						|
export 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 lazy.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.
 | 
						|
 */
 | 
						|
export 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;
 | 
						|
    let now = Math.round(Date.now() / 1000);
 | 
						|
    GMPPrefs.setInt(GMPPrefs.KEY_PLUGIN_LAST_INSTALL_START, now, gmpAddon.id);
 | 
						|
 | 
						|
    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 now = Math.round(Date.now() / 1000);
 | 
						|
        GMPPrefs.setInt(GMPPrefs.KEY_PLUGIN_LAST_DOWNLOAD, now, gmpAddon.id);
 | 
						|
        log.info(
 | 
						|
          `install to directory path: ${gmpAddon.id}/${gmpAddon.version}`
 | 
						|
        );
 | 
						|
        let gmpInstaller = new GMPExtractor(zipPath, [
 | 
						|
          gmpAddon.id,
 | 
						|
          gmpAddon.version,
 | 
						|
        ]);
 | 
						|
        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);
 | 
						|
            // We use the combination of the hash and version to ensure we are
 | 
						|
            // up to date.
 | 
						|
            GMPPrefs.setString(
 | 
						|
              GMPPrefs.KEY_PLUGIN_HASHVALUE,
 | 
						|
              gmpAddon.hashValue,
 | 
						|
              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;
 | 
						|
          },
 | 
						|
          reason => {
 | 
						|
            GMPPrefs.setString(
 | 
						|
              GMPPrefs.KEY_PLUGIN_LAST_INSTALL_FAIL_REASON,
 | 
						|
              reason,
 | 
						|
              gmpAddon.id
 | 
						|
            );
 | 
						|
            let now = Math.round(Date.now() / 1000);
 | 
						|
            GMPPrefs.setInt(
 | 
						|
              GMPPrefs.KEY_PLUGIN_LAST_INSTALL_FAILED,
 | 
						|
              now,
 | 
						|
              gmpAddon.id
 | 
						|
            );
 | 
						|
            throw reason;
 | 
						|
          }
 | 
						|
        );
 | 
						|
      },
 | 
						|
      reason => {
 | 
						|
        GMPPrefs.setString(
 | 
						|
          GMPPrefs.KEY_PLUGIN_LAST_DOWNLOAD_FAIL_REASON,
 | 
						|
          reason,
 | 
						|
          gmpAddon.id
 | 
						|
        );
 | 
						|
        let now = Math.round(Date.now() / 1000);
 | 
						|
        GMPPrefs.setInt(
 | 
						|
          GMPPrefs.KEY_PLUGIN_LAST_DOWNLOAD_FAILED,
 | 
						|
          now,
 | 
						|
          gmpAddon.id
 | 
						|
        );
 | 
						|
        throw reason;
 | 
						|
      }
 | 
						|
    );
 | 
						|
  },
 | 
						|
};
 |