Bug 1307568 - add a diagnostic system add-on to measure uptake of various updates r?kmag
MozReview-Commit-ID: DsJ3lluwBjk
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',
]