Bug 1079563 (part 2) - allow white-listed sites to request troubleshooting info. r=MattN
--- 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();
}
});