Bug 1479051 - [macOS 10.14] WebRTC sites silently fail if user previously clicked "Don't Allow" for Firefox camera/mic access r=johannh
authorHaik Aftandilian <haftandilian@mozilla.com>
Wed, 10 Oct 2018 08:28:41 +0000
changeset 496156 a79effc68816dc508ca25b07cfb61bcb2282e230
parent 496155 107392a74e8a7a8d39e66a9b176eb5060ff00924
child 496157 c9d9dd203994cd2251869d7b470564281d5d995f
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjohannh
bugs1479051
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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
browser/actors/WebRTCChild.jsm
browser/modules/webrtcUI.jsm
dom/media/MediaManager.cpp
--- a/browser/actors/WebRTCChild.jsm
+++ b/browser/actors/WebRTCChild.jsm
@@ -235,17 +235,23 @@ function prompt(aContentWindow, aWindowI
     videoDevices,
   };
 
   let mm = getMessageManagerForWindow(aContentWindow);
   mm.sendAsyncMessage("webrtc:Request", request);
 }
 
 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;
   let contentWindow = Services.wm.getOuterWindowWithId(aData.windowID);
   if (contentWindow.pendingGetUserMediaRequests)
     forgetGUMRequest(contentWindow, aData.callID);
 }
 
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -18,16 +18,20 @@ ChromeUtils.defineModuleGetter(this, "Pr
                                "resource://gre/modules/PrivateBrowsingUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "SitePermissions",
                                "resource:///modules/SitePermissions.jsm");
 
 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(),
 
   init() {
     Services.obs.addObserver(maybeAddMenuIndicator, "browser-delayed-startup-finished");
     Services.ppmm.addMessageListener("child-process-shutdown", this);
   },
@@ -295,16 +299,77 @@ var webrtcUI = {
 };
 
 function denyRequest(aBrowser, aRequest) {
   aBrowser.messageManager.sendAsyncMessage("webrtc:Deny",
                                            {callID: aRequest.callID,
                                             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 {
     if (!uri) {
       uri = Services.io.newURI(href);
     }
 
     let addonPolicy = WebExtensionPolicy.getByURI(uri);
@@ -512,20 +577,29 @@ function prompt(aBrowser, aRequest) {
           // Remember on which URIs we found persistent permissions so that we
           // can remove them if the user clicks 'Stop Sharing'. There's no
           // other way for the stop sharing code to know the hostnames of frames
           // using devices until bug 1066082 is fixed.
           let browser = this.browser;
           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;
         }
       }
 
       function listDevices(menupopup, devices) {
         while (menupopup.lastChild)
           menupopup.removeChild(menupopup.lastChild);
@@ -712,17 +786,17 @@ function prompt(aBrowser, aRequest) {
       } else {
         listDevices(camMenupopup, videoDevices);
         doc.getElementById("webRTC-shareDevices-notification").removeAttribute("invalidselection");
       }
 
       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;
         if (videoDevices.length) {
           let listId = "webRTC-select" + (sharingScreen ? "Window" : "Camera") + "-menulist";
           let videoDeviceIndex = doc.getElementById(listId).value;
           let allowVideoDevice = videoDeviceIndex != "-1";
           if (allowVideoDevice) {
@@ -779,16 +853,24 @@ function prompt(aBrowser, aRequest) {
 
         if (remember) {
           // Remember on which URIs we set persistent permissions so that we
           // can remove them if the user clicks 'Stop Sharing'.
           aBrowser._devicePermissionURIs = aBrowser._devicePermissionURIs || [];
           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,
                                              devices: allowedDevices});
       };
       return false;
     },
   };
--- a/dom/media/MediaManager.cpp
+++ b/dom/media/MediaManager.cpp
@@ -2255,16 +2255,17 @@ MediaManager::Get() {
 
     nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
     if (obs) {
       obs->AddObserver(sSingleton, "last-pb-context-exited", false);
       obs->AddObserver(sSingleton, "getUserMedia:got-device-permission", false);
       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)
     nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
     if (prefs) {
       prefs->AddObserver("media.navigator.video.default_width", sSingleton, false);
       prefs->AddObserver("media.navigator.video.default_height", sSingleton, false);
       prefs->AddObserver("media.navigator.video.default_fps", sSingleton, false);
@@ -3629,16 +3630,17 @@ MediaManager::Shutdown()
   }
 
   nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
 
   obs->RemoveObserver(this, "last-pb-context-exited");
   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);
   if (prefs) {
     prefs->RemoveObserver("media.navigator.video.default_width", this);
     prefs->RemoveObserver("media.navigator.video.default_height", this);
     prefs->RemoveObserver("media.navigator.video.default_fps", this);
     prefs->RemoveObserver("media.navigator.audio.fake_frequency", this);
@@ -3764,22 +3766,40 @@ MediaManager::SendPendingGUMRequest()
 {
   if (mPendingGUMRequest.Length() > 0) {
     nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
     obs->NotifyObservers(mPendingGUMRequest[0], "getUserMedia:request", nullptr);
     mPendingGUMRequest.RemoveElementAt(0);
   }
 }
 
+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) {
       GetPrefs(branch,NS_ConvertUTF16toUTF8(aData).get());
       LOG(("%s: %dx%d @%dfps", __FUNCTION__,
            mPrefs.mWidth, mPrefs.mHeight, mPrefs.mFPS));
     }
   } else if (!strcmp(aTopic, "last-pb-context-exited")) {
@@ -3855,22 +3875,22 @@ MediaManager::Observe(nsISupports* aSubj
     if (sHasShutdown) {
       return task->Denied(MediaMgrError::Name::AbortError,
                           NS_LITERAL_STRING("In shutdown"));
     }
     // Reuse the same thread to save memory.
     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;
       }
       array->RemoveElement(key);
       SendPendingGUMRequest();
     }
     return NS_OK;