Bug 1307568 - add a diagnostic system add-on to measure uptake of various updates r?kmag draft
authorRobert Helmer <rhelmer@mozilla.com>
Sat, 10 Dec 2016 15:41:37 -1000
changeset 465738 b94a68170bb03541004610f468bcf91ed5aabda8
parent 465527 8ff550409e1d1f8b54f6f7f115545dbef857be0b
child 543237 ab11cab62119f8d54093abd15dd11f0d63032bca
push id42695
push userrhelmer@mozilla.com
push dateTue, 24 Jan 2017 19:33:18 +0000
reviewerskmag
bugs1307568
milestone54.0a1
Bug 1307568 - add a diagnostic system add-on to measure uptake of various updates r?kmag MozReview-Commit-ID: DsJ3lluwBjk
browser/extensions/diagnostics/bootstrap.js
browser/extensions/diagnostics/install.rdf.in
browser/extensions/diagnostics/moz.build
browser/extensions/moz.build
new file mode 100644
--- /dev/null
+++ b/browser/extensions/diagnostics/bootstrap.js
@@ -0,0 +1,236 @@
+/* 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";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/CertUtils.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/TelemetryLog.jsm");
+Cu.import("resource://gre/modules/UpdateUtils.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+
+const expectedUpdateURL = "https://aus5.mozilla.org/update/3/SystemAddons/%VERSION%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/update.xml";
+const systemAddonURL = "https://ftp.mozilla.org/pub/system-addons/diagnostics/diagnostics@mozilla.org-ff51-v1.0.xpi";
+const diagnosticsPref = "extensions.diagnostics.v1.hasRun";
+// TODO use real SHA.
+const systemAddonSHA = "abcd...";
+const hashFunction = "sha256";
+const updatesDir = FileUtils.getDir("ProfD", ["features"], false);
+const TIMEOUT_DELAY_MS = 20000;
+
+function startup() {
+  let hasRun = Preferences.get(diagnosticsPref, false);
+  if (!hasRun) {
+    testSystemAddons();
+    Preferences.set(diagnosticsPref, true);
+  }
+}
+function shutdown() {}
+function install() {}
+function uninstall() {
+  Preferences.reset(diagnosticsPref);
+}
+
+async function testSystemAddons() {
+  // 1: check that update URL is set to expected value.
+  const updateUrl = Preferences.get("extensions.systemAddon.update.url");
+  if (updateUrl === expectedUpdateURL) {
+    TelemetryLog.log("DIAGNOSTICS_SUCCESS_EXPECTED_UPDATE_URL");
+  } else {
+    TelemetryLog.log("DIAGNOSTICS_ERROR_UNEXPECTED_UPDATE_URL", [updateUrl]);
+  }
+
+  // 2: try to connect to AUS.
+  const url = UpdateUtils.formatUpdateURL(expectedUpdateURL);
+  // try with default settings.
+  let serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"].
+                   createInstance(Ci.nsIDOMSerializer);
+  await downloadXML(url)
+    .then((xml) =>
+      TelemetryLog.log("DIAGNOSTICS_SUCCESS_AUS_BUILT_IN_CERT", [serializer.serializeToString(xml)])
+    )
+    .catch(err =>
+      TelemetryLog.log("DIAGNOSTICS_ERROR_AUS_BUILT_IN_CERT", [err])
+    );
+
+  // try allowing non-built-in certs.
+  await downloadXML(url, true)
+    .then((xml) =>
+      TelemetryLog.log("DIAGNOSTICS_SUCCESS_AUS_NON_BUILT_IN_CERT", [serializer.serializeToString(xml)])
+    )
+    .catch(err =>
+      TelemetryLog.log("DIAGNOSTICS_ERROR_AUS_NON_BUILT_IN_CERT", [err])
+    );
+  // 3: try to connect to artifact server.
+  await downloadFile(systemAddonURL)
+    .then(function(file) {
+      TelemetryLog.log("DIAGNOSTICS_SUCCESS_DOWNLOADED_SYSTEM_ADDON");
+      let hasher = Cc["@mozilla.org/security/hash;1"].
+      createInstance(Ci.nsICryptoHash);
+      hasher.initWithString(hashFunction);
+      let bytes;
+      do {
+        bytes = yield file.read(HASH_CHUNK_SIZE);
+        hasher.update(bytes, bytes.length);
+      } while (bytes.length == HASH_CHUNK_SIZE);
+      let result = binaryToHex(hasher.finish(false));
+      if (result) {
+        TelemetryLog.log("DIAGNOSTICS_SUCCESS_SYSTEM_ADDON_SHA_MATCHES")
+      } else {
+        TelemetryLog.log("DIAGNOSTICS_ERROR_SYSTEM_ADDON_BAD_SHA")
+      }
+      file.remove(false);
+    })
+    .catch(err =>
+      TelemetryLog.log("DIAGNOSTICS_ERROR_COULD_NOT_DOWNLOAD_SYSTEM_ADDON", [err])
+    );
+
+  // 4: check if system add-on update files are present.
+  // if system addon update dir exists, ensure it is r/w.
+  if (updatesDir.exists()) {
+    if (updatesDir.isReadable && updatesDir.isWritable) {
+      TelemetryLog.log("DIAGNOSTICS_SUCCESS_SYSTEM_ADDON_UPDATES_DIR_RW")
+      // TODO if system addon updates are present, ensure they are r/w.
+      if (true) {
+        TelemetryLog.log("DIAGNOSTICS_SUCCESS_SYSTEM_ADDON_UPDATES_RW")
+      } else {
+        TelemetryLog.log("DIAGNOSTICS_ERROR_SYSTEM_ADDON_UPDATES_NOT_RW")
+      }
+    } else {
+      TelemetryLog.log("DIAGNOSTICS_ERROR_SYSTEM_ADDON_UPDATES_DIR_NOT_RW", updatesDir.permissions)
+    }
+  }
+}
+
+/**
+ * Downloads file from a URL using XHR.
+ * NOTE - based on ProductAddonChecker.jsm
+ *
+ * @param  url
+ *         The url to download from.
+ * @return a promise that resolves to the path of a temporary file or rejects
+ *         with a JS exception in case of error.
+ */
+function downloadFile(url) {
+  return new Promise((resolve, reject) => {
+    let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
+                  createInstance(Ci.nsISupports);
+    xhr.onload = function(response) {
+      console.info("downloadXHR File download. status=" + xhr.status);
+      if (xhr.status != 200 && xhr.status != 206) {
+        reject(Components.Exception("File download failed", xhr.status));
+        return;
+      }
+      Task.spawn(function* () {
+        let f = yield OS.File.openUnique(OS.Path.join(OS.Constants.Path.tmpDir, "tmpaddon"));
+        let path = f.path;
+        console.info(`Downloaded file will be saved to ${path}`);
+        yield f.file.close();
+        yield OS.File.writeAtomic(path, new Uint8Array(xhr.response));
+        return path;
+      }).then(resolve, reject);
+    };
+
+    let fail = (event) => {
+      let request = event.target;
+      let status = getRequestStatus(request);
+      let message = "Failed downloading via XHR, status: " + status +  ", reason: " + event.type;
+      console.warn(message);
+      let ex = new Error(message);
+      ex.status = status;
+      reject(ex);
+    };
+    xhr.addEventListener("error", fail);
+    xhr.addEventListener("abort", fail);
+
+    xhr.responseType = "arraybuffer";
+    try {
+      xhr.open("GET", url);
+      xhr.send(null);
+    } catch (ex) {
+      reject(ex);
+    }
+  });
+}
+
+/**
+ * Downloads an XML document from a URL optionally testing the SSL certificate
+ * for certain attributes.
+ * NOTE - based on ProductAddonChecker.jsm
+ *
+ * @param  url
+ *         The url to download from.
+ * @param  allowNonBuiltIn
+ *         Whether to trust SSL certificates without a built-in CA issuer.
+ * @param  allowedCerts
+ *         The list of certificate attributes to match the SSL certificate
+ *         against or null to skip checks.
+ * @return a promise that resolves to the DOM document downloaded or rejects
+ *         with a JS exception in case of error.
+ */
+function downloadXML(url, allowNonBuiltIn = false, allowedCerts = null) {
+  return new Promise((resolve, reject) => {
+    let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
+                  createInstance(Ci.nsISupports);
+    // This is here to let unit test code override XHR
+    if (request.wrappedJSObject) {
+      request = request.wrappedJSObject;
+    }
+    request.open("GET", url, true);
+    request.channel.notificationCallbacks = new BadCertHandler(allowNonBuiltIn);
+    // Prevent the request from reading from the cache.
+    request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+    // Prevent the request from writing to the cache.
+    request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+    request.timeout = TIMEOUT_DELAY_MS;
+
+    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.
+    request.setRequestHeader("Cache-Control", "no-cache");
+    // HTTP/1.0 servers might not implement Cache-Control and
+    // might only implement Pragma: no-cache
+    request.setRequestHeader("Pragma", "no-cache");
+
+    let fail = (event) => {
+      request = event.target;
+      let status = getRequestStatus(request);
+      let message = "Failed downloading XML, status: " + status +  ", reason: " + event.type;
+      console.warn(message);
+      let ex = new Error(message);
+      ex.status = status;
+      reject(ex);
+    };
+
+    let success = (event) => {
+      console.info("Completed downloading document");
+      request = event.target;
+
+      try {
+        checkCert(request.channel, allowNonBuiltIn, allowedCerts);
+      } catch (ex) {
+        console.error("Request failed certificate checks: " + ex);
+        ex.status = getRequestStatus(request);
+        reject(ex);
+        return;
+      }
+
+      resolve(request.responseXML);
+    };
+
+    request.addEventListener("error", fail, false);
+    request.addEventListener("abort", fail, false);
+    request.addEventListener("timeout", fail, false);
+    request.addEventListener("load", success, false);
+
+    console.info("sending request to: " + url);
+    request.send(null);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/diagnostics/install.rdf.in
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+#filter substitution
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>diagnostics@mozilla.org</em:id>
+    <em:version>1.0</em:version>
+    <em:type>2</em:type>
+    <em:bootstrap>true</em:bootstrap>
+    <em:multiprocessCompatible>true</em:multiprocessCompatible>
+
+    <!-- Target Application this extension can install into,
+        with minimum and maximum supported versions. -->
+    <em:targetApplication>
+      <Description>
+        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+        <em:minVersion>@MOZ_APP_VERSION@</em:minVersion>
+        <em:maxVersion>@MOZ_APP_MAXVERSION@</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+
+    <!-- Front End MetaData -->
+    <em:name>Diagnostics</em:name>
+    <em:description>Diagnose Firefox update problems.</em:description>
+  </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/diagnostics/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
+DEFINES['MOZ_APP_MAXVERSION'] = CONFIG['MOZ_APP_MAXVERSION']
+
+FINAL_TARGET_FILES.features['diagnostics@mozilla.org'] += [
+  'bootstrap.js'
+]
+
+FINAL_TARGET_PP_FILES.features['diagnostics@mozilla.org'] += [
+  'install.rdf.in'
+]
+
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
--- a/browser/extensions/moz.build
+++ b/browser/extensions/moz.build
@@ -1,16 +1,17 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 DIRS += [
     'aushelper',
+    'diagnostics',
     'disableSHA1rollout',
     'e10srollout',
     'pdfjs',
     'pocket',
     'webcompat',
     'shield-recipe-client',
 ]