Bug 1189060 - let webrtcUI.jsm etc. block initial Offer/Answer exchange through hook. r=florian,fabrice,mfinkle,mt
authorJan-Ivar Bruaroey <jib@mozilla.com>
Fri, 07 Aug 2015 15:22:30 -0400
changeset 257302 bd7d66caa7930c815db5deee4d8d7878ad68510a
parent 257301 3a580b48d1adca56f74b2a7491b468af3e70bee8
child 257303 16248816135384e23b201125f4ebcd6600d49cf9
push id63586
push userrjesup@wgate.com
push dateTue, 11 Aug 2015 19:47:06 +0000
treeherdermozilla-inbound@162488161353 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersflorian, fabrice, mfinkle, mt
bugs1189060
milestone43.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 1189060 - let webrtcUI.jsm etc. block initial Offer/Answer exchange through hook. r=florian,fabrice,mfinkle,mt
browser/base/content/content.js
browser/modules/ContentWebRTC.jsm
browser/modules/webrtcUI.jsm
dom/media/PeerConnection.js
mobile/android/chrome/content/WebrtcUI.js
mobile/android/chrome/content/browser.js
toolkit/modules/AppConstants.jsm
webapprt/WebRTCHandler.jsm
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -520,16 +520,18 @@ addEventListener("DOMWebNotificationClic
   sendAsyncMessage("DOMWebNotificationClicked", {});
 }, false);
 
 addEventListener("DOMServiceWorkerFocusClient", function(event) {
   sendAsyncMessage("DOMServiceWorkerFocusClient", {});
 }, false);
 
 ContentWebRTC.init();
+addMessageListener("rtcpeer:Allow", ContentWebRTC);
+addMessageListener("rtcpeer:Deny", ContentWebRTC);
 addMessageListener("webrtc:Allow", ContentWebRTC);
 addMessageListener("webrtc:Deny", ContentWebRTC);
 addMessageListener("webrtc:StopSharing", ContentWebRTC);
 addMessageListener("webrtc:StartBrowserSharing", () => {
   let windowID = content.QueryInterface(Ci.nsIInterfaceRequestor)
                         .getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
   sendAsyncMessage("webrtc:response:StartBrowserSharing", {
     windowID: windowID
--- a/browser/modules/ContentWebRTC.jsm
+++ b/browser/modules/ContentWebRTC.jsm
@@ -17,69 +17,108 @@ XPCOMUtils.defineLazyServiceGetter(this,
 this.ContentWebRTC = {
   _initialized: false,
 
   init: function() {
     if (this._initialized)
       return;
 
     this._initialized = true;
-    Services.obs.addObserver(handleRequest, "getUserMedia:request", false);
+    Services.obs.addObserver(handleGUMRequest, "getUserMedia:request", false);
+    Services.obs.addObserver(handlePCRequest, "PeerConnection:request", false);
     Services.obs.addObserver(updateIndicators, "recording-device-events", false);
     Services.obs.addObserver(removeBrowserSpecificIndicator, "recording-window-ended", false);
   },
 
   // Called only for 'unload' to remove pending gUM prompts in reloaded frames.
   handleEvent: function(aEvent) {
     let contentWindow = aEvent.target.defaultView;
     let mm = getMessageManagerForWindow(contentWindow);
-    for (let key of contentWindow.pendingGetUserMediaRequests.keys())
+    for (let key of contentWindow.pendingGetUserMediaRequests.keys()) {
       mm.sendAsyncMessage("webrtc:CancelRequest", key);
+    }
+    for (let key of contentWindow.pendingPeerConnectionRequests.keys()) {
+      mm.sendAsyncMessage("rtcpeer:CancelRequest", key);
+    }
   },
 
   receiveMessage: function(aMessage) {
     switch (aMessage.name) {
-      case "webrtc:Allow":
+      case "rtcpeer:Allow":
+      case "rtcpeer:Deny": {
+        let callID = aMessage.data.callID;
+        let contentWindow = Services.wm.getOuterWindowWithId(aMessage.data.windowID);
+        forgetPCRequest(contentWindow, callID);
+        let topic = (aMessage.name == "rtcpeer:Allow") ? "PeerConnection:response:allow" :
+                                                         "PeerConnection:response:deny";
+        Services.obs.notifyObservers(null, topic, callID);
+        break;
+      }
+      case "webrtc:Allow": {
         let callID = aMessage.data.callID;
         let contentWindow = Services.wm.getOuterWindowWithId(aMessage.data.windowID);
         let devices = contentWindow.pendingGetUserMediaRequests.get(callID);
-        forgetRequest(contentWindow, callID);
+        forgetGUMRequest(contentWindow, callID);
 
         let allowedDevices = Cc["@mozilla.org/supports-array;1"]
                                .createInstance(Ci.nsISupportsArray);
         for (let deviceIndex of aMessage.data.devices)
            allowedDevices.AppendElement(devices[deviceIndex]);
 
         Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", callID);
         break;
+      }
       case "webrtc:Deny":
-        denyRequest(aMessage.data);
+        denyGUMRequest(aMessage.data);
         break;
       case "webrtc:StopSharing":
         Services.obs.notifyObservers(null, "getUserMedia:revoke", aMessage.data);
         break;
     }
   }
 };
 
-function handleRequest(aSubject, aTopic, aData) {
+function handlePCRequest(aSubject, aTopic, aData) {
+  // Need to access JS object behind XPCOM wrapper added by observer API (using a
+  // WebIDL interface didn't work here as object comes from JSImplemented code).
+  aSubject = aSubject.wrappedJSObject;
+  let { windowID, callID, isSecure } = aSubject;
+  let contentWindow = Services.wm.getOuterWindowWithId(windowID);
+
+  if (!contentWindow.pendingPeerConnectionRequests) {
+    setupPendingListsInitially(contentWindow);
+  }
+  contentWindow.pendingPeerConnectionRequests.add(callID);
+
+  let request = {
+    callID: callID,
+    windowID: windowID,
+    documentURI: contentWindow.document.documentURI,
+    secure: isSecure,
+  };
+
+  let mm = getMessageManagerForWindow(contentWindow);
+  mm.sendAsyncMessage("rtcpeer:Request", request);
+}
+
+function handleGUMRequest(aSubject, aTopic, aData) {
   let constraints = aSubject.getConstraints();
   let secure = aSubject.isSecure;
   let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID);
 
   contentWindow.navigator.mozGetUserMediaDevices(
     constraints,
     function (devices) {
       prompt(contentWindow, aSubject.windowID, aSubject.callID,
              constraints, devices, secure);
     },
     function (error) {
       // bug 827146 -- In the future, the UI should catch NotFoundError
       // and allow the user to plug in a device, instead of immediately failing.
-      denyRequest({callID: aSubject.callID}, error);
+      denyGUMRequest({callID: aSubject.callID}, error);
     },
     aSubject.innerWindowID);
 }
 
 function prompt(aContentWindow, aWindowID, aCallID, aConstraints, aDevices, aSecure) {
   let audioDevices = [];
   let videoDevices = [];
   let devices = [];
@@ -118,23 +157,22 @@ function prompt(aContentWindow, aWindowI
 
   let requestTypes = [];
   if (videoDevices.length)
     requestTypes.push(sharingScreen ? "Screen" : "Camera");
   if (audioDevices.length)
     requestTypes.push(sharingAudio ? "AudioCapture" : "Microphone");
 
   if (!requestTypes.length) {
-    denyRequest({callID: aCallID}, "NotFoundError");
+    denyGUMRequest({callID: aCallID}, "NotFoundError");
     return;
   }
 
   if (!aContentWindow.pendingGetUserMediaRequests) {
-    aContentWindow.pendingGetUserMediaRequests = new Map();
-    aContentWindow.addEventListener("unload", ContentWebRTC);
+    setupPendingListsInitially(aContentWindow);
   }
   aContentWindow.pendingGetUserMediaRequests.set(aCallID, devices);
 
   let request = {
     callID: aCallID,
     windowID: aWindowID,
     documentURI: aContentWindow.document.documentURI,
     secure: aSecure,
@@ -144,38 +182,58 @@ function prompt(aContentWindow, aWindowI
     audioDevices: audioDevices,
     videoDevices: videoDevices
   };
 
   let mm = getMessageManagerForWindow(aContentWindow);
   mm.sendAsyncMessage("webrtc:Request", request);
 }
 
-function denyRequest(aData, aError) {
+function denyGUMRequest(aData, aError) {
   let msg = null;
   if (aError) {
     msg = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
     msg.data = aError;
   }
   Services.obs.notifyObservers(msg, "getUserMedia:response:deny", aData.callID);
 
   if (!aData.windowID)
     return;
   let contentWindow = Services.wm.getOuterWindowWithId(aData.windowID);
   if (contentWindow.pendingGetUserMediaRequests)
-    forgetRequest(contentWindow, aData.callID);
+    forgetGUMRequest(contentWindow, aData.callID);
+}
+
+function forgetGUMRequest(aContentWindow, aCallID) {
+  aContentWindow.pendingGetUserMediaRequests.delete(aCallID);
+  forgetPendingListsEventually(aContentWindow);
+}
+
+function forgetPCRequest(aContentWindow, aCallID) {
+  aContentWindow.pendingPeerConnectionRequests.delete(aCallID);
+  forgetPendingListsEventually(aContentWindow);
 }
 
-function forgetRequest(aContentWindow, aCallID) {
-  aContentWindow.pendingGetUserMediaRequests.delete(aCallID);
-  if (aContentWindow.pendingGetUserMediaRequests.size)
+function setupPendingListsInitially(aContentWindow) {
+  if (aContentWindow.pendingGetUserMediaRequests) {
     return;
+  }
+  aContentWindow.pendingGetUserMediaRequests = new Map();
+  aContentWindow.pendingPeerConnectionRequests = new Set();
+  aContentWindow.addEventListener("unload", ContentWebRTC);
+}
 
+function forgetPendingListsEventually(aContentWindow) {
+  if (aContentWindow.pendingGetUserMediaRequests.size ||
+      aContentWindow.pendingPeerConnectionRequests.size) {
+    return;
+  }
+  aContentWindow.pendingGetUserMediaRequests = null;
+  aContentWindow.pendingPeerConnectionRequests = null;
   aContentWindow.removeEventListener("unload", ContentWebRTC);
-  aContentWindow.pendingGetUserMediaRequests = null;
 }
 
 function updateIndicators() {
   let contentWindowSupportsArray = MediaManagerService.activeMediaCaptureWindows;
   let count = contentWindowSupportsArray.Count();
 
   let state = {
     showGlobalIndicator: count > 0,
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -24,31 +24,35 @@ this.webrtcUI = {
 
     let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
                  .getService(Ci.nsIMessageBroadcaster);
     ppmm.addMessageListener("webrtc:UpdatingIndicators", this);
     ppmm.addMessageListener("webrtc:UpdateGlobalIndicators", this);
 
     let mm = Cc["@mozilla.org/globalmessagemanager;1"]
                .getService(Ci.nsIMessageListenerManager);
+    mm.addMessageListener("rtcpeer:Request", this);
+    mm.addMessageListener("rtcpeer:CancelRequest", this);
     mm.addMessageListener("webrtc:Request", this);
     mm.addMessageListener("webrtc:CancelRequest", this);
     mm.addMessageListener("webrtc:UpdateBrowserIndicators", this);
   },
 
   uninit: function () {
     Services.obs.removeObserver(maybeAddMenuIndicator, "browser-delayed-startup-finished");
 
     let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
                  .getService(Ci.nsIMessageBroadcaster);
     ppmm.removeMessageListener("webrtc:UpdatingIndicators", this);
     ppmm.removeMessageListener("webrtc:UpdateGlobalIndicators", this);
 
     let mm = Cc["@mozilla.org/globalmessagemanager;1"]
                .getService(Ci.nsIMessageListenerManager);
+    mm.removeMessageListener("rtcpeer:Request", this);
+    mm.removeMessageListener("rtcpeer:CancelRequest", this);
     mm.removeMessageListener("webrtc:Request", this);
     mm.removeMessageListener("webrtc:CancelRequest", this);
     mm.removeMessageListener("webrtc:UpdateBrowserIndicators", this);
   },
 
   showGlobalIndicator: false,
   showCameraIndicator: false,
   showMicrophoneIndicator: false,
@@ -119,16 +123,53 @@ this.webrtcUI = {
     let bundle = document.defaultView.gNavigatorBundle;
     let stringId = "getUserMedia.share" + (type || "SelectedItems") + ".label";
     let popupnotification = aMenuList.parentNode.parentNode;
     popupnotification.setAttribute("buttonlabel", bundle.getString(stringId));
   },
 
   receiveMessage: function(aMessage) {
     switch (aMessage.name) {
+
+      // Add-ons can override stock permission behavior by doing:
+      //
+      //   var stockReceiveMessage = webrtcUI.receiveMessage;
+      //
+      //   webrtcUI.receiveMessage = function(aMessage) {
+      //     switch (aMessage.name) {
+      //      case "rtcpeer:Request": {
+      //        // new code.
+      //        break;
+      //      ...
+      //      default:
+      //        return stockReceiveMessage.call(this, aMessage);
+      //
+      // Intercepting gUM and peerConnection requests should let an add-on
+      // limit PeerConnection activity with automatic rules and/or prompts
+      // in a sensible manner that avoids double-prompting in typical
+      // gUM+PeerConnection scenarios. For example:
+      //
+      //   State                                    Sample Action
+      //   --------------------------------------------------------------
+      //   No IP leaked yet + No gUM granted        Warn user
+      //   No IP leaked yet + gUM granted           Avoid extra dialog
+      //   No IP leaked yet + gUM request pending.  Delay until gUM grant
+      //   IP already leaked                        Too late to warn
+
+      case "rtcpeer:Request": {
+        // Always allow. This code-point exists for add-ons to override.
+        let request = aMessage.data;
+        let mm = aMessage.target.messageManager;
+        mm.sendAsyncMessage("rtcpeer:Allow", { callID: request.callID,
+                                               windowID: request.windowID });
+        break;
+      }
+      case "rtcpeer:CancelRequest":
+        // No data to release. This code-point exists for add-ons to override.
+        break;
       case "webrtc:Request":
         prompt(aMessage.target, aMessage.data);
         break;
       case "webrtc:CancelRequest":
         removePrompt(aMessage.target, aMessage.data);
         break;
       case "webrtc:UpdatingIndicators":
         webrtcUI._streams = [];
@@ -436,17 +477,17 @@ function prompt(aBrowser, aRequest) {
           }
         }
 
         if (!allowedDevices.length) {
           denyRequest(notification.browser, aRequest);
           return;
         }
 
-        let mm = notification.browser.messageManager
+        let mm = notification.browser.messageManager;
         mm.sendAsyncMessage("webrtc:Allow", {callID: aRequest.callID,
                                              windowID: aRequest.windowID,
                                              devices: allowedDevices});
       };
       return false;
     }
   };
 
--- a/dom/media/PeerConnection.js
+++ b/dom/media/PeerConnection.js
@@ -8,16 +8,18 @@
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PeerConnectionIdp",
   "resource://gre/modules/media/PeerConnectionIdp.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "convertToRTCStatsReport",
   "resource://gre/modules/media/RTCStatsReport.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+  "resource://gre/modules/AppConstants.jsm");
 
 const PC_CONTRACT = "@mozilla.org/dom/peerconnection;1";
 const PC_OBS_CONTRACT = "@mozilla.org/dom/peerconnectionobserver;1";
 const PC_ICE_CONTRACT = "@mozilla.org/dom/rtcicecandidate;1";
 const PC_SESSION_CONTRACT = "@mozilla.org/dom/rtcsessiondescription;1";
 const PC_MANAGER_CONTRACT = "@mozilla.org/dom/peerconnectionmanager;1";
 const PC_STATS_CONTRACT = "@mozilla.org/dom/rtcstatsreport;1";
 const PC_STATIC_CONTRACT = "@mozilla.org/dom/peerconnectionstatic;1";
@@ -35,21 +37,24 @@ const PC_SENDER_CID = Components.ID("{4f
 const PC_RECEIVER_CID = Components.ID("{d974b814-8fde-411c-8c45-b86791b81030}");
 
 // Global list of PeerConnection objects, so they can be cleaned up when
 // a page is torn down. (Maps inner window ID to an array of PC objects).
 function GlobalPCList() {
   this._list = {};
   this._networkdown = false; // XXX Need to query current state somehow
   this._lifecycleobservers = {};
+  this._nextId = 1;
   Services.obs.addObserver(this, "inner-window-destroyed", true);
   Services.obs.addObserver(this, "profile-change-net-teardown", true);
   Services.obs.addObserver(this, "network:offline-about-to-go-offline", true);
   Services.obs.addObserver(this, "network:offline-status-changed", true);
   Services.obs.addObserver(this, "gmp-plugin-crash", true);
+  Services.obs.addObserver(this, "PeerConnection:response:allow", true);
+  Services.obs.addObserver(this, "PeerConnection:response:deny", true);
   if (Cc["@mozilla.org/childprocessmessagemanager;1"]) {
     let mm = Cc["@mozilla.org/childprocessmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
     mm.addMessageListener("gmp-plugin-crash", this);
   }
 }
 GlobalPCList.prototype = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsIMessageListener,
@@ -73,19 +78,33 @@ GlobalPCList.prototype = {
 
   addPC: function(pc) {
     let winID = pc._winID;
     if (this._list[winID]) {
       this._list[winID].push(Cu.getWeakReference(pc));
     } else {
       this._list[winID] = [Cu.getWeakReference(pc)];
     }
+    pc._globalPCListId = this._nextId++;
     this.removeNullRefs(winID);
   },
 
+  findPC: function(globalPCListId) {
+    for (let winId in this._list) {
+      if (this._list.hasOwnProperty(winId)) {
+        for (let pcref of this._list[winId]) {
+          let pc = pcref.get();
+          if (pc && pc._globalPCListId == globalPCListId) {
+            return pc;
+          }
+        }
+      }
+    }
+  },
+
   removeNullRefs: function(winID) {
     if (this._list[winID] === undefined) {
       return;
     }
     this._list[winID] = this._list[winID].filter(
       function (e,i,a) { return e.get() !== null; });
 
     if (this._list[winID].length === 0) {
@@ -182,16 +201,28 @@ GlobalPCList.prototype = {
       }
     } else if (topic == "gmp-plugin-crash") {
       if (subject instanceof Ci.nsIWritablePropertyBag2) {
         let pluginID = subject.getPropertyAsUint32("pluginID");
         let pluginName = subject.getPropertyAsAString("pluginName");
         let data = { pluginID, pluginName };
         this.handleGMPCrash(data);
       }
+    } else if (topic == "PeerConnection:response:allow" ||
+               topic == "PeerConnection:response:deny") {
+      var pc = this.findPC(data);
+      if (pc) {
+        if (topic == "PeerConnection:response:allow") {
+          pc._settlePermission.allow();
+        } else {
+          let err = new pc._win.DOMException("The operation is insecure.",
+                                             "SecurityError");
+          pc._settlePermission.deny(err);
+        }
+      }
     }
   },
 
   _registerPeerConnectionLifecycleCallback: function(winID, cb) {
     this._lifecycleobservers[winID] = cb;
   },
 };
 let _globalPCList = new GlobalPCList();
@@ -350,16 +381,17 @@ RTCPeerConnection.prototype = {
     } else {
       // This gets executed in the typical case when iceServers
       // are passed in through the web page.
       this._mustValidateRTCConfiguration(rtcConfig,
         "RTCPeerConnection constructor passed invalid RTCConfiguration");
     }
     // Save the appId
     this._appId = Cu.getWebIDLCallerPrincipal().appId;
+    this._https = this._win.document.documentURIObject.schemeIs("https");
 
     // Get the offline status for this appId
     let appOffline = false;
     if (this._appId != Ci.nsIScriptSecurityManager.NO_APP_ID &&
         this._appId != Ci.nsIScriptSecurityManager.UNKNOWN_APP_ID) {
       let ios = Cc['@mozilla.org/network/io-service;1'].getService(Ci.nsIIOService);
       appOffline = ios.isAppOffline(this._appId);
     }
@@ -685,23 +717,22 @@ RTCPeerConnection.prototype = {
         this.logWarning(
           "Mandatory/optional in createOffer options is deprecated! Use " +
             JSON.stringify(options) + " instead (note the case difference)!",
           null, 0);
       }
 
       let origin = Cu.getWebIDLCallerPrincipal().origin;
       return this._chain(() => {
-        let p = this._certificateReady.then(
-          () => new this._win.Promise((resolve, reject) => {
+        let p = Promise.all([this.getPermission(), this._certificateReady])
+          .then(() => new this._win.Promise((resolve, reject) => {
             this._onCreateOfferSuccess = resolve;
             this._onCreateOfferFailure = reject;
             this._impl.createOffer(options);
-          })
-        );
+          }));
         p = this._addIdentityAssertion(p, origin);
         return p.then(
           sdp => new this._win.mozRTCSessionDescription({ type: "offer", sdp: sdp }));
       });
     });
   },
 
   createAnswer: function(optionsOrOnSuccess, onError) {
@@ -710,42 +741,62 @@ RTCPeerConnection.prototype = {
     if (typeof optionsOrOnSuccess == "function") {
       onSuccess = optionsOrOnSuccess;
     } else {
       options = optionsOrOnSuccess;
     }
     return this._legacyCatch(onSuccess, onError, () => {
       let origin = Cu.getWebIDLCallerPrincipal().origin;
       return this._chain(() => {
-        let p = this._certificateReady.then(
-          () => new this._win.Promise((resolve, reject) => {
+        let p = Promise.all([this.getPermission(), this._certificateReady])
+          .then(() => new this._win.Promise((resolve, reject) => {
             // We give up line-numbers in errors by doing this here, but do all
             // state-checks inside the chain, to support the legacy feature that
             // callers don't have to wait for setRemoteDescription to finish.
             if (!this.remoteDescription) {
               throw new this._win.DOMException("setRemoteDescription not called",
                                                "InvalidStateError");
             }
             if (this.remoteDescription.type != "offer") {
               throw new this._win.DOMException("No outstanding offer",
                                                "InvalidStateError");
             }
             this._onCreateAnswerSuccess = resolve;
             this._onCreateAnswerFailure = reject;
             this._impl.createAnswer();
-          })
-        );
+          }));
         p = this._addIdentityAssertion(p, origin);
         return p.then(sdp => {
           return new this._win.mozRTCSessionDescription({ type: "answer", sdp: sdp });
         });
       });
     });
   },
 
+  getPermission: function() {
+    if (this._havePermission) {
+      return this._havePermission;
+    }
+    if (AppConstants.MOZ_B2G ||
+        Services.prefs.getBoolPref("media.navigator.permission.disabled")) {
+      return this._havePermission = Promise.resolve();
+    }
+    return this._havePermission = new Promise((resolve, reject) => {
+      this._settlePermission = { allow: resolve, deny: reject };
+      let outerId = this._win.QueryInterface(Ci.nsIInterfaceRequestor).
+          getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
+      let request = { windowID: outerId,
+                      innerWindowId: this._winID,
+                      callID: this._globalPCListId,
+                      isSecure: this._https };
+      request.wrappedJSObject = request;
+      Services.obs.notifyObservers(request, "PeerConnection:request", null);
+    });
+  },
+
   setLocalDescription: function(desc, onSuccess, onError) {
     return this._legacyCatch(onSuccess, onError, () => {
       this._localType = desc.type;
 
       let type;
       switch (desc.type) {
         case "offer":
           type = Ci.IPeerConnection.kActionOffer;
@@ -766,21 +817,22 @@ RTCPeerConnection.prototype = {
       }
 
       if (desc.type !== "rollback" && !desc.sdp) {
         throw new this._win.DOMException(
             "Empty or null SDP provided to setLocalDescription",
             "InvalidParameterError");
       }
 
-      return this._chain(() => new this._win.Promise((resolve, reject) => {
+      return this._chain(() => this.getPermission()
+          .then(() => new this._win.Promise((resolve, reject) => {
         this._onSetLocalDescriptionSuccess = resolve;
         this._onSetLocalDescriptionFailure = reject;
         this._impl.setLocalDescription(type, desc.sdp);
-      }));
+      })));
     });
   },
 
   _validateIdentity: function(sdp, origin) {
     let expectedIdentity;
 
     // Only run a single identity verification at a time.  We have to do this to
     // avoid problems with the fact that identity validation doesn't block the
@@ -853,21 +905,22 @@ RTCPeerConnection.prototype = {
             "Empty or null SDP provided to setRemoteDescription",
             "InvalidParameterError");
       }
 
       // Get caller's origin before hitting the promise chain
       let origin = Cu.getWebIDLCallerPrincipal().origin;
 
       return this._chain(() => {
-        let setRem = new this._win.Promise((resolve, reject) => {
-          this._onSetRemoteDescriptionSuccess = resolve;
-          this._onSetRemoteDescriptionFailure = reject;
-          this._impl.setRemoteDescription(type, desc.sdp);
-        });
+        let setRem = this.getPermission()
+          .then(() => new this._win.Promise((resolve, reject) => {
+            this._onSetRemoteDescriptionSuccess = resolve;
+            this._onSetRemoteDescriptionFailure = reject;
+            this._impl.setRemoteDescription(type, desc.sdp);
+          }));
 
         if (desc.type === "rollback") {
           return setRem;
         }
 
         // Do setRemoteDescription and identity validation in parallel
         let validId = this._validateIdentity(desc.sdp, origin);
         return this._win.Promise.all([setRem, validId])
--- a/mobile/android/chrome/content/WebrtcUI.js
+++ b/mobile/android/chrome/content/WebrtcUI.js
@@ -1,21 +1,40 @@
 /* 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";
 
+this.EXPORTED_SYMBOLS = ["WebrtcUI"];
+
 XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
 
 var WebrtcUI = {
   _notificationId: null,
 
+  // Add-ons can override stock permission behavior by doing:
+  //
+  //   var stockObserve = WebrtcUI.observe;
+  //
+  //   webrtcUI.observe = function(aSubject, aTopic, aData) {
+  //     switch (aTopic) {
+  //      case "PeerConnection:request": {
+  //        // new code.
+  //        break;
+  //      ...
+  //      default:
+  //        return stockObserve.call(this, aSubject, aTopic, aData);
+  //
+  // See browser/modules/webrtcUI.jsm for details.
+
   observe: function(aSubject, aTopic, aData) {
     if (aTopic === "getUserMedia:request") {
-      this.handleRequest(aSubject, aTopic, aData);
+      this.handleGumRequest(aSubject, aTopic, aData);
+    } else if (aTopic === "PeerConnection:request") {
+      this.handlePCRequest(aSubject, aTopic, aData);
     } else if (aTopic === "recording-device-events") {
       switch (aData) {
         case "shutdown":
         case "starting":
           this.notify();
           break;
       }
     }
@@ -67,17 +86,27 @@ var WebrtcUI = {
           Notifications.update(this._notificationId, notificationOptions);
       else
         this._notificationId = Notifications.create(notificationOptions);
       if (count > 1)
         msg.count = count;
     }
   },
 
-  handleRequest: function handleRequest(aSubject, aTopic, aData) {
+  handlePCRequest: function handlePCRequest(aSubject, aTopic, aData) {
+    aSubject = aSubject.wrappedJSObject;
+    let { callID } = aSubject;
+    // Also available: windowID, isSecure, innerWindowID. For contentWindow do:
+    //
+    //   let contentWindow = Services.wm.getOuterWindowWithId(windowID);
+
+    Services.obs.notifyObservers(null, "PeerConnection:response:allow", callID);
+  },
+
+  handleGumRequest: function handleGumRequest(aSubject, aTopic, aData) {
     let constraints = aSubject.getConstraints();
     let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID);
 
     contentWindow.navigator.mozGetUserMediaDevices(
       constraints,
       function (devices) {
         WebrtcUI.prompt(contentWindow, aSubject.callID, constraints.audio,
                         constraints.video, devices);
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -157,17 +157,19 @@ let lazilyLoadedObserverScripts = [
 if (AppConstants.NIGHTLY_BUILD) {
   lazilyLoadedObserverScripts.push(
     ["ActionBarHandler", ["ActionBar:OpenNew", "ActionBar:Close", "TextSelection:Get"],
       "chrome://browser/content/ActionBarHandler.js"]
   );
 }
 if (AppConstants.MOZ_WEBRTC) {
   lazilyLoadedObserverScripts.push(
-    ["WebrtcUI", ["getUserMedia:request", "recording-device-events"], "chrome://browser/content/WebrtcUI.js"])
+    ["WebrtcUI", ["getUserMedia:request",
+                  "PeerConnection:request",
+                  "recording-device-events"], "chrome://browser/content/WebrtcUI.js"])
 }
 
 lazilyLoadedObserverScripts.forEach(function (aScript) {
   let [name, notifications, script] = aScript;
   XPCOMUtils.defineLazyGetter(window, name, function() {
     let sandbox = {};
     Services.scriptloader.loadSubScript(script, sandbox);
     return sandbox[name];
--- a/toolkit/modules/AppConstants.jsm
+++ b/toolkit/modules/AppConstants.jsm
@@ -96,16 +96,24 @@ this.AppConstants = Object.freeze({
 
   MOZ_WEBRTC:
 #ifdef MOZ_WEBRTC
   true,
 #else
   false,
 #endif
 
+# MOZ_B2G covers both device and desktop b2g
+  MOZ_B2G:
+#ifdef MOZ_B2G
+  true,
+#else
+  false,
+#endif
+
 # NOTE! XP_LINUX has to go after MOZ_WIDGET_ANDROID otherwise Android
 # builds will be misidentified as linux.
   platform:
 #ifdef MOZ_WIDGET_GTK
   "linux",
 #elif MOZ_WIDGET_QT
   "linux",
 #elif XP_WIN
--- a/webapprt/WebRTCHandler.jsm
+++ b/webapprt/WebRTCHandler.jsm
@@ -8,17 +8,25 @@ this.EXPORTED_SYMBOLS = [];
 
 let Cc = Components.classes;
 let Ci = Components.interfaces;
 let Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-function handleRequest(aSubject, aTopic, aData) {
+function handlePCRequest(aSubject, aTopic, aData) {
+  aSubject = aSubject.wrappedJSObject;
+  let { windowID, innerWindowID, callID, isSecure } = aSubject;
+  let contentWindow = Services.wm.getOuterWindowWithId(windowID);
+
+  Services.obs.notifyObservers(null, "PeerConnection:response:allow", callID);
+}
+
+function handleGumRequest(aSubject, aTopic, aData) {
   let { windowID, callID } = aSubject;
   let constraints = aSubject.getConstraints();
   let contentWindow = Services.wm.getOuterWindowWithId(windowID);
 
   contentWindow.navigator.mozGetUserMediaDevices(
     constraints,
     function (devices) {
       prompt(contentWindow, callID, constraints.audio,
@@ -95,9 +103,10 @@ function denyRequest(aCallID, aError) {
     msg = Cc["@mozilla.org/supports-string;1"].
           createInstance(Ci.nsISupportsString);
     msg.data = aError;
   }
 
   Services.obs.notifyObservers(msg, "getUserMedia:response:deny", aCallID);
 }
 
-Services.obs.addObserver(handleRequest, "getUserMedia:request", false);
+Services.obs.addObserver(handleGumRequest, "getUserMedia:request", false);
+Services.obs.addObserver(handlePCRequest, "PeerConnection:request", false);