/* 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"; requestLongerTimeout(2); const permissionError = "error: NotAllowedError: The request is not allowed " + "by the user agent or the platform in the current context."; const SAME_ORIGIN = "https://example.com"; const CROSS_ORIGIN = "https://example.org"; const PATH = "/browser/browser/base/content/test/webrtc/get_user_media.html"; const PATH2 = "/browser/browser/base/content/test/webrtc/get_user_media2.html"; const GRACE_PERIOD_MS = 3000; const WAIT_PERIOD_MS = GRACE_PERIOD_MS + 500; // We're inherently testing timeouts (grace periods) /* eslint-disable mozilla/no-arbitrary-setTimeout */ const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); const perms = SitePermissions; // These tests focus on camera and microphone, so we define some helpers. async function prompt(audio, video) { let observerPromise = expectObserverCalled("getUserMedia:request"); let promise = promisePopupNotificationShown("webRTC-shareDevices"); await promiseRequestDevice(audio, video); await promise; await observerPromise; const expectedDeviceSelectorTypes = [ audio && "microphone", video && "camera", ].filter(x => x); checkDeviceSelectors(expectedDeviceSelectorTypes); } async function allow(audio, video) { let indicator = promiseIndicatorWindow(); let observerPromise1 = expectObserverCalled("getUserMedia:response:allow"); let observerPromise2 = expectObserverCalled("recording-device-events"); await promiseMessage("ok", () => { PopupNotifications.panel.firstElementChild.button.click(); }); await observerPromise1; await observerPromise2; Assert.deepEqual( Object.assign({ audio: false, video: false }, await getMediaCaptureState()), { audio, video }, `expected ${video ? "camera " : ""} ${audio ? "microphone " : ""}shared` ); await indicator; await checkSharingUI({ audio, video }); } async function deny(action) { let observerPromise1 = expectObserverCalled("getUserMedia:response:deny"); let observerPromise2 = expectObserverCalled("recording-window-ended"); await promiseMessage(permissionError, () => { activateSecondaryAction(action); }); await observerPromise1; await observerPromise2; await checkNotSharing(); } async function noPrompt(audio, video) { let observerPromises = [ expectObserverCalled("getUserMedia:request"), expectObserverCalled("getUserMedia:response:allow"), expectObserverCalled("recording-device-events"), ]; let promise = promiseMessage("ok"); await promiseRequestDevice(audio, video); await promise; await Promise.all(observerPromises); await promiseNoPopupNotification("webRTC-shareDevices"); Assert.deepEqual( Object.assign({ audio: false, video: false }, await getMediaCaptureState()), { audio, video }, `expected ${video ? "camera " : ""} ${audio ? "microphone " : ""}shared` ); await checkSharingUI({ audio, video }); } async function navigate(browser, url) { await disableObserverVerification(); let loaded = BrowserTestUtils.browserLoaded(browser, false, url); await SpecialPowers.spawn( browser, [url], u => (content.document.location = u) ); await loaded; await enableObserverVerification(); } var gTests = [ { desc: "getUserMedia camera+mic survives track.stop but not past grace", run: async function checkAudioVideoGracePastStop() { await prompt(true, true); await allow(true, true); info( "After closing all streams, gUM(camera+mic) returns a stream " + "without prompting within grace period." ); await closeStream(); await checkNotSharingWithinGracePeriod(); await noPrompt(true, true); info( "After closing all streams, gUM(mic) returns a stream " + "without prompting within grace period." ); await closeStream(); await checkNotSharingWithinGracePeriod(); await noPrompt(true, false); info( "After closing all streams, gUM(camera) returns a stream " + "without prompting within grace period." ); await closeStream(); await checkNotSharingWithinGracePeriod(); await noPrompt(false, true); info("gUM(screen) still causes a prompt."); let observerPromise = expectObserverCalled("getUserMedia:request"); let promise = promisePopupNotificationShown("webRTC-shareDevices"); await promiseRequestDevice(false, true, null, "screen"); await promise; await observerPromise; is( PopupNotifications.getNotification("webRTC-shareDevices").anchorID, "webRTC-shareScreen-notification-icon", "anchored to device icon" ); checkDeviceSelectors(["screen"]); observerPromise = expectObserverCalled("getUserMedia:response:deny"); await promiseMessage(permissionError, () => { activateSecondaryAction(kActionDeny); }); await observerPromise; perms.removeFromPrincipal(null, "screen", gBrowser.selectedBrowser); await closeStream(); info("Closed stream. Waiting past grace period."); await checkNotSharingWithinGracePeriod(); await wait(WAIT_PERIOD_MS); await checkNotSharing(); info("After grace period expires, gUM(camera) causes a prompt."); await prompt(false, true); await deny(kActionDeny); perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser); info("After grace period expires, gUM(mic) causes a prompt."); await prompt(true, false); await deny(kActionDeny); perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser); }, }, { desc: "getUserMedia camera+mic survives page reload but not past grace", run: async function checkAudioVideoGracePastReload(browser) { await prompt(true, true); await allow(true, true); await closeStream(); await reloadFromContent(); info( "After page reload, gUM(camera+mic) returns a stream " + "without prompting within grace period." ); await checkNotSharingWithinGracePeriod(); await noPrompt(true, true); await closeStream(); await reloadAsUser(); info( "After user page reload, gUM(camera+mic) returns a stream " + "without prompting within grace period." ); await checkNotSharingWithinGracePeriod(); await noPrompt(true, true); info("gUM(screen) still causes a prompt."); let observerPromise = expectObserverCalled("getUserMedia:request"); let promise = promisePopupNotificationShown("webRTC-shareDevices"); await promiseRequestDevice(false, true, null, "screen"); await promise; await observerPromise; is( PopupNotifications.getNotification("webRTC-shareDevices").anchorID, "webRTC-shareScreen-notification-icon", "anchored to device icon" ); checkDeviceSelectors(["screen"]); observerPromise = expectObserverCalled("getUserMedia:response:deny"); await promiseMessage(permissionError, () => { activateSecondaryAction(kActionDeny); }); await observerPromise; perms.removeFromPrincipal(null, "screen", gBrowser.selectedBrowser); await closeStream(); info("Closed stream. Waiting past grace period."); await checkNotSharingWithinGracePeriod(); await wait(WAIT_PERIOD_MS); await checkNotSharing(); info("After grace period expires, gUM(camera) causes a prompt."); await prompt(false, true); await deny(kActionDeny); perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser); info("After grace period expires, gUM(mic) causes a prompt."); await prompt(true, false); await deny(kActionDeny); perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser); }, }, { desc: "getUserMedia camera+mic grace period does not carry over to new tab", run: async function checkAudioVideoGraceEndsNewTab() { await prompt(true, true); await allow(true, true); info("Open same page in a new tab"); await disableObserverVerification(); await BrowserTestUtils.withNewTab(SAME_ORIGIN + PATH, async browser => { info("In new tab, gUM(camera+mic) causes a prompt."); await prompt(true, true); }); info("Closed tab"); await enableObserverVerification(); await closeStream(); info("Closed stream. Waiting past grace period."); await checkNotSharingWithinGracePeriod(); await wait(WAIT_PERIOD_MS); await checkNotSharing(); info("After grace period expires, gUM(camera+mic) causes a prompt."); await prompt(true, true); await deny(kActionDeny); perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser); perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser); }, }, { desc: "getUserMedia camera+mic survives navigation but not past grace", run: async function checkAudioVideoGracePastNavigation(browser) { // Use longer grace period in this test to accommodate navigation const LONG_GRACE_PERIOD_MS = 9000; const LONG_WAIT_PERIOD_MS = LONG_GRACE_PERIOD_MS + 500; await SpecialPowers.pushPrefEnv({ set: [ ["privacy.webrtc.deviceGracePeriodTimeoutMs", LONG_GRACE_PERIOD_MS], ], }); await prompt(true, true); await allow(true, true); await closeStream(); info("Navigate to a second same-origin page"); await navigate(browser, SAME_ORIGIN + PATH2); info( "After navigating to second same-origin page, gUM(camera+mic) " + "returns a stream without prompting within grace period." ); await checkNotSharingWithinGracePeriod(); await noPrompt(true, true); await closeStream(); info("Closed stream. Waiting past grace period."); await checkNotSharingWithinGracePeriod(); await wait(LONG_WAIT_PERIOD_MS); await checkNotSharing(); info("After grace period expires, gUM(camera+mic) causes a prompt."); await prompt(true, true); await allow(true, true); info("Navigate to a different-origin page"); await navigate(browser, CROSS_ORIGIN + PATH2); info( "After navigating to a different-origin page, gUM(camera+mic) " + "causes a prompt." ); await prompt(true, true); await deny(kActionDeny); perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser); perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser); info("Navigate back to the first page"); await navigate(browser, SAME_ORIGIN + PATH); info( "After navigating back to the first page, gUM(camera+mic) " + "returns a stream without prompting within grace period." ); await checkNotSharingWithinGracePeriod(); await noPrompt(true, true); await closeStream(); info("Closed stream. Waiting past grace period."); await checkNotSharingWithinGracePeriod(); await wait(LONG_WAIT_PERIOD_MS); await checkNotSharing(); info("After grace period expires, gUM(camera+mic) causes a prompt."); await prompt(true, true); await deny(kActionDeny); perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser); perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser); }, }, { desc: "getUserMedia camera+mic grace period cleared on permission block", run: async function checkAudioVideoGraceEndsNewTab(browser) { await SpecialPowers.pushPrefEnv({ set: [["privacy.webrtc.deviceGracePeriodTimeoutMs", 10000]], }); info("Set up longer camera grace period."); await prompt(false, true); await allow(false, true); await closeStream(); let principal = gBrowser.selectedBrowser.contentPrincipal; info("Request both to get prompted so we can block both."); await prompt(true, true); // We need to remember this decision to set a block permission here and not just 'Not now' the request, see Bug:1609578 await deny(kActionNever); // Clear the block so we can prompt again. perms.removeFromPrincipal(principal, "camera", gBrowser.selectedBrowser); perms.removeFromPrincipal( principal, "microphone", gBrowser.selectedBrowser ); info("Revoking permission clears camera grace period."); await prompt(false, true); await deny(kActionDeny); perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser); info("Set up longer microphone grace period."); await prompt(true, false); await allow(true, false); await closeStream(); info("Request both to get prompted so we can block both."); await prompt(true, true); // We need to remember this decision to be able to set a block permission here await deny(kActionNever); perms.removeFromPrincipal(principal, "camera", gBrowser.selectedBrowser); perms.removeFromPrincipal( principal, "microphone", gBrowser.selectedBrowser ); info("Revoking permission clears microphone grace period."); await prompt(true, false); // We need to remember this decision to be able to set a block permission here await deny(kActionNever); perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser); }, }, ]; add_task(async function test() { await SpecialPowers.pushPrefEnv({ set: [["privacy.webrtc.deviceGracePeriodTimeoutMs", GRACE_PERIOD_MS]], }); await runTests(gTests); });