Bug 1307568 - add a diagnostic system add-on to measure uptake of various updates r?kmag draft
authorRobert Helmer <rhelmer@mozilla.com>
Thu, 26 Jan 2017 15:16:40 -0800
changeset 468117 5b316de35533849b4bb09fa7fb6a63320f8a1cdf
parent 466287 f72f13052d7479bf21cc81d8b8a6d44b34aab1a6
child 543853 cdf1595ee83b8bbb19d3ff280c4b8d9393920299
push id43359
push userrhelmer@mozilla.com
push dateMon, 30 Jan 2017 19:19:29 +0000
reviewerskmag
bugs1307568
milestone54.0a1
Bug 1307568 - add a diagnostic system add-on to measure uptake of various updates r?kmag MozReview-Commit-ID: 71uSNlGcfQt
browser/extensions/diagnostics/bootstrap.js
browser/extensions/diagnostics/install.rdf.in
browser/extensions/diagnostics/moz.build
browser/extensions/diagnostics/test/browser/.eslintrc.js
browser/extensions/diagnostics/test/browser/browser.ini
browser/extensions/diagnostics/test/browser/browser_check_installed.js
browser/extensions/diagnostics/test/browser/diagnostics-data.xpi
browser/extensions/diagnostics/test/moz.build
browser/extensions/moz.build
layout/tools/reftest/reftest-preferences.js
testing/profiles/prefs_general.js
testing/talos/talos/config.py
new file mode 100644
--- /dev/null
+++ b/browser/extensions/diagnostics/bootstrap.js
@@ -0,0 +1,277 @@
+/* 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/osfile.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+  "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Log",
+  "resource://gre/modules/Log.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+  "resource://gre/modules/Preferences.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+  "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryLog",
+  "resource://gre/modules/TelemetryLog.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
+  "resource://gre/modules/UpdateUtils.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 diagnosticsFile = "diagnostics-data.xpi";
+const diagnosticsPref = "extensions.diagnostics.v2.hasRun";
+const diagnosticsUrlPref = "extensions.systemAddon.diagnostics.url";
+const defaultDiagnosticsUrl = "https://ftp.mozilla.org/pub/system-addons/diagnostics/";
+const systemAddonUrlPref = "extensions.systemAddon.update.url";
+const diagnosticsChecksum = "a3fd8615a1b8dd8437ac1c0ef68ed4cf8a3795dfa53df02a6c309b1df92be9d3522a4d2e588d86289ed05c3103750e053cf09fd1c5c27cb6dcb9d2e59445afac";
+const hashFunction = "sha512";
+const TIMEOUT_DELAY_MS = 20000;
+
+function startup() {
+  let hasRun = Preferences.get(diagnosticsPref, false);
+  if (!hasRun) {
+    testSystemAddons();
+  }
+}
+function shutdown() {}
+function install() {}
+function uninstall() {
+  Preferences.reset(diagnosticsPref);
+}
+
+/**
+ * Convert a string containing binary values to hex.
+ * NOTE - originally from ProductAddonChecker
+ */
+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;
+}
+
+async function testSystemAddons() {
+  // 1: check that update URL is set to expected value.
+  const updateUrl = Preferences.get(systemAddonUrlPref);
+  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(updateUrl);
+  // try with default settings.
+  let serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"]
+                   .createInstance();
+  await downloadXML(url).then(
+    xml => TelemetryLog.log("DIAGNOSTICS_SUCCESS_AUS_BUILT_IN_CERT",
+                            [serializer.serializeToString(xml)]),
+    async function(err) {
+      TelemetryLog.log("DIAGNOSTICS_ERROR_AUS_BUILT_IN_CERT",
+                       [JSON.stringify(err, ["message", "arguments", "type", "name"])]);
+
+      // try allowing non-built-in certs.
+      await downloadXML(url, true).then(
+        xml => TelemetryLog.log("DIAGNOSTICS_SUCCESS_AUS_NON_BUILT_IN_CERT",
+                                [serializer.serializeToString(xml)]),
+        err => TelemetryLog.log("DIAGNOSTICS_ERROR_AUS_NON_BUILT_IN_CERT",
+                                [JSON.stringify(err, ["message", "arguments", "type", "name"])])
+      );
+    }
+  );
+
+  // 3: check if system add-on update directories are present.
+  // if system addon update dir exists, ensure it is r/w.
+  const updatesDir = await FileUtils.getDir("ProfD", ["features"], false);
+  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);
+    }
+  } else {
+    TelemetryLog.log("DIAGNOSTICS_WARN_SYSTEM_ADDON_UPDATES_DIR_DOES_NOT_EXIST");
+  }
+
+  // 4: connect to artifact server, download and check sha/size of deliverable
+  let diagnosticsUrl = Preferences.get(diagnosticsUrlPref, defaultDiagnosticsUrl);
+  diagnosticsUrl += diagnosticsFile;
+
+  // NOTE - based on computeHash in ProductAddonChecker.jsm
+  await downloadFile(diagnosticsUrl).then(
+    async function(path) {
+      const HASH_CHUNK_SIZE = 8192;
+
+      let file = await OS.File.open(path, { existing: true, read: true });
+      try {
+        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 = await file.read(HASH_CHUNK_SIZE);
+          await 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_CHECKSUM_MATCHES");
+        } else {
+          TelemetryLog.log("DIAGNOSTICS_ERROR_SYSTEM_ADDON_BAD_CHECKSUM");
+        }
+      } catch(e) {
+        TelemetryLog.log("DIAGNOSTICS_ERROR_SYSTEM_ADDON_COMPUTING_CHECKSUM_FAILED");
+      } finally {
+        await file.close();
+      }
+      await file.remove(false);
+    },
+    err => TelemetryLog.log("DIAGNOSTICS_ERROR_COULD_NOT_DOWNLOAD_SYSTEM_ADDON",
+                            [JSON.stringify(err, ["message", "arguments", "type", "name"])])
+  );
+
+  Preferences.set(diagnosticsPref, true);
+}
+
+/**
+ * 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";
+    xhr.open("GET", url);
+    xhr.send(null);
+  });
+}
+
+/**
+ * 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>2.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']
new file mode 100644
--- /dev/null
+++ b/browser/extensions/diagnostics/test/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+  "extends": [
+    "../../../../../testing/mochitest/browser.eslintrc.js"
+  ]
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/diagnostics/test/browser/browser.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+
+[browser_check_installed.js]
+support-files =
+  diagnostics-data.xpi
new file mode 100644
--- /dev/null
+++ b/browser/extensions/diagnostics/test/browser/browser_check_installed.js
@@ -0,0 +1,39 @@
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/TelemetryLog.jsm");
+
+add_task(function* test_enabled() {
+  let addon = yield new Promise(
+    resolve => AddonManager.getAddonByID("diagnostics@mozilla.org", resolve)
+  );
+  isnot(addon, null, "Check addon exists");
+  is(addon.version, "2.0", "Check version");
+  is(addon.name, "Diagnostics", "Check name");
+  ok(addon.isCompatible, "Check application compatibility");
+  ok(!addon.appDisabled, "Check not app disabled");
+  ok(addon.isActive, "Check addon is active");
+
+  let expected_results = [
+    "DIAGNOSTICS_ERROR_UNEXPECTED_UPDATE_URL",
+    "DIAGNOSTICS_SUCCESS_AUS_BUILT_IN_CERT",
+    "DIAGNOSTICS_SUCCESS_DOWNLOADED_SYSTEM_ADDON",
+    "DIAGNOSTICS_SUCCESS_SYSTEM_ADDON_CHECKSUM_MATCHES",
+    "DIAGNOSTICS_WARN_SYSTEM_ADDON_UPDATES_DIR_DOES_NOT_EXIST"
+  ];
+
+  while(true) {
+    let reasons = TelemetryLog.entries().map(entry => {
+      if (entry[0].startsWith("DIAGNOSTICS")) {
+        return entry[0];
+      }
+    });
+    if (expected_results.every(elem => reasons.includes(elem))) {
+      break;
+    }
+  }
+
+  delete window.TelemetryLog;
+});
new file mode 100644
index 0000000000000000000000000000000000000000..ffdcd26b46f4f9353ac03c54dbc03425fcc71a2e
GIT binary patch
literal 172
zc$^FHW@h1H0D;fdR$dH9fSW;vp|~_TIkmW0ucV?RG=!6Z`DR;J1PGT_a5FHnd}U-{
rU=aZ-4e(}Ul4HhYj0Bv$q!C1-7|IGU6vIGPHjq+AAPfZ3u3+r|tH>Qx
new file mode 100644
--- /dev/null
+++ b/browser/extensions/diagnostics/test/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',
 ]
 
--- a/layout/tools/reftest/reftest-preferences.js
+++ b/layout/tools/reftest/reftest-preferences.js
@@ -18,16 +18,17 @@ user_pref("hangmonitor.timeout", 0);
 user_pref("media.autoplay.enabled", true);
 // Disable updates
 user_pref("app.update.enabled", false);
 user_pref("app.update.staging.enabled", false);
 user_pref("app.update.url.android", "");
 // Disable addon updates and prefetching so we don't leak them
 user_pref("extensions.update.enabled", false);
 user_pref("extensions.systemAddon.update.url", "http://localhost/dummy-system-addons.xml");
+user_pref("extensions.systemAddon.diagnostics.url", "http://localhost/dummy-system-addons-diagnostics/");
 user_pref("extensions.getAddons.cache.enabled", false);
 // Disable blocklist updates so we don't have them reported as leaks
 user_pref("extensions.blocklist.enabled", false);
 // Make url-classifier updates so rare that they won't affect tests
 user_pref("urlclassifier.updateinterval", 172800);
 // Disable downscale-during-decode, since it makes reftests more difficult.
 user_pref("image.downscale-during-decode.enabled", false);
 // Checking whether two files are the same is slow on Windows.
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -109,16 +109,17 @@ user_pref("privacy.trackingprotection.in
 // Point update checks to the local testing server for fast failures
 user_pref("extensions.update.url", "http://%(server)s/extensions-dummy/updateURL");
 user_pref("extensions.update.background.url", "http://%(server)s/extensions-dummy/updateBackgroundURL");
 user_pref("extensions.blocklist.detailsURL", "http://%(server)s/extensions-dummy/blocklistDetailsURL");
 user_pref("extensions.blocklist.itemURL", "http://%(server)s/extensions-dummy/blocklistItemURL");
 user_pref("extensions.blocklist.url", "http://%(server)s/extensions-dummy/blocklistURL");
 user_pref("extensions.hotfix.url", "http://%(server)s/extensions-dummy/hotfixURL");
 user_pref("extensions.systemAddon.update.url", "http://%(server)s/dummy-system-addons.xml");
+user_pref("extensions.systemAddon.diagnostics.url", "http://example.com/browser/browser/extensions/diagnostics/test/browser/");
 // Turn off extension updates so they don't bother tests
 user_pref("extensions.update.enabled", false);
 // Make sure opening about:addons won't hit the network
 user_pref("extensions.webservice.discoverURL", "http://%(server)s/extensions-dummy/discoveryURL");
 // Make sure AddonRepository won't hit the network
 user_pref("extensions.getAddons.maxResults", 0);
 user_pref("extensions.getAddons.get.url", "http://%(server)s/extensions-dummy/repositoryGetURL");
 user_pref("extensions.getAddons.getWithPerformance.url", "http://%(server)s/extensions-dummy/repositoryGetWithPerformanceURL");
--- a/testing/talos/talos/config.py
+++ b/testing/talos/talos/config.py
@@ -142,16 +142,18 @@ DEFAULTS = dict(
             'http://127.0.0.1/extensions-dummy/repositoryBrowseURL',
         'extensions.getAddons.search.url':
             'http://127.0.0.1/extensions-dummy/repositorySearchURL',
         'media.gmp-manager.url':
             'http://127.0.0.1/gmpmanager-dummy/update.xml',
         'media.gmp-manager.updateEnabled': False,
         'extensions.systemAddon.update.url':
             'http://127.0.0.1/dummy-system-addons.xml',
+        'extensions.systemAddon.diagnostics.url':
+            'http://127.0.0.1/dummy-system-addons-diagnostics/',
         'extensions.shield-recipe-client.api_url':
             'https://127.0.0.1/selfsupport-dummy/',
         'media.navigator.enabled': True,
         'media.peerconnection.enabled': True,
         'media.navigator.permission.disabled': True,
         'media.capturestream_hints.enabled': True,
         'browser.contentHandlers.types.0.uri': 'http://127.0.0.1/rss?url=%s',
         'browser.contentHandlers.types.1.uri': 'http://127.0.0.1/rss?url=%s',