Bug 1310355 Improve resiliency of webrtc add-on hooks r?jib draft
authorAndrew Swan <aswan@mozilla.com>
Fri, 14 Oct 2016 16:04:13 -0700
changeset 427729 f128fed6499dfeb17c92911b72a42a7a663564b8
parent 425306 cb2dd5a34dd7b374500fedd72fe19df13c9a7a4d
child 534543 5d6fc6d776e49266c5ef9e0668dc33e95c1658ad
push id33100
push useraswan@mozilla.com
push dateThu, 20 Oct 2016 20:29:05 +0000
reviewersjib
bugs1310355
milestone52.0a1
Bug 1310355 Improve resiliency of webrtc add-on hooks r?jib MozReview-Commit-ID: 29DN2cmXTtk
browser/modules/webrtcUI.jsm
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -10,20 +10,25 @@ const Cu = Components.utils;
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
+                                  "resource:///modules/syncedtabs/EventEmitter.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
                                   "resource://gre/modules/PluralForm.jsm");
 
 this.webrtcUI = {
+  peerConnectionBlockers: new Set(),
+  emitter: new EventEmitter(),
+
   init: function () {
     Services.obs.addObserver(maybeAddMenuIndicator, "browser-delayed-startup-finished", false);
 
     let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
                  .getService(Ci.nsIMessageBroadcaster);
     ppmm.addMessageListener("webrtc:UpdatingIndicators", this);
     ppmm.addMessageListener("webrtc:UpdateGlobalIndicators", this);
     ppmm.addMessageListener("child-process-shutdown", this);
@@ -166,72 +171,142 @@ this.webrtcUI = {
       type = "";
 
     let bundle = document.defaultView.gNavigatorBundle;
     let stringId = "getUserMedia.share" + (type || "SelectedItems") + ".label";
     let popupnotification = aMenuList.parentNode.parentNode;
     popupnotification.setAttribute("buttonlabel", bundle.getString(stringId));
   },
 
+  // Add-ons can override stock permission behavior by doing:
+  //
+  //   webrtcUI.addPeerConnectionBlocker(function(aParams) {
+  //     // new permission checking logic
+  //   }));
+  //
+  // The blocking function receives an object with origin, callID, and windowID
+  // parameters.  If it returns the string "allow" or "deny" or a Promise
+  // that resolves to one of those values, the connection is allowed
+  // or denied.  Any other return value is ignored and control is passed
+  // to other registered blockers, or eventually to the default (which is
+  // to allow).
+  //
+  // Add-ons may also use webrtcUI.on/off to listen to events without
+  // blocking anything:
+  //   peer-request is emitted when a new peer connection is being established.
+  //   peer-request-blocked is emitted when a peer connection request is
+  //                        blocked by some blocking connection handler.
+  //   peer-request-cancel is emitted when a peer-request connection request
+  //                       is canceled.  (This would typically be used in
+  //                       conjunction with a blocking handler to cancel
+  //                       a user prompt or other work done by the handler)
+  //   media-permissions is emitted when new getUserMedia() user permissions
+  //                     are established.
+  //
+  // Listening on gUM events and blocking peer connection 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
+  addPeerConnectionBlocker: function(aCallback) {
+    this.peerConnectionBlockers.add(aCallback);
+  },
+
+  removePeerConnectionBlocker: function(aCallback) {
+    this.peerConnectionBlockers.delete(aCallback);
+  },
+
+  on: function(...args) {
+    return this.emitter.on(...args);
+  },
+
+  off: function(...args) {
+    return this.emitter.off(...args);
+  },
+
   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 { callID, windowID } = aMessage.data;
-        // Also available: isSecure, innerWindowID. For contentWindow:
-        //
-        //   let contentWindow = Services.wm.getOuterWindowWithId(windowID);
+        let params = Object.freeze(Object.assign({
+          origin: aMessage.target.contentPrincipal.origin
+        }, aMessage.data));
 
         let mm = aMessage.target.messageManager;
-        mm.sendAsyncMessage("rtcpeer:Allow",
-                            { callID: callID, windowID: windowID });
+        let answer = reply => {
+          if (reply == "rtcpeer:Allow") {
+            this.emitter.emit("peer-request", params);
+          } else if (reply == "rtcpeer:Deny") {
+            this.emitter.emit("peer-request-blocked", params);
+          }
+
+          mm.sendAsyncMessage(reply, {
+            callID: params.callID,
+            windowID: params.windowID,
+          });
+        }
+
+        let blockers = [ ...this.peerConnectionBlockers, () => "allow" ];
+
+        function next() {
+          let blocker = blockers.shift();
+          return Promise.resolve()
+            .then(() => blocker(params))
+            .catch(err => Cu.reportError(`error in PeerConnection blocked: ${err.message}`))
+            .then(result => {
+              if (result == "allow") {
+                answer("rtcpeer:Allow");
+                return undefined;
+              }
+              if (result == "deny") {
+                answer("rtcpeer:Deny");
+                return undefined;
+              }
+              return next();
+            });
+        }
+
+        next().catch(err => {
+          Cu.reportError(`error in PeerConnection blocker: ${err.message}`);
+          answer("rtcpeer:Allow");
+        });
         break;
       }
-      case "rtcpeer:CancelRequest":
-        // No data to release. This code-point exists for add-ons to override.
+      case "rtcpeer:CancelRequest": {
+        let params = Object.freeze(Object.assign({
+          origin: aMessage.target.contentPrincipal.origin
+        }, aMessage.data));
+        this.emitter.emit("peer-request-cancel", params);
         break;
+      }
       case "webrtc:Request":
         prompt(aMessage.target, aMessage.data);
         break;
       case "webrtc:CancelRequest":
         removePrompt(aMessage.target, aMessage.data);
         break;
       case "webrtc:UpdatingIndicators":
         webrtcUI._streams = [];
         break;
       case "webrtc:UpdateGlobalIndicators":
         updateIndicators(aMessage.data, aMessage.target);
         break;
       case "webrtc:UpdateBrowserIndicators":
+        // Beware, this will need to change when https://bugzil.la/1299577 lands
+        let origin = aMessage.target.contentPrincipal.origin;
+        let {camera, microphone} = aMessage.data;
+        let params = Object.freeze({camera, microphone, origin});
+        this.emitter.emit("media-permissions", params);
+
         let id = aMessage.data.windowId;
         let index;
         for (index = 0; index < webrtcUI._streams.length; ++index) {
           if (webrtcUI._streams[index].state.windowId == id)
             break;
         }
         // If there's no documentURI, the update is actually a removal of the
         // stream, triggered by the recording-window-ended notification.