Bug 1479051 - [macOS 10.14] WebRTC sites silently fail if user previously clicked "Don't Allow" for Firefox camera/mic access r=johannh

Check if we have permission from the OS to access the camera and microphone after the user has granted access to a site.

If permission is denied at the OS level, but granted to the site within Firefox, return NotFoundError.

Differential Revision: https://phabricator.services.mozilla.com/D5458

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Haik Aftandilian 2018-10-10 08:28:41 +00:00
parent 5a64fcf173
commit 206f542c37
3 changed files with 116 additions and 8 deletions

View File

@ -240,7 +240,13 @@ function prompt(aContentWindow, aWindowID, aCallID, aConstraints, aDevices, aSec
}
function denyGUMRequest(aData) {
Services.obs.notifyObservers(null, "getUserMedia:response:deny", aData.callID);
let subject;
if (aData.noOSPermission) {
subject = "getUserMedia:response:noOSPermission";
} else {
subject = "getUserMedia:response:deny";
}
Services.obs.notifyObservers(null, subject, aData.callID);
if (!aData.windowID)
return;

View File

@ -23,6 +23,10 @@ XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() {
return Services.strings.createBundle("chrome://branding/locale/brand.properties");
});
XPCOMUtils.defineLazyServiceGetter(this, "OSPermissions",
"@mozilla.org/ospermissionrequest;1",
"nsIOSPermissionRequest");
var webrtcUI = {
peerConnectionBlockers: new Set(),
emitter: new EventEmitter(),
@ -300,6 +304,67 @@ function denyRequest(aBrowser, aRequest) {
windowID: aRequest.windowID});
}
//
// Deny the request because the browser does not have access to the
// camera or microphone due to OS security restrictions. The user may
// have granted camera/microphone access to the site, but not have
// allowed the browser access in OS settings.
//
function denyRequestNoPermission(aBrowser, aRequest) {
aBrowser.messageManager.sendAsyncMessage("webrtc:Deny",
{callID: aRequest.callID,
windowID: aRequest.windowID,
noOSPermission: true});
}
//
// Check if we have permission to access the camera and or microphone at the
// OS level. Triggers a request to access the device if access is needed and
// the permission state has not yet been determined.
//
async function checkOSPermission(camNeeded, micNeeded) {
let camStatus = {}, micStatus = {};
OSPermissions.getMediaCapturePermissionState(camStatus, micStatus);
if (camNeeded) {
let camPermission = camStatus.value;
let camAccessible = await checkAndGetOSPermission(camPermission,
OSPermissions.requestVideoCapturePermission);
if (!camAccessible) {
return false;
}
}
if (micNeeded) {
let micPermission = micStatus.value;
let micAccessible = await checkAndGetOSPermission(micPermission,
OSPermissions.requestAudioCapturePermission);
if (!micAccessible) {
return false;
}
}
return true;
}
//
// Given a device's permission, return true if the device is accessible. If
// the device's permission is not yet determined, request access to the device.
// |requestPermissionFunc| must return a promise that resolves with true
// if the device is accessible and false otherwise.
//
async function checkAndGetOSPermission(devicePermission,
requestPermissionFunc) {
if (devicePermission == OSPermissions.PERMISSION_STATE_DENIED ||
devicePermission == OSPermissions.PERMISSION_STATE_RESTRICTED) {
return false;
}
if (devicePermission == OSPermissions.PERMISSION_STATE_NOTDETERMINED) {
let deviceAllowed = await requestPermissionFunc();
if (!deviceAllowed) {
return false;
}
}
return true;
}
function getHostOrExtensionName(uri, href) {
let host;
try {
@ -517,10 +582,19 @@ function prompt(aBrowser, aRequest) {
browser._devicePermissionURIs = browser._devicePermissionURIs || [];
browser._devicePermissionURIs.push(uri);
let mm = browser.messageManager;
mm.sendAsyncMessage("webrtc:Allow", {callID: aRequest.callID,
windowID: aRequest.windowID,
devices: allowedDevices});
let camNeeded = videoDevices.length > 0;
let micNeeded = audioDevices.length > 0;
checkOSPermission(camNeeded, micNeeded).then((havePermission) => {
if (havePermission) {
let mm = browser.messageManager;
mm.sendAsyncMessage("webrtc:Allow", {callID: aRequest.callID,
windowID: aRequest.windowID,
devices: allowedDevices});
} else {
denyRequestNoPermission(browser, aRequest);
}
});
this.remove();
return true;
}
@ -717,7 +791,7 @@ function prompt(aBrowser, aRequest) {
if (!sharingAudio)
listDevices(micMenupopup, audioDevices);
this.mainAction.callback = function(aState) {
this.mainAction.callback = async function(aState) {
let remember = aState && aState.checkboxChecked;
let allowedDevices = [];
let perms = Services.perms;
@ -784,6 +858,14 @@ function prompt(aBrowser, aRequest) {
aBrowser._devicePermissionURIs.push(uri);
}
let camNeeded = videoDevices.length > 0;
let micNeeded = audioDevices.length > 0;
let havePermission = await checkOSPermission(camNeeded, micNeeded);
if (!havePermission) {
denyRequestNoPermission(notification.browser, aRequest);
return;
}
let mm = notification.browser.messageManager;
mm.sendAsyncMessage("webrtc:Allow", {callID: aRequest.callID,
windowID: aRequest.windowID,

View File

@ -2260,6 +2260,7 @@ MediaManager::Get() {
obs->AddObserver(sSingleton, "getUserMedia:privileged:allow", false);
obs->AddObserver(sSingleton, "getUserMedia:response:allow", false);
obs->AddObserver(sSingleton, "getUserMedia:response:deny", false);
obs->AddObserver(sSingleton, "getUserMedia:response:noOSPermission", false);
obs->AddObserver(sSingleton, "getUserMedia:revoke", false);
}
// else MediaManager won't work properly and will leak (see bug 837874)
@ -3634,6 +3635,7 @@ MediaManager::Shutdown()
obs->RemoveObserver(this, "getUserMedia:privileged:allow");
obs->RemoveObserver(this, "getUserMedia:response:allow");
obs->RemoveObserver(this, "getUserMedia:response:deny");
obs->RemoveObserver(this, "getUserMedia:response:noOSPermission");
obs->RemoveObserver(this, "getUserMedia:revoke");
nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
@ -3769,12 +3771,30 @@ MediaManager::SendPendingGUMRequest()
}
}
bool
IsGUMResponseNoAccess(const char* aTopic, MediaMgrError::Name& aErrorName)
{
if (!strcmp(aTopic, "getUserMedia:response:deny")) {
aErrorName = MediaMgrError::Name::NotAllowedError;
return true;
}
if (!strcmp(aTopic, "getUserMedia:response:noOSPermission")) {
aErrorName = MediaMgrError::Name::NotFoundError;
return true;
}
return false;
}
nsresult
MediaManager::Observe(nsISupports* aSubject, const char* aTopic,
const char16_t* aData)
{
MOZ_ASSERT(NS_IsMainThread());
MediaMgrError::Name gumNoAccessError = MediaMgrError::Name::NotAllowedError;
if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
nsCOMPtr<nsIPrefBranch> branch( do_QueryInterface(aSubject) );
if (branch) {
@ -3860,12 +3880,12 @@ MediaManager::Observe(nsISupports* aSubject, const char* aTopic,
MediaManager::PostTask(task.forget());
return NS_OK;
} else if (!strcmp(aTopic, "getUserMedia:response:deny")) {
} else if (IsGUMResponseNoAccess(aTopic, gumNoAccessError)) {
nsString key(aData);
RefPtr<GetUserMediaTask> task;
mActiveCallbacks.Remove(key, getter_AddRefs(task));
if (task) {
task->Denied(MediaMgrError::Name::NotAllowedError);
task->Denied(gumNoAccessError);
nsTArray<nsString>* array;
if (!mCallIds.Get(task->GetWindowID(), &array)) {
return NS_OK;