Bug 1456899 - Consider the page URI when checking WebRTC permissions in webrtcUI.jsm. r=nhnt11
While we already do a permission check in native webrtc code, the one
in webrtcUI is needed to provide extra protection against permission spam
by checking if there's a temporarily denied permission on the tab.
Since we introduced custom default permission values it is necessary to
also pass the URI to make sure we catch exceptions added by the user.
MozReview-Commit-ID: C8r6ymbKE3a
--- a/browser/base/content/test/webrtc/browser.ini
+++ b/browser/base/content/test/webrtc/browser.ini
@@ -3,16 +3,17 @@ support-files =
get_user_media.html
get_user_media_in_frame.html
get_user_media_content_script.js
head.js
[browser_devices_get_user_media.js]
skip-if = (os == "linux" && debug) # linux: bug 976544
[browser_devices_get_user_media_anim.js]
+[browser_devices_get_user_media_default_permissions.js]
[browser_devices_get_user_media_in_frame.js]
skip-if = debug # bug 1369731
[browser_devices_get_user_media_multi_process.js]
skip-if = debug && (os == "win" || os == "mac") # bug 1393761
[browser_devices_get_user_media_paused.js]
[browser_devices_get_user_media_screen.js]
skip-if = (os == "win" && ccov) # bug 1421724
[browser_devices_get_user_media_tear_off_tab.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_default_permissions.js
@@ -0,0 +1,164 @@
+/* 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/. */
+
+const permissionError = "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+
+const CAMERA_PREF = "permissions.default.camera";
+const MICROPHONE_PREF = "permissions.default.microphone";
+
+var gTests = [
+
+{
+ desc: "getUserMedia audio+video: globally blocking camera",
+ run: async function checkAudioVideo() {
+ Services.prefs.setIntPref(CAMERA_PREF, SitePermissions.BLOCK);
+
+ // Requesting audio+video shouldn't work.
+ let promise = promiseMessage(permissionError);
+ await promiseRequestDevice(true, true);
+ await promise;
+ await expectObserverCalled("recording-window-ended");
+ await checkNotSharing();
+
+ // Requesting only video shouldn't work.
+ promise = promiseMessage(permissionError);
+ await promiseRequestDevice(false, true);
+ await promise;
+ await expectObserverCalled("recording-window-ended");
+ await checkNotSharing();
+
+ // Requesting audio should work.
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true);
+ await promise;
+ await expectObserverCalled("getUserMedia:request");
+
+ is(PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareMicrophone-notification-icon", "anchored to mic icon");
+ checkDeviceSelectors(true);
+ let iconclass =
+ PopupNotifications.panel.firstChild.getAttribute("iconclass");
+ ok(iconclass.includes("microphone-icon"), "panel using microphone icon");
+
+ let indicator = promiseIndicatorWindow();
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstChild.button.click();
+ });
+ await expectObserverCalled("getUserMedia:response:allow");
+ await expectObserverCalled("recording-device-events");
+ Assert.deepEqual((await getMediaCaptureState()), {audio: true},
+ "expected microphone to be shared");
+
+ await indicator;
+ await checkSharingUI({audio: true});
+ await closeStream();
+ Services.prefs.clearUserPref(CAMERA_PREF);
+ }
+},
+
+{
+ desc: "getUserMedia video: globally blocking camera + local exception",
+ run: async function checkAudioVideo() {
+ let browser = gBrowser.selectedBrowser;
+ Services.prefs.setIntPref(CAMERA_PREF, SitePermissions.BLOCK);
+ // Overwrite the permission for that URI, requesting video should work again.
+ SitePermissions.set(browser.currentURI, "camera", SitePermissions.ALLOW);
+
+ // Requesting video should work.
+ let indicator = promiseIndicatorWindow();
+ let promise = promiseMessage("ok");
+ await promiseRequestDevice(false, true);
+ await promise;
+
+ await expectObserverCalled("getUserMedia:request");
+ await expectObserverCalled("getUserMedia:response:allow");
+ await expectObserverCalled("recording-device-events");
+ await indicator;
+ await checkSharingUI({video: true});
+ await closeStream();
+
+ SitePermissions.remove(browser.currentURI, "camera");
+ Services.prefs.clearUserPref(CAMERA_PREF);
+ }
+},
+
+{
+ desc: "getUserMedia audio+video: globally blocking microphone",
+ run: async function checkAudioVideo() {
+ Services.prefs.setIntPref(MICROPHONE_PREF, SitePermissions.BLOCK);
+
+ // Requesting audio+video shouldn't work.
+ let promise = promiseMessage(permissionError);
+ await promiseRequestDevice(true, true);
+ await promise;
+ await expectObserverCalled("recording-window-ended");
+ await checkNotSharing();
+
+ // Requesting only audio shouldn't work.
+ promise = promiseMessage(permissionError);
+ await promiseRequestDevice(true);
+ await promise;
+ await expectObserverCalled("recording-window-ended");
+ await checkNotSharing();
+
+ // Requesting video should work.
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promise;
+ await expectObserverCalled("getUserMedia:request");
+
+ is(PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareDevices-notification-icon", "anchored to device icon");
+ checkDeviceSelectors(false, true);
+ let iconclass =
+ PopupNotifications.panel.firstChild.getAttribute("iconclass");
+ ok(iconclass.includes("camera-icon"), "panel using devices icon");
+
+ let indicator = promiseIndicatorWindow();
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstChild.button.click();
+ });
+ await expectObserverCalled("getUserMedia:response:allow");
+ await expectObserverCalled("recording-device-events");
+ Assert.deepEqual((await getMediaCaptureState()), {video: true},
+ "expected camera to be shared");
+
+ await indicator;
+ await checkSharingUI({video: true});
+ await closeStream();
+ Services.prefs.clearUserPref(MICROPHONE_PREF);
+ }
+},
+
+{
+ desc: "getUserMedia audio: globally blocking microphone + local exception",
+ run: async function checkAudioVideo() {
+ let browser = gBrowser.selectedBrowser;
+ Services.prefs.setIntPref(MICROPHONE_PREF, SitePermissions.BLOCK);
+ // Overwrite the permission for that URI, requesting video should work again.
+ SitePermissions.set(browser.currentURI, "microphone", SitePermissions.ALLOW);
+
+ // Requesting audio should work.
+ let indicator = promiseIndicatorWindow();
+ let promise = promiseMessage("ok");
+ await promiseRequestDevice(true);
+ await promise;
+
+ await expectObserverCalled("getUserMedia:request");
+ await expectObserverCalled("getUserMedia:response:allow");
+ await expectObserverCalled("recording-device-events");
+ await indicator;
+ await checkSharingUI({audio: true});
+ await closeStream();
+
+ SitePermissions.remove(browser.currentURI, "microphone");
+ Services.prefs.clearUserPref(MICROPHONE_PREF);
+ }
+},
+
+];
+add_task(async function test() {
+ await runTests(gTests);
+});
--- a/browser/base/content/test/webrtc/head.js
+++ b/browser/base/content/test/webrtc/head.js
@@ -109,17 +109,17 @@ async function assertWebRTCIndicatorStat
is(ui.showCameraIndicator, expectVideo, "camera global indicator as expected");
is(ui.showMicrophoneIndicator, expectAudio, "microphone global indicator as expected");
is(ui.showScreenSharingIndicator, expectScreen, "screen global indicator as expected");
let windows = Services.wm.getEnumerator("navigator:browser");
while (windows.hasMoreElements()) {
let win = windows.getNext();
let menu = win.document.getElementById("tabSharingMenu");
- is(menu && !menu.hidden, !!expected, "WebRTC menu should be " + expectedState);
+ is(!!menu && !menu.hidden, !!expected, "WebRTC menu should be " + expectedState);
}
if (!("nsISystemStatusBar" in Ci)) {
if (!expected) {
let win = Services.wm.getMostRecentWindow("Browser:WebRTCGlobalIndicator");
if (win) {
await new Promise((resolve, reject) => {
win.addEventListener("unload", function listener(e) {
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -336,39 +336,39 @@ function stopRecording(aBrowser, aReques
set.delete(aRequest.windowID + aRequest.mediaSource + aRequest.rawID);
}
}
function prompt(aBrowser, aRequest) {
let { audioDevices, videoDevices, sharingScreen, sharingAudio,
requestTypes } = aRequest;
+ let uri;
+ try {
+ // This fails for principals that serialize to "null", e.g. file URIs.
+ uri = Services.io.newURI(aRequest.origin);
+ } catch (e) {
+ uri = Services.io.newURI(aRequest.documentURI);
+ }
+
// If the user has already denied access once in this tab,
// deny again without even showing the notification icon.
if ((audioDevices.length && SitePermissions
- .get(null, "microphone", aBrowser).state == SitePermissions.BLOCK) ||
+ .get(uri, "microphone", aBrowser).state == SitePermissions.BLOCK) ||
(videoDevices.length && SitePermissions
- .get(null, sharingScreen ? "screen" : "camera", aBrowser).state == SitePermissions.BLOCK)) {
+ .get(uri, sharingScreen ? "screen" : "camera", aBrowser).state == SitePermissions.BLOCK)) {
denyRequest(aBrowser, aRequest);
return;
}
// Tell the browser to refresh the identity block display in case there
// are expired permission states.
aBrowser.dispatchEvent(new aBrowser.ownerGlobal
.CustomEvent("PermissionStateChange"));
- let uri;
- try {
- // This fails for principals that serialize to "null", e.g. file URIs.
- uri = Services.io.newURI(aRequest.origin);
- } catch (e) {
- uri = Services.io.newURI(aRequest.documentURI);
- }
-
let chromeDoc = aBrowser.ownerDocument;
let stringBundle = chromeDoc.defaultView.gNavigatorBundle;
// Mind the order, because for simplicity we're iterating over the list using
// "includes()". This allows the rotation of string identifiers. We list the
// full identifiers here so they can be cross-referenced more easily.
let joinedRequestTypes = requestTypes.join("And");
let stringId = [