Bug 1189060 - let webrtcUI.jsm etc. block initial Offer/Answer exchange through hook. r=khuey a=sylvestre
authorJan-Ivar Bruaroey <jib@mozilla.com>
Fri, 07 Aug 2015 15:22:30 -0400
changeset 288816 00baa70421f03f8e8bb8be9276abed3207cb5a1c
parent 288815 82eea4a52184431fde5fda491954da05ca24574f
child 288817 1c9bbc9454dbeff074cd3a542e291fa058f7ab06
push id5067
push userraliiev@mozilla.com
push dateMon, 21 Sep 2015 14:04:52 +0000
treeherdermozilla-beta@14221ffe5b2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskhuey, sylvestre
bugs1189060
milestone42.0a2
Bug 1189060 - let webrtcUI.jsm etc. block initial Offer/Answer exchange through hook. r=khuey a=sylvestre
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);