diff --git a/browser/base/content/content.js b/browser/base/content/content.js index b5541e474c72..0e3436de2cf6 100644 --- a/browser/base/content/content.js +++ b/browser/base/content/content.js @@ -525,6 +525,8 @@ addEventListener("DOMServiceWorkerFocusClient", function(event) { }, false); ContentWebRTC.init(); +addMessageListener("rtcpeer:Allow", ContentWebRTC); +addMessageListener("rtcpeer:Deny", ContentWebRTC); addMessageListener("webrtc:Allow", ContentWebRTC); addMessageListener("webrtc:Deny", ContentWebRTC); addMessageListener("webrtc:StopSharing", ContentWebRTC); diff --git a/browser/modules/ContentWebRTC.jsm b/browser/modules/ContentWebRTC.jsm index e059c60d3d2f..f31f1cf65500 100644 --- a/browser/modules/ContentWebRTC.jsm +++ b/browser/modules/ContentWebRTC.jsm @@ -22,7 +22,8 @@ this.ContentWebRTC = { 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); }, @@ -31,17 +32,31 @@ this.ContentWebRTC = { 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); @@ -50,8 +65,9 @@ this.ContentWebRTC = { 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); @@ -60,7 +76,30 @@ this.ContentWebRTC = { } }; -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); @@ -74,7 +113,7 @@ function handleRequest(aSubject, aTopic, aData) { 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); } @@ -123,13 +162,12 @@ function prompt(aContentWindow, aWindowID, aCallID, aConstraints, aDevices, aSec 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); @@ -149,7 +187,7 @@ function prompt(aContentWindow, aWindowID, aCallID, aConstraints, aDevices, aSec 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); @@ -161,16 +199,36 @@ function denyRequest(aData, aError) { return; let contentWindow = Services.wm.getOuterWindowWithId(aData.windowID); if (contentWindow.pendingGetUserMediaRequests) - forgetRequest(contentWindow, aData.callID); + forgetGUMRequest(contentWindow, aData.callID); } -function forgetRequest(aContentWindow, aCallID) { +function forgetGUMRequest(aContentWindow, aCallID) { aContentWindow.pendingGetUserMediaRequests.delete(aCallID); - if (aContentWindow.pendingGetUserMediaRequests.size) - return; + forgetPendingListsEventually(aContentWindow); +} - aContentWindow.removeEventListener("unload", ContentWebRTC); +function forgetPCRequest(aContentWindow, aCallID) { + aContentWindow.pendingPeerConnectionRequests.delete(aCallID); + forgetPendingListsEventually(aContentWindow); +} + +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); } function updateIndicators() { diff --git a/browser/modules/webrtcUI.jsm b/browser/modules/webrtcUI.jsm index 792c58bf1dcd..94988ff0a876 100644 --- a/browser/modules/webrtcUI.jsm +++ b/browser/modules/webrtcUI.jsm @@ -29,6 +29,8 @@ this.webrtcUI = { 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); @@ -44,6 +46,8 @@ this.webrtcUI = { 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); @@ -124,6 +128,43 @@ this.webrtcUI = { 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; @@ -441,7 +482,7 @@ function prompt(aBrowser, aRequest) { return; } - let mm = notification.browser.messageManager + let mm = notification.browser.messageManager; mm.sendAsyncMessage("webrtc:Allow", {callID: aRequest.callID, windowID: aRequest.windowID, devices: allowedDevices}); diff --git a/dom/media/PeerConnection.js b/dom/media/PeerConnection.js index c67365daff04..a1c762847097 100644 --- a/dom/media/PeerConnection.js +++ b/dom/media/PeerConnection.js @@ -13,6 +13,8 @@ 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"; @@ -40,11 +42,14 @@ 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); @@ -78,9 +83,23 @@ GlobalPCList.prototype = { } 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; @@ -187,6 +206,18 @@ GlobalPCList.prototype = { 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); + } + } } }, @@ -355,6 +386,7 @@ RTCPeerConnection.prototype = { } // 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; @@ -690,13 +722,12 @@ RTCPeerConnection.prototype = { 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 })); @@ -715,8 +746,8 @@ RTCPeerConnection.prototype = { 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. @@ -731,8 +762,7 @@ RTCPeerConnection.prototype = { 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 }); @@ -741,6 +771,27 @@ RTCPeerConnection.prototype = { }); }, + 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; @@ -771,11 +822,12 @@ RTCPeerConnection.prototype = { "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); - })); + }))); }); }, @@ -858,11 +910,12 @@ RTCPeerConnection.prototype = { 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; diff --git a/mobile/android/chrome/content/WebrtcUI.js b/mobile/android/chrome/content/WebrtcUI.js index 1efe59b34632..296bbb196011 100644 --- a/mobile/android/chrome/content/WebrtcUI.js +++ b/mobile/android/chrome/content/WebrtcUI.js @@ -3,14 +3,33 @@ * 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": @@ -72,7 +91,17 @@ var WebrtcUI = { } }, - 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); diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index fdc523884570..f81d743214db 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -162,7 +162,9 @@ if (AppConstants.NIGHTLY_BUILD) { } 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) { diff --git a/toolkit/modules/AppConstants.jsm b/toolkit/modules/AppConstants.jsm index c4ece380ac0a..5a278c2d2971 100644 --- a/toolkit/modules/AppConstants.jsm +++ b/toolkit/modules/AppConstants.jsm @@ -101,6 +101,14 @@ this.AppConstants = Object.freeze({ 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: diff --git a/webapprt/WebRTCHandler.jsm b/webapprt/WebRTCHandler.jsm index 2f2ba8b517eb..2ee7f279c637 100644 --- a/webapprt/WebRTCHandler.jsm +++ b/webapprt/WebRTCHandler.jsm @@ -13,7 +13,15 @@ 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); @@ -100,4 +108,5 @@ function denyRequest(aCallID, 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);