Bug 1079563 (part 2) - allow white-listed sites to request troubleshooting info. r=MattN
☠☠ backed out by 9ff5cf552a7f ☠ ☠
authorMark Hammond <mhammond@skippinet.com.au>
Fri, 31 Oct 2014 11:29:54 +1100
changeset 213270 780d8f101fe094f5b9545523210c0b2be9bdc1e2
parent 213269 cb3179f8ea2bc1429eb22de1700864fd23e8ddd7
child 213271 44161bd0f41c7e24a9a18e4fead67dfd003b4fc3
push idunknown
push userunknown
push dateunknown
reviewersMattN
bugs1079563
milestone36.0a1
Bug 1079563 (part 2) - allow white-listed sites to request troubleshooting info. r=MattN
browser/app/default_permissions
browser/base/content/browser.js
browser/base/content/test/general/browser.ini
browser/base/content/test/general/browser_remoteTroubleshoot.js
browser/base/content/test/general/test_remoteTroubleshoot.html
toolkit/modules/WebChannel.jsm
toolkit/modules/tests/xpcshell/test_web_channel.js
toolkit/modules/tests/xpcshell/test_web_channel_broker.js
--- a/browser/app/default_permissions
+++ b/browser/app/default_permissions
@@ -9,8 +9,12 @@
 # UITour
 host	uitour	1	www.mozilla.org
 host	uitour	1	support.mozilla.org
 host	uitour	1	about:home
 
 # XPInstall
 host	install	1	addons.mozilla.org
 host	install	1	marketplace.firefox.com
+
+# Remote troubleshooting
+host	remote-troubleshooting	1	input.mozilla.org
+host	remote-troubleshooting	1	support.mozilla.org
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -29,16 +29,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/NewTabUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ContentSearch",
                                   "resource:///modules/ContentSearch.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AboutHome",
                                   "resource:///modules/AboutHome.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "gDNSService",
                                    "@mozilla.org/network/dns-service;1",
                                    "nsIDNSService");
+XPCOMUtils.defineLazyModuleGetter(this, "WebChannel",
+                                  "resource://gre/modules/WebChannel.jsm");
 
 const nsIWebNavigation = Ci.nsIWebNavigation;
 
 var gLastBrowserCharset = null;
 var gProxyFavIcon = null;
 var gLastValidURLStr = "";
 var gInPrintPreviewMode = false;
 var gContextMenu = null; // nsContextMenu instance
@@ -1294,16 +1296,31 @@ var gBrowserInit = {
 
     window.addEventListener("mousemove", MousePosTracker, false);
     window.addEventListener("dragover", MousePosTracker, false);
 
     gNavToolbox.addEventListener("customizationstarting", CustomizationHandler);
     gNavToolbox.addEventListener("customizationchange", CustomizationHandler);
     gNavToolbox.addEventListener("customizationending", CustomizationHandler);
 
+    // Initialize "remote troubleshooting" code...
+    let channel = new WebChannel("remote-troubleshooting", "remote-troubleshooting");
+    channel.listen((id, data, target) => {
+      if (data.command == "request") {
+        let {Troubleshoot} = Cu.import("resource://gre/modules/Troubleshoot.jsm", {});
+        Troubleshoot.snapshot(data => {
+          // for privacy we remove crash IDs and all preferences (but bug 1091944
+          // exists to expose prefs once we are confident of privacy implications)
+          delete data.crashes;
+          delete data.modifiedPreferences;
+          channel.send(data, target);
+        });
+      }
+    });
+
     // End startup crash tracking after a delay to catch crashes while restoring
     // tabs and to postpone saving the pref to disk.
     try {
       const startupCrashEndDelay = 30 * 1000;
       setTimeout(Services.startup.trackStartupCrashEnd, startupCrashEndDelay);
     } catch (ex) {
       Cu.reportError("Could not end startup crash tracking: " + ex);
     }
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -384,16 +384,19 @@ skip-if = buildapp == 'mulet' || e10s # 
 [browser_popup_blocker.js]
 [browser_printpreview.js]
 skip-if = buildapp == 'mulet' || e10s # Bug ?????? - timeout after logging "Error: Channel closing: too late to send/recv, messages will be lost"
 [browser_private_browsing_window.js]
 skip-if = buildapp == 'mulet'
 [browser_private_no_prompt.js]
 skip-if = buildapp == 'mulet'
 [browser_relatedTabs.js]
+[browser_remoteTroubleshoot.js]
+support-files =
+  test_remoteTroubleshoot.html
 [browser_removeTabsToTheEnd.js]
 [browser_removeUnsafeProtocolsFromURLBarPaste.js]
 skip-if = e10s
 [browser_restore_isAppTab.js]
 [browser_sanitize-download-history.js]
 skip-if = true # bug 432425
 [browser_sanitize-passwordDisabledHosts.js]
 [browser_sanitize-sitepermissions.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_remoteTroubleshoot.js
@@ -0,0 +1,64 @@
+/* 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/. */
+
+let {WebChannel} = Cu.import("resource://gre/modules/WebChannel.jsm", {});
+
+const TEST_URL_TAIL = "example.com/browser/browser/base/content/test/general/test_remoteTroubleshoot.html"
+const TEST_URI_GOOD = Services.io.newURI("https://" + TEST_URL_TAIL, null, null);
+const TEST_URI_BAD = Services.io.newURI("http://" + TEST_URL_TAIL, null, null);
+
+// Creates a one-shot web-channel for the test data to be sent back from the test page.
+function promiseChannelResponse(channelID, originOrPermission) {
+  return new Promise((resolve, reject) => {
+    let channel = new WebChannel(channelID, originOrPermission);
+    channel.listen((id, data, target) => {
+      channel.stopListening();
+      resolve(data);
+    });
+  });
+};
+
+// Loads the specified URI in a new tab and waits for it to send us data on our
+// test web-channel and resolves with that data.
+function promiseNewChannelResponse(uri) {
+  let channelPromise = promiseChannelResponse("test-remote-troubleshooting-backchannel",
+                                              uri);
+  let tab = gBrowser.loadOneTab(uri.spec, { inBackground: false });
+  return promiseTabLoaded(tab).then(
+    () => channelPromise
+  ).then(data => {
+    gBrowser.removeTab(tab);
+    return data;
+  });
+}
+
+add_task(function*() {
+  // We haven't set a permission yet - so even the "good" URI should fail.
+  let got = yield promiseNewChannelResponse(TEST_URI_GOOD);
+  // Should have no data.
+  Assert.ok(got.message === undefined, "should have failed to get any data");
+
+  // Add a permission manager entry for our URI.
+  Services.perms.add(TEST_URI_GOOD,
+                     "remote-troubleshooting",
+                     Services.perms.ALLOW_ACTION);
+  registerCleanupFunction(() => {
+    Services.perms.remove(TEST_URI_GOOD.spec, "remote-troubleshooting");
+  });
+
+  // Try again - now we are expecting a response with the actual data.
+  got = yield promiseNewChannelResponse(TEST_URI_GOOD);
+
+  // Check some keys we expect to always get.
+  Assert.ok(got.message.extensions, "should have extensions");
+  Assert.ok(got.message.graphics, "should have graphics");
+
+  // And check some keys we know we decline to return.
+  Assert.ok(!got.message.modifiedPreferences, "should not have a modifiedPreferences key");
+  Assert.ok(!got.message.crashes, "should not have crash info");
+
+  // Now a http:// URI - should get nothing even with the permission setup.
+  got = yield promiseNewChannelResponse(TEST_URI_BAD);
+  Assert.ok(got.message === undefined, "should have failed to get any data");
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/test_remoteTroubleshoot.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<script>
+// Add a listener for responses to our remote requests.
+window.addEventListener("WebChannelMessageToContent", function (event) {
+  if (event.detail.id == "remote-troubleshooting") {
+    // Send what we got back to the test.
+    var backEvent = new window.CustomEvent("WebChannelMessageToChrome", {
+      detail: {
+        id: "test-remote-troubleshooting-backchannel",
+        message: {
+          message: event.detail.message,
+        },
+      },
+    });
+    window.dispatchEvent(backEvent);
+    // and stick it in our DOM just for good measure/diagnostics.
+    document.getElementById("troubleshooting").textContent =
+      JSON.stringify(event.detail.message, null, 2);
+  }
+});
+
+// Make a request for the troubleshooting data as we load.
+window.onload = function() {
+  var event = new window.CustomEvent("WebChannelMessageToChrome", {
+    detail: {
+      id: "remote-troubleshooting",
+      message: {
+        command: "request",
+      },
+    },
+  });
+  window.dispatchEvent(event);
+}
+</script>
+
+<body>
+  <pre id="troubleshooting"/>
+</body>
+
+</html>
--- a/toolkit/modules/WebChannel.jsm
+++ b/toolkit/modules/WebChannel.jsm
@@ -72,17 +72,17 @@ let WebChannelBroker = Object.create({
       if (!event.principal) {
         this._sendErrorEventToContent(data.id, sender, "Message principal missing");
       } else {
         let validChannelFound = false;
         data.message = data.message || {};
 
         for (var channel of this._channelMap.keys()) {
           if (channel.id === data.id &&
-            channel.origin.prePath === event.principal.origin) {
+            channel._originCheckCallback(event.principal)) {
             validChannelFound = true;
             channel.deliver(data, sender);
           }
         }
 
         // if no valid origins send an event that there is no such valid channel
         if (!validChannelFound) {
           this._sendErrorEventToContent(data.id, sender, "No Such Channel");
@@ -128,40 +128,73 @@ let WebChannelBroker = Object.create({
 });
 
 
 /**
  * Creates a new WebChannel that listens and sends messages over some channel id
  *
  * @param id {String}
  *        WebChannel id
- * @param origin {nsIURI}
- *        Valid origin that should be part of requests for this channel
+ * @param originOrPermission {nsIURI/string}
+ *        If an nsIURI, a valid origin that should be part of requests for
+ *        this channel.  If a string, a permission for which the permission
+ *        manager will be checked to determine if the request is allowed. Note
+ *        that in addition to the permission manager check, the request must
+ *        be made over https://
  * @constructor
  */
-this.WebChannel = function(id, origin) {
-  if (!id || !origin) {
-    throw new Error("WebChannel id and origin are required.");
+this.WebChannel = function(id, originOrPermission) {
+  if (!id || !originOrPermission) {
+    throw new Error("WebChannel id and originOrPermission are required.");
   }
 
   this.id = id;
-  this.origin = origin;
+  // originOrPermission can be either an nsIURI or a string representing a
+  // permission name.
+  if (typeof originOrPermission == "string") {
+    this._originCheckCallback = requestPrincipal => {
+      // The permission manager operates on domain names rather than true
+      // origins (bug 1066517).  To mitigate that, we explicitly check that
+      // the scheme is https://.
+      let uri = Services.io.newURI(requestPrincipal.origin, null, null);
+      if (uri.scheme != "https") {
+        return false;
+      }
+      // OK - we have https - now we can check the permission.
+      let perm = Services.perms.testExactPermissionFromPrincipal(requestPrincipal,
+                                                                 originOrPermission);
+      return perm == Ci.nsIPermissionManager.ALLOW_ACTION;
+    }
+  } else {
+    // a simple URI, so just check for an exact match.
+    this._originCheckCallback = requestPrincipal => {
+      return originOrPermission.prePath === requestPrincipal.origin;
+    }
+  }
+  this._originOrPermission = originOrPermission;
 };
 
 this.WebChannel.prototype = {
 
   /**
    * WebChannel id
    */
   id: null,
 
   /**
-   * WebChannel origin
+   * The originOrPermission value passed to the constructor, mainly for
+   * debugging and tests.
    */
-  origin: null,
+  _originOrPermission: null,
+
+  /**
+   * Callback that will be called with the principal of an incoming message
+   * to check if the request should be dispatched to the listeners.
+   */
+  _originCheckCallback: null,
 
   /**
    * WebChannelBroker that manages WebChannels
    */
   _broker: WebChannelBroker,
 
   /**
    * Callback that will be called with the contents of an incoming message
--- a/toolkit/modules/tests/xpcshell/test_web_channel.js
+++ b/toolkit/modules/tests/xpcshell/test_web_channel.js
@@ -3,20 +3,21 @@
 
 "use strict";
 
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/WebChannel.jsm");
 
-const ERROR_ID_ORIGIN_REQUIRED = "WebChannel id and origin are required.";
+const ERROR_ID_ORIGIN_REQUIRED = "WebChannel id and originOrPermission are required.";
 const VALID_WEB_CHANNEL_ID = "id";
 const URL_STRING = "http://example.com";
 const VALID_WEB_CHANNEL_ORIGIN = Services.io.newURI(URL_STRING, null, null);
+const TEST_PERMISSION_NAME = "test-webchannel-permissions";
 
 let MockWebChannelBroker = {
   _channelMap: new Map(),
   registerChannel: function(channel) {
     if (!this._channelMap.has(channel)) {
       this._channelMap.set(channel);
     }
   },
@@ -29,26 +30,73 @@ function run_test() {
   run_next_test();
 }
 
 /**
  * Web channel tests
  */
 
 /**
- * Test channel listening
+ * Test channel listening with originOrPermission being an nsIURI.
  */
 add_task(function test_web_channel_listen() {
   return new Promise((resolve, reject) => {
     let channel = new WebChannel(VALID_WEB_CHANNEL_ID, VALID_WEB_CHANNEL_ORIGIN, {
       broker: MockWebChannelBroker
     });
     let delivered = 0;
     do_check_eq(channel.id, VALID_WEB_CHANNEL_ID);
-    do_check_eq(channel.origin.spec, VALID_WEB_CHANNEL_ORIGIN.spec);
+    do_check_eq(channel._originOrPermission.spec, VALID_WEB_CHANNEL_ORIGIN.spec);
+    do_check_eq(channel._deliverCallback, null);
+
+    channel.listen(function(id, message, target) {
+      do_check_eq(id, VALID_WEB_CHANNEL_ID);
+      do_check_true(message);
+      do_check_true(message.command);
+      do_check_true(target.sender);
+      delivered++;
+      // 2 messages should be delivered
+      if (delivered === 2) {
+        channel.stopListening();
+        do_check_eq(channel._deliverCallback, null);
+        resolve();
+      }
+    });
+
+    // send two messages
+    channel.deliver({
+      id: VALID_WEB_CHANNEL_ID,
+      message: {
+        command: "one"
+      }
+    }, { sender: true });
+
+    channel.deliver({
+      id: VALID_WEB_CHANNEL_ID,
+      message: {
+        command: "two"
+      }
+    }, { sender: true });
+  });
+});
+
+/**
+ * Test channel listening with originOrPermission being a permission string.
+ */
+add_task(function test_web_channel_listen_permission() {
+  return new Promise((resolve, reject) => {
+    // add a new permission
+    Services.perms.add(VALID_WEB_CHANNEL_ORIGIN, TEST_PERMISSION_NAME, Services.perms.ALLOW_ACTION);
+    do_register_cleanup(() => Services.perms.remove(VALID_WEB_CHANNEL_ORIGIN.spec, TEST_PERMISSION_NAME));
+    let channel = new WebChannel(VALID_WEB_CHANNEL_ID, TEST_PERMISSION_NAME, {
+      broker: MockWebChannelBroker
+    });
+    let delivered = 0;
+    do_check_eq(channel.id, VALID_WEB_CHANNEL_ID);
+    do_check_eq(channel._originOrPermission, TEST_PERMISSION_NAME);
     do_check_eq(channel._deliverCallback, null);
 
     channel.listen(function(id, message, target) {
       do_check_eq(id, VALID_WEB_CHANNEL_ID);
       do_check_true(message);
       do_check_true(message.command);
       do_check_true(target.sender);
       delivered++;
--- a/toolkit/modules/tests/xpcshell/test_web_channel_broker.js
+++ b/toolkit/modules/tests/xpcshell/test_web_channel_broker.js
@@ -50,17 +50,19 @@ add_test(function test_web_channel_broke
 
 /**
  * Test WebChannelBroker _listener test
  */
 add_task(function test_web_channel_broker_listener() {
   return new Promise((resolve, reject) => {
     var channel = new Object({
       id: VALID_WEB_CHANNEL_ID,
-      origin: VALID_WEB_CHANNEL_ORIGIN,
+      _originCheckCallback: requestPrincipal => {
+        return VALID_WEB_CHANNEL_ORIGIN.prePath === requestPrincipal.origin;
+      },
       deliver: function(data, sender) {
         do_check_eq(data.id, VALID_WEB_CHANNEL_ID);
         do_check_eq(data.message.command, "hello");
         WebChannelBroker.unregisterChannel(channel);
         resolve();
       }
     });