diff -r 000000000000 -r 6474c204b198 dom/media/PeerConnectionIdp.jsm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dom/media/PeerConnectionIdp.jsm Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,377 @@ +/* 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"]; + +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"); + +/** + * 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 + */ +function PeerConnectionIdp(window, timeout, warningFunc, dispatchEventFunc) { + this._win = window; + this._timeout = timeout || 5000; + this._warning = warningFunc; + this._dispatchEvent = dispatchEventFunc; + + this.assertion = null; + this.provider = null; +} + +(function() { + 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"); +})(); + +PeerConnectionIdp.prototype = { + setIdentityProvider: function(provider, protocol, username) { + this.provider = provider; + this.protocol = protocol; + this.username = username; + if (this._idpchannel) { + if (this._idpchannel.isSame(provider, protocol)) { + return; + } + this._idpchannel.close(); + } + this._idpchannel = new IdpProxy(provider, protocol); + }, + + close: function() { + this.assertion = null; + this.provider = null; + if (this._idpchannel) { + this._idpchannel.close(); + this._idpchannel = null; + } + }, + + /** + * Generate an error event of the identified type; + * and put a little more precise information in the console. + */ + 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); + }, + + _getFingerprintFromSdp: function(sdp) { + let sections = sdp.split(PeerConnectionIdp._mLinePattern); + let attributes = sections.map(function(sect) { + let m = sect.match(PeerConnectionIdp._fingerprintPattern); + if (m) { + let remainder = sect.substring(m.index + m[0].length); + if (!remainder.match(PeerConnectionIdp._fingerprintPattern)) { + return { algorithm: m[1], digest: m[2] }; + } + this.reportError("validation", "two fingerprint values" + + " in same media section are not supported"); + // we have to return non-falsy here so that a media section doesn't + // accidentally fall back to the session-level stuff (which is bad) + return "error"; + } + // return undefined unless there is exactly one match + }, this); + + let sessionLevel = attributes.shift(); + attributes = attributes.map(function(sectionLevel) { + return sectionLevel || sessionLevel; + }); + + let first = attributes.shift(); + function sameAsFirst(attr) { + return typeof attr === "object" && + first.algorithm === attr.algorithm && + first.digest === attr.digest; + } + + if (typeof first === "object" && attributes.every(sameAsFirst)) { + return first; + } + // undefined! + }, + + _getIdentityFromSdp: function(sdp) { + // a=identity is session level + let mLineMatch = sdp.match(PeerConnectionIdp._mLinePattern); + let sessionLevel = sdp.substring(0, mLineMatch.index); + let idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern); + 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"); + } + // undefined! + }, + + /** + * Queues a task to verify 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. + */ + verifyIdentityFromSDP: function(sdp, callback) { + let identity = this._getIdentityFromSdp(sdp); + let fingerprint = this._getFingerprintFromSdp(sdp); + // it's safe to use the fingerprint we got from the SDP here, + // only because we ensure that there is only one + if (!fingerprint || !identity) { + callback(null); + return; + } + + this.setIdentityProvider(identity.idp.domain, identity.idp.protocol); + this._verifyIdentity(identity.assertion, fingerprint, callback); + }, + + /** + * Checks that the name in the identity provided by the IdP is OK. + * + * @param name (string) the name to validate + * @returns (string) an error message, iff the name isn't good + */ + _validateName: function(name) { + if (typeof name !== "string") { + return "name not a string"; + } + let atIdx = name.indexOf("@"); + if (atIdx > 0) { + // 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); + } + 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; + } + return "missing authority in name from IdP"; + }, + + // 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, fingerprint) { + let warn = function(msg) { + this.reportError("validation", + "assertion validation failure: " + msg); + }.bind(this); + + try { + let contents = JSON.parse(message.contents); + if (typeof contents.fingerprint !== "object") { + warn("fingerprint is not an object"); + } else if (contents.fingerprint.digest !== fingerprint.digest || + contents.fingerprint.algorithm !== fingerprint.algorithm) { + warn("fingerprint does not match"); + } else { + let error = this._validateName(message.identity); + if (error) { + warn(error); + } else { + return true; + } + } + } catch(e) { + warn("invalid JSON in content"); + } + return false; + }, + + /** + * Asks the IdP proxy to verify an identity. + */ + _verifyIdentity: function( + assertion, fingerprint, callback) { + function onVerification(message) { + if (message && this._checkVerifyResponse(message, fingerprint)) { + callback(message); + } else { + this._warning("RTC identity: assertion validation failure", null, 0); + callback(null); + } + } + + let request = { + type: "VERIFY", + message: assertion + }; + this._sendToIdp(request, "validation", onVerification.bind(this)); + }, + + /** + * 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. + */ + appendIdentityToSDP: function(sdp, fingerprint, callback) { + let onAssertion = function() { + callback(this.wrapSdp(sdp), this.assertion); + }.bind(this); + + if (!this._idpchannel || this.assertion) { + onAssertion(); + return; + } + + this._getIdentityAssertion(fingerprint, onAssertion); + }, + + /** + * Inserts an identity assertion into the given SDP. + */ + wrapSdp: 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" + + sdp.substring(match.index); + }, + + getIdentityAssertion: function(fingerprint, callback) { + if (!this._idpchannel) { + this.reportError("assertion", "IdP not set"); + callback(null); + return; + } + + this._getIdentityAssertion(fingerprint, callback); + }, + + _getIdentityAssertion: function(fingerprint, callback) { + let [algorithm, digest] = fingerprint.split(" "); + let message = { + fingerprint: { + algorithm: algorithm, + digest: digest + } + }; + let request = { + type: "SIGN", + message: JSON.stringify(message), + username: 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)); + }, + + /** + * 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 + */ + _sendToIdp: function(request, type, callback) { + request.origin = Cu.getWebIDLCallerPrincipal().origin; + 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); + } +}; + +this.PeerConnectionIdp = PeerConnectionIdp;