deleted file mode 100644
--- a/dom/media/IdpProxy.jsm
+++ /dev/null
@@ -1,272 +0,0 @@
-/* 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 = ["IdpProxy"];
-
-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, "Sandbox",
- "resource://gre/modules/identity/Sandbox.jsm");
-
-/**
- * An invisible iframe for hosting the idp shim.
- *
- * There is no visible UX here, as we assume the user has already
- * logged in elsewhere (on a different screen in the web site hosting
- * the RTC functions).
- */
-function IdpChannel(uri, messageCallback) {
- this.sandbox = null;
- this.messagechannel = null;
- this.source = uri;
- this.messageCallback = messageCallback;
-}
-
-IdpChannel.prototype = {
- /**
- * Create a hidden, sandboxed iframe for hosting the IdP's js shim.
- *
- * @param callback
- * (function) invoked when this completes, with an error
- * argument if there is a problem, no argument if everything is
- * ok
- */
- open: function(callback) {
- if (this.sandbox) {
- return callback(new Error("IdP channel already open"));
- }
-
- let ready = this._sandboxReady.bind(this, callback);
- this.sandbox = new Sandbox(this.source, ready);
- },
-
- _sandboxReady: function(aCallback, aSandbox) {
- // Inject a message channel into the subframe.
- try {
- this.messagechannel = new aSandbox._frame.contentWindow.MessageChannel();
- Object.defineProperty(
- aSandbox._frame.contentWindow.wrappedJSObject,
- "rtcwebIdentityPort",
- {
- value: this.messagechannel.port2,
- configurable: true
- }
- );
- } catch (e) {
- this.close();
- aCallback(e); // oops, the IdP proxy overwrote this.. bad
- return;
- }
- this.messagechannel.port1.onmessage = function(msg) {
- this.messageCallback(msg.data);
- }.bind(this);
- this.messagechannel.port1.start();
- aCallback();
- },
-
- send: function(msg) {
- this.messagechannel.port1.postMessage(msg);
- },
-
- close: function IdpChannel_close() {
- if (this.sandbox) {
- if (this.messagechannel) {
- this.messagechannel.port1.close();
- }
- this.sandbox.free();
- }
- this.messagechannel = null;
- this.sandbox = null;
- }
-};
-
-/**
- * A message channel between the RTC PeerConnection and a designated IdP Proxy.
- *
- * @param domain (string) the domain to load up
- * @param protocol (string) Optional string for the IdP protocol
- */
-function IdpProxy(domain, protocol) {
- IdpProxy.validateDomain(domain);
- IdpProxy.validateProtocol(protocol);
-
- this.domain = domain;
- this.protocol = protocol || "default";
-
- this._reset();
-}
-
-/**
- * Checks that the domain is only a domain, and doesn't contain anything else.
- * Adds it to a URI, then checks that it matches perfectly.
- */
-IdpProxy.validateDomain = function(domain) {
- let message = "Invalid domain for identity provider; ";
- if (!domain || typeof domain !== "string") {
- throw new Error(message + "must be a non-zero length string");
- }
-
- message += "must only have a domain name and optionally a port";
- try {
- let ioService = Components.classes["@mozilla.org/network/io-service;1"]
- .getService(Components.interfaces.nsIIOService);
- let uri = ioService.newURI('https://' + domain + '/', null, null);
-
- // this should trap errors
- // we could check uri.userPass, uri.path and uri.ref, but there is no need
- if (uri.hostPort !== domain) {
- throw new Error(message);
- }
- } catch (e if (e.result === Cr.NS_ERROR_MALFORMED_URI)) {
- throw new Error(message);
- }
-};
-
-/**
- * Checks that the IdP protocol is sane. In particular, we don't want someone
- * adding relative paths (e.g., "../../myuri"), which could be used to move
- * outside of /.well-known/ and into space that they control.
- */
-IdpProxy.validateProtocol = function(protocol) {
- if (!protocol) {
- return; // falsy values turn into "default", so they are OK
- }
- let message = "Invalid protocol for identity provider; ";
- if (typeof protocol !== "string") {
- throw new Error(message + "must be a string");
- }
- if (decodeURIComponent(protocol).match(/[\/\\]/)) {
- throw new Error(message + "must not include '/' or '\\'");
- }
-};
-
-IdpProxy.prototype = {
- _reset: function() {
- this.channel = null;
- this.ready = false;
-
- this.counter = 0;
- this.tracking = {};
- this.pending = [];
- },
-
- isSame: function(domain, protocol) {
- return this.domain === domain && ((protocol || "default") === this.protocol);
- },
-
- /**
- * Get a sandboxed iframe for hosting the idp-proxy's js. Create a message
- * channel down to the frame.
- *
- * @param errorCallback (function) a callback that will be invoked if there
- * is a fatal error starting the proxy
- */
- start: function(errorCallback) {
- if (this.channel) {
- return;
- }
- let well_known = "https://" + this.domain;
- well_known += "/.well-known/idp-proxy/" + this.protocol;
- this.channel = new IdpChannel(well_known, this._messageReceived.bind(this));
- this.channel.open(function(error) {
- if (error) {
- this.close();
- if (typeof errorCallback === "function") {
- errorCallback(error);
- }
- }
- }.bind(this));
- },
-
- /**
- * Send a message up to the idp proxy. This should be an RTC "SIGN" or
- * "VERIFY" message. This method adds the tracking 'id' parameter
- * automatically to the message so that the callback is only invoked for the
- * response to the message.
- *
- * This enqueues the message to send if the IdP hasn't signaled that it is
- * "READY", and sends the message when it is.
- *
- * The caller is responsible for ensuring that a response is received. If the
- * IdP doesn't respond, the callback simply isn't invoked.
- */
- send: function(message, callback) {
- this.start();
- if (this.ready) {
- message.id = "" + (++this.counter);
- this.tracking[message.id] = callback;
- this.channel.send(message);
- } else {
- this.pending.push({ message: message, callback: callback });
- }
- },
-
- /**
- * Handle a message from the IdP. This automatically sends if the message is
- * 'READY' so there is no need to track readiness state outside of this obj.
- */
- _messageReceived: function(message) {
- if (!message) {
- return;
- }
- if (!this.ready && message.type === "READY") {
- this.ready = true;
- this.pending.forEach(function(p) {
- this.send(p.message, p.callback);
- }, this);
- this.pending = [];
- } else if (this.tracking[message.id]) {
- var callback = this.tracking[message.id];
- delete this.tracking[message.id];
- callback(message);
- } else {
- let console = Cc["@mozilla.org/consoleservice;1"].
- getService(Ci.nsIConsoleService);
- console.logStringMessage("Received bad message from IdP: " +
- message.id + ":" + message.type);
- }
- },
-
- /**
- * Performs cleanup. The object should be OK to use again.
- */
- close: function() {
- if (!this.channel) {
- return;
- }
-
- // clear out before letting others know in case they do something bad
- let trackingCopy = this.tracking;
- let pendingCopy = this.pending;
-
- this.channel.close();
- this._reset();
-
- // dump a message of type "ERROR" in response to all outstanding
- // messages to the IdP
- let error = { type: "ERROR", error: "IdP closed" };
- Object.keys(trackingCopy).forEach(function(k) {
- trackingCopy[k](error);
- });
- pendingCopy.forEach(function(p) {
- p.callback(error);
- });
- },
-
- toString: function() {
- return this.domain + '/.../' + this.protocol;
- }
-};
-
-this.IdpProxy = IdpProxy;
new file mode 100644
--- /dev/null
+++ b/dom/media/IdpSandbox.jsm
@@ -0,0 +1,238 @@
+/* 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';
+
+const {
+ classes: Cc,
+ interfaces: Ci,
+ utils: Cu,
+ results: Cr
+} = Components;
+
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+/** This little class ensures that redirects maintain an https:// origin */
+function RedirectHttpsOnly() {}
+
+RedirectHttpsOnly.prototype = {
+ asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) {
+ if (newChannel.URI.scheme !== 'https') {
+ callback.onRedirectVerifyCallback(Cr.NS_ERROR_ABORT);
+ } else {
+ callback.onRedirectVerifyCallback(Cr.NS_OK);
+ }
+ },
+
+ getInterface: function(iid) {
+ return this.QueryInterface(iid);
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannelEventSink])
+};
+
+/** This class loads a resource into a single string. ResourceLoader.load() is
+ * the entry point. */
+function ResourceLoader(res, rej) {
+ this.resolve = res;
+ this.reject = rej;
+ this.data = '';
+}
+
+/** Loads the identified https:// URL. */
+ResourceLoader.load = function(uri) {
+ return new Promise((resolve, reject) => {
+ let listener = new ResourceLoader(resolve, reject);
+ let ioService = Cc['@mozilla.org/network/io-service;1']
+ .getService(Ci.nsIIOService);
+ let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+ // the '2' identifies this as a script load
+ let ioChannel = ioService.newChannelFromURI2(uri, null, systemPrincipal,
+ systemPrincipal, 0, 2);
+ ioChannel.notificationCallbacks = new RedirectHttpsOnly();
+ ioChannel.asyncOpen(listener, null);
+ });
+};
+
+ResourceLoader.prototype = {
+ onDataAvailable: function(request, context, input, offset, count) {
+ let stream = Cc['@mozilla.org/scriptableinputstream;1']
+ .createInstance(Ci.nsIScriptableInputStream);
+ stream.init(input);
+ this.data += stream.read(count);
+ },
+
+ onStartRequest: function (request, context) {},
+
+ onStopRequest: function(request, context, status) {
+ if (Components.isSuccessCode(status)) {
+ var statusCode = request.QueryInterface(Ci.nsIHttpChannel).responseStatus;
+ if (statusCode === 200) {
+ this.resolve({ request: request, data: this.data });
+ } else {
+ this.reject(new Error('Non-200 response from server: ' + statusCode));
+ }
+ } else {
+ this.reject(new Error('Load failed: ' + status));
+ }
+ },
+
+ getInterface: function(iid) {
+ return this.QueryInterface(iid);
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener])
+};
+
+/**
+ * A simple implementation of the WorkerLocation interface.
+ */
+function createLocationFromURI(uri) {
+ return {
+ href: uri.spec,
+ protocol: uri.scheme + ':',
+ host: uri.host + ((uri.port >= 0) ?
+ (':' + uri.port) : ''),
+ port: uri.port,
+ hostname: uri.host,
+ pathname: uri.path.replace(/[#\?].*/, ''),
+ search: uri.path.replace(/^[^\?]*/, '').replace(/#.*/, ''),
+ hash: uri.hasRef ? ('#' + uri.ref) : '',
+ origin: uri.prePath,
+ toString: function() {
+ return uri.spec;
+ }
+ };
+}
+
+/**
+ * A javascript sandbox for running an IdP.
+ *
+ * @param domain (string) the domain of the IdP
+ * @param protocol (string?) the protocol of the IdP [default: 'default']
+ * @throws if the domain or protocol aren't valid
+ */
+function IdpSandbox(domain, protocol) {
+ this.source = IdpSandbox.createIdpUri(domain, protocol || "default");
+ this.active = null;
+ this.sandbox = null;
+}
+
+IdpSandbox.checkDomain = function(domain) {
+ if (!domain || typeof domain !== 'string') {
+ throw new Error('Invalid domain for identity provider: ' +
+ 'must be a non-zero length string');
+ }
+};
+
+/**
+ * Checks that the IdP protocol is superficially sane. In particular, we don't
+ * want someone adding relative paths (e.g., '../../myuri'), which could be used
+ * to move outside of /.well-known/ and into space that they control.
+ */
+IdpSandbox.checkProtocol = function(protocol) {
+ let message = 'Invalid protocol for identity provider: ';
+ if (!protocol || typeof protocol !== 'string') {
+ throw new Error(message + 'must be a non-zero length string');
+ }
+ if (decodeURIComponent(protocol).match(/[\/\\]/)) {
+ throw new Error(message + "must not include '/' or '\\'");
+ }
+};
+
+/**
+ * Turns a domain and protocol into a URI. This does some aggressive checking
+ * to make sure that we aren't being fooled somehow. Throws on fooling.
+ */
+IdpSandbox.createIdpUri = function(domain, protocol) {
+ IdpSandbox.checkDomain(domain);
+ IdpSandbox.checkProtocol(protocol);
+
+ let message = 'Invalid IdP parameters: ';
+ try {
+ let wkIdp = 'https://' + domain + '/.well-known/idp-proxy/' + protocol;
+ let ioService = Components.classes['@mozilla.org/network/io-service;1']
+ .getService(Ci.nsIIOService);
+ let uri = ioService.newURI(wkIdp, null, null);
+
+ if (uri.hostPort !== domain) {
+ throw new Error(message + 'domain is invalid');
+ }
+ if (uri.path.indexOf('/.well-known/idp-proxy/') !== 0) {
+ throw new Error(message + 'must produce a /.well-known/idp-proxy/ URI');
+ }
+
+ return uri;
+ } catch (e if (typeof e.result !== 'undefined' &&
+ e.result === Cr.NS_ERROR_MALFORMED_URI)) {
+ throw new Error(message + 'must produce a valid URI');
+ }
+};
+
+IdpSandbox.prototype = {
+ isSame: function(domain, protocol) {
+ return this.source.spec === IdpSandbox.createIdpUri(domain, protocol).spec;
+ },
+
+ start: function() {
+ if (!this.active) {
+ this.active = ResourceLoader.load(this.source)
+ .then(result => this._createSandbox(result));
+ }
+ return this.active;
+ },
+
+ // Provides the sandbox with some useful facilities. Initially, this is only
+ // a minimal set; it is far easier to add more as the need arises, than to
+ // take them back if we discover a mistake.
+ _populateSandbox: function() {
+ this.sandbox.location = Cu.cloneInto(createLocationFromURI(this.source),
+ this.sandbox,
+ { cloneFunctions: true });
+ },
+
+ _createSandbox: function(result) {
+ let principal = Services.scriptSecurityManager
+ .getChannelResultPrincipal(result.request);
+
+ this.sandbox = Cu.Sandbox(principal, {
+ sandboxName: 'IdP-' + this.source.host,
+ wantComponents: false,
+ wantExportHelpers: false,
+ wantGlobalProperties: [
+ 'indexedDB', 'XMLHttpRequest', 'TextEncoder', 'TextDecoder',
+ 'URL', 'URLSearchParams', 'atob', 'btoa', 'Blob', 'crypto',
+ 'rtcIdentityProvider'
+ ]
+ });
+ this._populateSandbox();
+
+ let registrar = this.sandbox.rtcIdentityProvider;
+ if (!Cu.isXrayWrapper(registrar)) {
+ throw new Error('IdP setup failed');
+ }
+ // putting a javascript version of 1.8 here seems fragile
+ Cu.evalInSandbox(result.data, this.sandbox,
+ '1.8', result.request.URI.spec, 1);
+
+ if (!registrar.idp) {
+ throw new Error('IdP failed to call rtcIdentityProvider.register()');
+ }
+ return registrar;
+ },
+
+ stop: function() {
+ if (this.sandbox) {
+ Cu.nukeSandbox(this.sandbox);
+ }
+ this.sandbox = null;
+ this.active = null;
+ },
+
+ toString: function() {
+ return this.source.spec;
+ }
+};
+
+this.EXPORTED_SYMBOLS = ['IdpSandbox'];
+this.IdpSandbox = IdpSandbox;
--- a/dom/media/PeerConnection.js
+++ b/dom/media/PeerConnection.js
@@ -384,21 +384,23 @@ RTCPeerConnection.prototype = {
"InvalidStateError");
}
return this._pc;
},
_initIdp: function() {
let prefName = "media.peerconnection.identity.timeout";
let idpTimeout = Services.prefs.getIntPref(prefName);
- let warningFunc = this.logWarning.bind(this);
- this._localIdp = new PeerConnectionIdp(this._win, idpTimeout, warningFunc,
- this.dispatchEvent.bind(this));
- this._remoteIdp = new PeerConnectionIdp(this._win, idpTimeout, warningFunc,
- this.dispatchEvent.bind(this));
+ let warn = this.logWarning.bind(this);
+ let idpErrorReport = (type, args) => {
+ this.dispatchEvent(
+ new this._win.RTCPeerConnectionIdentityErrorEvent(type, args));
+ };
+ this._localIdp = new PeerConnectionIdp(idpTimeout, warn, idpErrorReport);
+ this._remoteIdp = new PeerConnectionIdp(idpTimeout, warn, idpErrorReport);
},
// Add a function to the internal operations chain.
_chain: function(func) {
this._checkClosed(); // out here DOMException line-numbers work.
let p = this._operationsChain.then(() => {
// Don't _checkClosed() inside the chain, because it throws, and spec
@@ -701,61 +703,57 @@ RTCPeerConnection.prototype = {
// Do setRemoteDescription and identity validation in parallel
let p = new this._win.Promise((resolve, reject) => {
this._onSetRemoteDescriptionSuccess = resolve;
this._onSetRemoteDescriptionFailure = reject;
this._impl.setRemoteDescription(type, desc.sdp);
});
- let pp = new Promise(resolve =>
- this._remoteIdp.verifyIdentityFromSDP(desc.sdp, origin, resolve))
- .then(msg => {
- // If this pc has an identity already, then identity in sdp must match
- if (expectedIdentity && (!msg || msg.identity !== expectedIdentity)) {
- throw new this._win.DOMException(
- "Peer Identity mismatch, expected: " + expectedIdentity,
- "IncompatibleSessionDescriptionError");
- }
- if (msg) {
- // Set new identity and generate an event.
- this._impl.peerIdentity = msg.identity;
- this._peerIdentity = new this._win.RTCIdentityAssertion(
- this._remoteIdp.provider, msg.identity);
- this.dispatchEvent(new this._win.Event("peeridentity"));
- }
- });
+ let pp = this._remoteIdp.verifyIdentityFromSDP(desc.sdp, origin)
+ .then(msg => {
+ // If this pc has an identity already, then identity in sdp must match
+ if (expectedIdentity && (!msg || msg.identity !== expectedIdentity)) {
+ throw new this._win.DOMException(
+ "Peer Identity mismatch, expected: " + expectedIdentity,
+ "IncompatibleSessionDescriptionError");
+ }
+ if (msg) {
+ // Set new identity and generate an event.
+ this._impl.peerIdentity = msg.identity;
+ this._peerIdentity = new this._win.RTCIdentityAssertion(
+ this._remoteIdp.provider, msg.identity);
+ this.dispatchEvent(new this._win.Event("peeridentity"));
+ }
+ });
// Only wait for Idp validation if we need identity matching.
return expectedIdentity? this._win.Promise.all([p, pp]).then(() => {}) : p;
});
});
},
setIdentityProvider: function(provider, protocol, username) {
this._checkClosed();
this._localIdp.setIdentityProvider(provider, protocol, username);
},
- _gotIdentityAssertion: function(assertion){
+ _gotIdentityAssertion: function(assertion) {
+ if (!assertion) {
+ return;
+ }
let args = { assertion: assertion };
let ev = new this._win.RTCPeerConnectionIdentityEvent("identityresult", args);
this.dispatchEvent(ev);
},
getIdentityAssertion: function() {
this._checkClosed();
- var gotAssertion = assertion => {
- if (assertion) {
- this._gotIdentityAssertion(assertion);
- }
- };
-
- this._localIdp.getIdentityAssertion(this._impl.fingerprint,
- gotAssertion);
+ this._localIdp.getIdentityAssertion(this._impl.fingerprint)
+ .then(assertion => this._gotIdentityAssertion(assertion));
},
updateIce: function(config) {
throw new this._win.DOMException("updateIce not yet implemented",
"NotSupportedError");
},
addIceCandidate: function(c, onSuccess, onError) {
@@ -883,17 +881,17 @@ RTCPeerConnection.prototype = {
get localDescription() {
this._checkClosed();
let sdp = this._impl.localDescription;
if (sdp.length == 0) {
return null;
}
- sdp = this._localIdp.wrapSdp(sdp);
+ sdp = this._localIdp.addIdentityAttribute(sdp);
return new this._win.mozRTCSessionDescription({ type: this._localType,
sdp: sdp });
},
get remoteDescription() {
this._checkClosed();
let sdp = this._impl.remoteDescription;
if (sdp.length == 0) {
@@ -1037,42 +1035,52 @@ PeerConnectionObserver.prototype = {
},
dispatchEvent: function(event) {
this._dompc.dispatchEvent(event);
},
onCreateOfferSuccess: function(sdp) {
let pc = this._dompc;
- let fp = pc._impl.fingerprint;
- let origin = Cu.getWebIDLCallerPrincipal().origin;
- pc._localIdp.appendIdentityToSDP(sdp, fp, origin, function(sdp, assertion) {
- if (assertion) {
- pc._gotIdentityAssertion(assertion);
- }
+ let idp = pc._localIdp;
+
+ if (idp.enabled) {
+ idp.getIdentityAssertion(pc._impl.fingerprint)
+ .then(assertion => {
+ pc._gotIdentityAssertion(assertion);
+ sdp = idp.addIdentityAttribute(sdp);
+ pc._onCreateOfferSuccess(new pc._win.mozRTCSessionDescription({ type: "offer",
+ sdp: sdp }));
+ }, e => {}); // errors are handled in the IdP
+ } else {
pc._onCreateOfferSuccess(new pc._win.mozRTCSessionDescription({ type: "offer",
sdp: sdp }));
- }.bind(this));
+ }
},
onCreateOfferError: function(code, message) {
this._dompc._onCreateOfferFailure(this.newError(message, code));
},
onCreateAnswerSuccess: function(sdp) {
let pc = this._dompc;
- let fp = pc._impl.fingerprint;
- let origin = Cu.getWebIDLCallerPrincipal().origin;
- pc._localIdp.appendIdentityToSDP(sdp, fp, origin, function(sdp, assertion) {
- if (assertion) {
- pc._gotIdentityAssertion(assertion);
- }
+ let idp = pc._localIdp;
+
+ if (idp.enabled) {
+ idp.getIdentityAssertion(pc._impl.fingerprint)
+ .then(assertion => {
+ pc._gotIdentityAssertion(assertion);
+ sdp = idp.addIdentityAttribute(sdp);
+ pc._onCreateAnswerSuccess(new pc._win.mozRTCSessionDescription({ type: "answer",
+ sdp: sdp }));
+ }, e => {});
+ } else {
pc._onCreateAnswerSuccess(new pc._win.mozRTCSessionDescription({ type: "answer",
sdp: sdp }));
- }.bind(this));
+ }
},
onCreateAnswerError: function(code, message) {
this._dompc._onCreateAnswerFailure(this.newError(message, code));
},
onSetLocalDescriptionSuccess: function() {
this._dompc._onSetLocalDescriptionSuccess();
--- a/dom/media/PeerConnectionIdp.jsm
+++ b/dom/media/PeerConnectionIdp.jsm
@@ -1,366 +1,354 @@
/* jshint moz:true, browser:true */
/* 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/. */
-this.EXPORTED_SYMBOLS = ["PeerConnectionIdp"];
+this.EXPORTED_SYMBOLS = ['PeerConnectionIdp'];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "IdpProxy",
- "resource://gre/modules/media/IdpProxy.jsm");
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'IdpSandbox',
+ 'resource://gre/modules/media/IdpSandbox.jsm');
+
+function TimerResolver(resolve) {
+ this.notify = resolve;
+}
+TimerResolver.prototype = {
+ getInterface: function(iid) {
+ return this.QueryInterface(iid);
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback])
+}
+function delay(t) {
+ return new Promise(resolve => {
+ let timer = Cc['@mozilla.org/timer;1'].getService(Ci.nsITimer);
+ timer.initWithCallback(new TimerResolver(resolve), t, 0); // One shot
+ });
+}
/**
* Creates an IdP helper.
*
- * @param window (object) the window object to use for miscellaneous goodies
* @param timeout (int) the timeout in milliseconds
* @param warningFunc (function) somewhere to dump warning messages
- * @param dispatchEventFunc (function) somewhere to dump error events
+ * @param dispatchErrorFunc (function(string, dict)) somewhere to dump errors
*/
-function PeerConnectionIdp(window, timeout, warningFunc, dispatchEventFunc) {
- this._win = window;
+function PeerConnectionIdp(timeout, warningFunc, dispatchErrorFunc) {
this._timeout = timeout || 5000;
this._warning = warningFunc;
- this._dispatchEvent = dispatchEventFunc;
+ this._dispatchError = dispatchErrorFunc;
this.assertion = null;
this.provider = null;
}
(function() {
- PeerConnectionIdp._mLinePattern = new RegExp("^m=", "m");
+ PeerConnectionIdp._mLinePattern = new RegExp('^m=', 'm');
// attributes are funny, the 'a' is case sensitive, the name isn't
- let pattern = "^a=[iI][dD][eE][nN][tT][iI][tT][yY]:(\\S+)";
- PeerConnectionIdp._identityPattern = new RegExp(pattern, "m");
- pattern = "^a=[fF][iI][nN][gG][eE][rR][pP][rR][iI][nN][tT]:(\\S+) (\\S+)";
- PeerConnectionIdp._fingerprintPattern = new RegExp(pattern, "m");
+ let pattern = '^a=[iI][dD][eE][nN][tT][iI][tT][yY]:(\\S+)';
+ PeerConnectionIdp._identityPattern = new RegExp(pattern, 'm');
+ pattern = '^a=[fF][iI][nN][gG][eE][rR][pP][rR][iI][nN][tT]:(\\S+) (\\S+)';
+ PeerConnectionIdp._fingerprintPattern = new RegExp(pattern, 'm');
})();
PeerConnectionIdp.prototype = {
+ get enabled() {
+ return !!this._idp;
+ },
+
setIdentityProvider: function(provider, protocol, username) {
this.provider = provider;
- this.protocol = protocol;
+ this.protocol = protocol || 'default';
this.username = username;
- if (this._idpchannel) {
- if (this._idpchannel.isSame(provider, protocol)) {
- return;
+ if (this._idp) {
+ if (this._idp.isSame(provider, protocol)) {
+ return; // noop
}
- this._idpchannel.close();
+ this._idp.stop();
}
- this._idpchannel = new IdpProxy(provider, protocol);
+ this._idp = new IdpSandbox(provider, protocol);
},
close: function() {
this.assertion = null;
this.provider = null;
- if (this._idpchannel) {
- this._idpchannel.close();
- this._idpchannel = null;
+ this.protocol = null;
+ if (this._idp) {
+ this._idp.stop();
+ this._idp = null;
}
},
/**
* Generate an error event of the identified type;
* and put a little more precise information in the console.
+ *
+ * A little note on error handling in this class: this class reports errors
+ * exclusively through the event handlers that are passed to it
+ * (this._dispatchError, specifically). That means that all the functions
+ * return resolved promises; promises are never rejected. This probably isn't
+ * the best design, but the refactor can wait.
*/
reportError: function(type, message, extra) {
let args = {
idp: this.provider,
protocol: this.protocol
};
if (extra) {
Object.keys(extra).forEach(function(k) {
args[k] = extra[k];
});
}
- this._warning("RTC identity: " + message, null, 0);
- let ev = new this._win.RTCPeerConnectionIdentityErrorEvent('idp' + type + 'error', args);
- this._dispatchEvent(ev);
+ this._warning('RTC identity: ' + message, null, 0);
+ this._dispatchError('idp' + type + 'error', args);
},
_getFingerprintsFromSdp: function(sdp) {
let fingerprints = {};
let m = sdp.match(PeerConnectionIdp._fingerprintPattern);
while (m) {
fingerprints[m[0]] = { algorithm: m[1], digest: m[2] };
sdp = sdp.substring(m.index + m[0].length);
m = sdp.match(PeerConnectionIdp._fingerprintPattern);
}
return Object.keys(fingerprints).map(k => fingerprints[k]);
},
+ _isValidAssertion: function(assertion) {
+ return assertion && assertion.idp &&
+ typeof assertion.idp.domain === 'string' &&
+ (!assertion.idp.protocol ||
+ typeof assertion.idp.protocol === 'string') &&
+ typeof assertion.assertion === 'string';
+ },
+
_getIdentityFromSdp: function(sdp) {
// a=identity is session level
let idMatch;
let mLineMatch = sdp.match(PeerConnectionIdp._mLinePattern);
if (mLineMatch) {
let sessionLevel = sdp.substring(0, mLineMatch.index);
- idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern);
+ let idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern);
+ }
+ if (!idMatch) {
+ return; // undefined === no identity
}
- if (idMatch) {
- let assertion = {};
- try {
- assertion = JSON.parse(atob(idMatch[1]));
- } catch (e) {
- this.reportError("validation",
- "invalid identity assertion: " + e);
- } // for JSON.parse
- if (typeof assertion.idp === "object" &&
- typeof assertion.idp.domain === "string" &&
- typeof assertion.assertion === "string") {
- return assertion;
- }
- this.reportError("validation", "assertion missing" +
- " idp/idp.domain/assertion");
+ let assertion;
+ try {
+ assertion = JSON.parse(atob(idMatch[1]));
+ } catch (e) {
+ this.reportError('validation',
+ 'invalid identity assertion: ' + e);
}
- // undefined!
+ if (!this._isValidAssertion(assertion)) {
+ this.reportError('validation', 'assertion missing' +
+ ' idp/idp.domain/assertion');
+ }
+ return assertion;
},
/**
- * Queues a task to verify the a=identity line the given SDP contains, if any.
+ * Verifies the a=identity line the given SDP contains, if any.
* If the verification succeeds callback is called with the message from the
* IdP proxy as parameter, else (verification failed OR no a=identity line in
* SDP at all) null is passed to callback.
+ *
+ * Note that this only verifies that the SDP is coherent. This relies on the
+ * invariant that the RTCPeerConnection won't connect to a peer if the
+ * fingerprint of the certificate they offer doesn't appear in the SDP.
*/
- verifyIdentityFromSDP: function(sdp, origin, callback) {
+ verifyIdentityFromSDP: function(sdp, origin) {
let identity = this._getIdentityFromSdp(sdp);
let fingerprints = this._getFingerprintsFromSdp(sdp);
- // it's safe to use the fingerprint we got from the SDP here,
- // only because we ensure that there is only one
if (!identity || fingerprints.length <= 0) {
- callback(null);
- return;
+ return Promise.resolve();
}
this.setIdentityProvider(identity.idp.domain, identity.idp.protocol);
- this._verifyIdentity(identity.assertion, fingerprints, origin, callback);
+ return this._verifyIdentity(identity.assertion, fingerprints, origin);
},
/**
* Checks that the name in the identity provided by the IdP is OK.
*
+ * @param error (function) an error function to call
* @param name (string) the name to validate
- * @returns (string) an error message, iff the name isn't good
+ * @throws if the name isn't valid
*/
- _validateName: function(name) {
- if (typeof name !== "string") {
- return "name not a string";
+ _validateName: function(error, name) {
+ if (typeof name !== 'string') {
+ return error('name not a string');
}
- let atIdx = name.indexOf("@");
- if (atIdx > 0) {
- // no third party assertions... for now
- let tail = name.substring(atIdx + 1);
+ let atIdx = name.indexOf('@');
+ if (atIdx <= 0) {
+ return error('missing authority in name from IdP');
+ }
- // strip the port number, if present
- let provider = this.provider;
- let providerPortIdx = provider.indexOf(":");
- if (providerPortIdx > 0) {
- provider = provider.substring(0, providerPortIdx);
- }
- let idnService = Components.classes["@mozilla.org/network/idn-service;1"].
- getService(Components.interfaces.nsIIDNService);
- if (idnService.convertUTF8toACE(tail) !==
- idnService.convertUTF8toACE(provider)) {
- return "name '" + identity.name +
- "' doesn't match IdP: '" + this.provider + "'";
- }
- return null;
+ // no third party assertions... for now
+ let tail = name.substring(atIdx + 1);
+
+ // strip the port number, if present
+ let provider = this.provider;
+ let providerPortIdx = provider.indexOf(':');
+ if (providerPortIdx > 0) {
+ provider = provider.substring(0, providerPortIdx);
}
- return "missing authority in name from IdP";
+ let idnService = Components.classes['@mozilla.org/network/idn-service;1'].
+ getService(Components.interfaces.nsIIDNService);
+ if (idnService.convertUTF8toACE(tail) !==
+ idnService.convertUTF8toACE(provider)) {
+ return error('name "' + identity.name +
+ '" doesn\'t match IdP: "' + this.provider + '"');
+ }
+ return true;
},
- // we are very defensive here when handling the message from the IdP
- // proxy so that broken IdPs can only do as little harm as possible.
- _checkVerifyResponse: function(message, fingerprints) {
- let warn = msg => {
- this.reportError("validation",
- "assertion validation failure: " + msg);
+ /**
+ * Check the validation response. We are very defensive here when handling
+ * the message from the IdP proxy. That way, broken IdPs aren't likely to
+ * cause catastrophic damage.
+ */
+ _isValidVerificationResponse: function(validation, sdpFingerprints) {
+ let error = msg => {
+ this.reportError('validation', 'assertion validation failure: ' + msg);
+ return false;
};
- let isSubsetOf = (outer, inner, cmp) => {
- return inner.some(i => {
- return !outer.some(o => cmp(i, o));
+ if (typeof validation !== 'object' ||
+ typeof validation.contents !== 'string' ||
+ typeof validation.identity !== 'string') {
+ return error('no payload in validation response');
+ }
+
+ let fingerprints;
+ try {
+ fingerprints = JSON.parse(validation.contents).fingerprint;
+ } catch (e) {
+ return error('idp returned invalid JSON');
+ }
+
+ let isFingerprint = f =>
+ (typeof f.digest === 'string') &&
+ (typeof f.algorithm === 'string');
+ if (!Array.isArray(fingerprints) || !fingerprints.every(isFingerprint)) {
+ return error('fingerprints must be an array of objects' +
+ ' with digest and algorithm attributes');
+ }
+
+ let isSubsetOf = (outerSet, innerSet, comparator) => {
+ return innerSet.every(i => {
+ return outerSet.some(o => comparator(i, o));
});
};
let compareFingerprints = (a, b) => {
return (a.digest === b.digest) && (a.algorithm === b.algorithm);
};
-
- try {
- let contents = JSON.parse(message.contents);
- if (!Array.isArray(contents.fingerprint)) {
- warn("fingerprint is not an array");
- } else if (isSubsetOf(contents.fingerprint, fingerprints,
- compareFingerprints)) {
- warn("fingerprints in SDP aren't a subset of those in the assertion");
- } else {
- let error = this._validateName(message.identity);
- if (error) {
- warn(error);
- } else {
- return true;
- }
- }
- } catch(e) {
- warn("invalid JSON in content");
+ if (!isSubsetOf(fingerprints, sdpFingerprints, compareFingerprints)) {
+ return error('the fingerprints in SDP aren\'t covered by the assertion');
}
- return false;
+ return this._validateName(error, validation.identity);
},
/**
- * Asks the IdP proxy to verify an identity.
+ * Asks the IdP proxy to verify an identity assertion.
*/
- _verifyIdentity: function(assertion, fingerprints, origin, callback) {
- function onVerification(message) {
- if (message && this._checkVerifyResponse(message, fingerprints)) {
- callback(message);
- } else {
- this._warning("RTC identity: assertion validation failure", null, 0);
- callback(null);
- }
- }
+ _verifyIdentity: function(assertion, fingerprints, origin) {
+ let validationPromise = this._idp.start()
+ .then(idp => idp.validateAssertion(assertion, origin));
- let request = {
- type: "VERIFY",
- message: assertion,
- origin: origin
- };
- this._sendToIdp(request, "validation", onVerification.bind(this));
+ return this._safetyNet('validation', validationPromise)
+ .then(validation => {
+ if (validation &&
+ this._isValidVerificationResponse(validation, fingerprints)) {
+ return validation;
+ }
+ });
},
/**
- * Asks the IdP proxy for an identity assertion and, on success, enriches the
- * given SDP with an a=identity line and calls callback with the new SDP as
- * parameter. If no IdP is configured the original SDP (without a=identity
- * line) is passed to the callback.
+ * Enriches the given SDP with an `a=identity` line. getIdentityAssertion()
+ * must have already run successfully, otherwise this does nothing to the sdp.
*/
- appendIdentityToSDP: function(sdp, fingerprint, origin, callback) {
- let onAssertion = function() {
- callback(this.wrapSdp(sdp), this.assertion);
- }.bind(this);
-
- if (!this._idpchannel || this.assertion) {
- onAssertion();
- return;
- }
-
- this._getIdentityAssertion(fingerprint, origin, onAssertion);
- },
-
- /**
- * Inserts an identity assertion into the given SDP.
- */
- wrapSdp: function(sdp) {
+ addIdentityAttribute: function(sdp) {
if (!this.assertion) {
return sdp;
}
// yes, we assume that this matches; if it doesn't something is *wrong*
let match = sdp.match(PeerConnectionIdp._mLinePattern);
return sdp.substring(0, match.index) +
- "a=identity:" + this.assertion + "\r\n" +
+ 'a=identity:' + this.assertion + '\r\n' +
sdp.substring(match.index);
},
- getIdentityAssertion: function(fingerprint, callback) {
- if (!this._idpchannel) {
- this.reportError("assertion", "IdP not set");
- callback(null);
- return;
+ /**
+ * Asks the IdP proxy for an identity assertion. Don't call this unless you
+ * have checked .enabled, or you really like exceptions.
+ */
+ getIdentityAssertion: function(fingerprint) {
+ if (!this.enabled) {
+ this.reportError('assertion', 'no IdP set,' +
+ ' call setIdentityProvider() to set one');
+ return Promise.resolve();
}
- let origin = Cu.getWebIDLCallerPrincipal().origin;
- this._getIdentityAssertion(fingerprint, origin, callback);
- },
-
- _getIdentityAssertion: function(fingerprint, origin, callback) {
- let [algorithm, digest] = fingerprint.split(" ", 2);
- let message = {
+ let [algorithm, digest] = fingerprint.split(' ', 2);
+ let content = {
fingerprint: [{
algorithm: algorithm,
digest: digest
}]
};
- let request = {
- type: "SIGN",
- message: JSON.stringify(message),
- username: this.username,
- origin: origin
- };
+ let origin = Cu.getWebIDLCallerPrincipal().origin;
+
+ let assertionPromise = this._idp.start()
+ .then(idp => idp.generateAssertion(JSON.stringify(content),
+ origin, this.username));
- // catch the assertion, clean it up, warn if absent
- function trapAssertion(assertion) {
- if (!assertion) {
- this._warning("RTC identity: assertion generation failure", null, 0);
- this.assertion = null;
- } else {
- this.assertion = btoa(JSON.stringify(assertion));
- }
- callback(this.assertion);
- }
-
- this._sendToIdp(request, "assertion", trapAssertion.bind(this));
+ return this._safetyNet('assertion', assertionPromise)
+ .then(assertion => {
+ if (this._isValidAssertion(assertion)) {
+ // save the base64+JSON assertion, since that is all that is used
+ this.assertion = btoa(JSON.stringify(assertion));
+ } else {
+ if (assertion) {
+ // only report an error for an invalid assertion
+ // other paths generate more specific error reports
+ this.reportError('assertion', 'invalid assertion generated');
+ }
+ this.assertion = null;
+ }
+ return this.assertion;
+ });
},
/**
- * Packages a message and sends it to the IdP.
- * @param request (dictionary) the message to send
- * @param type (DOMString) the type of message (assertion/validation)
- * @param callback (function) the function to call with the results
+ * Wraps a promise, adding a timeout guard on it so that it can't take longer
+ * than the specified time. Returns a promise that always resolves; if there
+ * is a problem the resolved value is undefined.
*/
- _sendToIdp: function(request, type, callback) {
- this._idpchannel.send(request, this._wrapCallback(type, callback));
- },
-
- _reportIdpError: function(type, message) {
- let args = {};
- let msg = "";
- if (message.type === "ERROR") {
- msg = message.error;
- } else {
- msg = JSON.stringify(message.message);
- if (message.type === "LOGINNEEDED") {
- args.loginUrl = message.loginUrl;
- }
- }
- this.reportError(type, "received response of type '" +
- message.type + "' from IdP: " + msg, args);
- },
-
- /**
- * Wraps a callback, adding a timeout and ensuring that the callback doesn't
- * receive any message other than one where the IdP generated a "SUCCESS"
- * response.
- */
- _wrapCallback: function(type, callback) {
- let timeout = this._win.setTimeout(function() {
- this.reportError(type, "IdP timeout for " + this._idpchannel + " " +
- (this._idpchannel.ready ? "[ready]" : "[not ready]"));
- timeout = null;
- callback(null);
- }.bind(this), this._timeout);
-
- return function(message) {
- if (!timeout) {
- return;
- }
- this._win.clearTimeout(timeout);
- timeout = null;
-
- let content = null;
- if (message.type === "SUCCESS") {
- content = message.message;
- } else {
- this._reportIdpError(type, message);
- }
- callback(content);
- }.bind(this);
+ _safetyNet: function(type, p) {
+ let done = false; // ... all because Promises don't expose state
+ let timeoutPromise = delay(this._timeout)
+ .then(() => {
+ if (!done) {
+ this.reportError(type, 'IdP timed out');
+ }
+ });
+ let realPromise = p
+ .catch(e => this.reportError(type, 'error reported by IdP: ' + e.message))
+ .then(result => {
+ done = true;
+ return result;
+ });
+ // If timeoutPromise completes first, the returned value will be undefined,
+ // just like when there is an error.
+ return Promise.race([realPromise, timeoutPromise]);
}
};
this.PeerConnectionIdp = PeerConnectionIdp;
--- a/dom/media/moz.build
+++ b/dom/media/moz.build
@@ -233,17 +233,17 @@ if CONFIG['GNU_CC'] or CONFIG['CLANG_CL'
SOURCES['DecoderTraits.cpp'].flags += ['-Wno-error=multichar']
EXTRA_COMPONENTS += [
'PeerConnection.js',
'PeerConnection.manifest',
]
EXTRA_JS_MODULES.media += [
- 'IdpProxy.jsm',
+ 'IdpSandbox.jsm',
'PeerConnectionIdp.jsm',
'RTCStatsReport.jsm',
]
FAIL_ON_WARNINGS = True
MSVC_ENABLE_PGO = True