Bug 1079563 (part 2) - allow white-listed sites to request troubleshooting info. r=MattN

This commit is contained in:
Mark Hammond 2014-11-07 18:12:26 +11:00
parent 2b75638713
commit e42b3b0a84
8 changed files with 226 additions and 13 deletions

View File

@ -14,3 +14,7 @@ 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

View File

@ -385,6 +385,9 @@ 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

View File

@ -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");
});

View File

@ -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>

View File

@ -132,6 +132,9 @@ XPCOMUtils.defineLazyGetter(this, "ShellService", function() {
XPCOMUtils.defineLazyModuleGetter(this, "FormValidationHandler",
"resource:///modules/FormValidationHandler.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WebChannel",
"resource://gre/modules/WebChannel.jsm");
const PREF_PLUGINS_NOTIFYUSER = "plugins.update.notifyUser";
const PREF_PLUGINS_UPDATEURL = "plugins.update.url";
@ -767,6 +770,21 @@ BrowserGlue.prototype = {
}
#endif
// A channel for "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);
});
}
});
this._trackSlowStartup();
// Offer to reset a user's profile if it hasn't been used for 60 days.

View File

@ -77,7 +77,7 @@ let WebChannelBroker = Object.create({
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);
}
@ -133,17 +133,43 @@ let WebChannelBroker = Object.create({
*
* @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 = {
@ -154,9 +180,16 @@ this.WebChannel.prototype = {
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

View File

@ -8,10 +8,11 @@ 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(),
@ -34,7 +35,7 @@ function run_test() {
*/
/**
* Test channel listening
* Test channel listening with originOrPermission being an nsIURI.
*/
add_task(function test_web_channel_listen() {
return new Promise((resolve, reject) => {
@ -43,7 +44,54 @@ add_task(function test_web_channel_listen() {
});
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) {

View File

@ -55,7 +55,9 @@ 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");