michael@0: /* jshint moz:true, browser:true */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: this.EXPORTED_SYMBOLS = ["PeerConnectionIdp"]; michael@0: michael@0: const {classes: Cc, interfaces: Ci, utils: Cu} = Components; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "IdpProxy", michael@0: "resource://gre/modules/media/IdpProxy.jsm"); michael@0: michael@0: /** michael@0: * Creates an IdP helper. michael@0: * michael@0: * @param window (object) the window object to use for miscellaneous goodies michael@0: * @param timeout (int) the timeout in milliseconds michael@0: * @param warningFunc (function) somewhere to dump warning messages michael@0: * @param dispatchEventFunc (function) somewhere to dump error events michael@0: */ michael@0: function PeerConnectionIdp(window, timeout, warningFunc, dispatchEventFunc) { michael@0: this._win = window; michael@0: this._timeout = timeout || 5000; michael@0: this._warning = warningFunc; michael@0: this._dispatchEvent = dispatchEventFunc; michael@0: michael@0: this.assertion = null; michael@0: this.provider = null; michael@0: } michael@0: michael@0: (function() { michael@0: PeerConnectionIdp._mLinePattern = new RegExp("^m=", "m"); michael@0: // attributes are funny, the 'a' is case sensitive, the name isn't michael@0: let pattern = "^a=[iI][dD][eE][nN][tT][iI][tT][yY]:(\\S+)"; michael@0: PeerConnectionIdp._identityPattern = new RegExp(pattern, "m"); michael@0: pattern = "^a=[fF][iI][nN][gG][eE][rR][pP][rR][iI][nN][tT]:(\\S+) (\\S+)"; michael@0: PeerConnectionIdp._fingerprintPattern = new RegExp(pattern, "m"); michael@0: })(); michael@0: michael@0: PeerConnectionIdp.prototype = { michael@0: setIdentityProvider: function(provider, protocol, username) { michael@0: this.provider = provider; michael@0: this.protocol = protocol; michael@0: this.username = username; michael@0: if (this._idpchannel) { michael@0: if (this._idpchannel.isSame(provider, protocol)) { michael@0: return; michael@0: } michael@0: this._idpchannel.close(); michael@0: } michael@0: this._idpchannel = new IdpProxy(provider, protocol); michael@0: }, michael@0: michael@0: close: function() { michael@0: this.assertion = null; michael@0: this.provider = null; michael@0: if (this._idpchannel) { michael@0: this._idpchannel.close(); michael@0: this._idpchannel = null; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Generate an error event of the identified type; michael@0: * and put a little more precise information in the console. michael@0: */ michael@0: reportError: function(type, message, extra) { michael@0: let args = { michael@0: idp: this.provider, michael@0: protocol: this.protocol michael@0: }; michael@0: if (extra) { michael@0: Object.keys(extra).forEach(function(k) { michael@0: args[k] = extra[k]; michael@0: }); michael@0: } michael@0: this._warning("RTC identity: " + message, null, 0); michael@0: let ev = new this._win.RTCPeerConnectionIdentityErrorEvent('idp' + type + 'error', args); michael@0: this._dispatchEvent(ev); michael@0: }, michael@0: michael@0: _getFingerprintFromSdp: function(sdp) { michael@0: let sections = sdp.split(PeerConnectionIdp._mLinePattern); michael@0: let attributes = sections.map(function(sect) { michael@0: let m = sect.match(PeerConnectionIdp._fingerprintPattern); michael@0: if (m) { michael@0: let remainder = sect.substring(m.index + m[0].length); michael@0: if (!remainder.match(PeerConnectionIdp._fingerprintPattern)) { michael@0: return { algorithm: m[1], digest: m[2] }; michael@0: } michael@0: this.reportError("validation", "two fingerprint values" + michael@0: " in same media section are not supported"); michael@0: // we have to return non-falsy here so that a media section doesn't michael@0: // accidentally fall back to the session-level stuff (which is bad) michael@0: return "error"; michael@0: } michael@0: // return undefined unless there is exactly one match michael@0: }, this); michael@0: michael@0: let sessionLevel = attributes.shift(); michael@0: attributes = attributes.map(function(sectionLevel) { michael@0: return sectionLevel || sessionLevel; michael@0: }); michael@0: michael@0: let first = attributes.shift(); michael@0: function sameAsFirst(attr) { michael@0: return typeof attr === "object" && michael@0: first.algorithm === attr.algorithm && michael@0: first.digest === attr.digest; michael@0: } michael@0: michael@0: if (typeof first === "object" && attributes.every(sameAsFirst)) { michael@0: return first; michael@0: } michael@0: // undefined! michael@0: }, michael@0: michael@0: _getIdentityFromSdp: function(sdp) { michael@0: // a=identity is session level michael@0: let mLineMatch = sdp.match(PeerConnectionIdp._mLinePattern); michael@0: let sessionLevel = sdp.substring(0, mLineMatch.index); michael@0: let idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern); michael@0: if (idMatch) { michael@0: let assertion = {}; michael@0: try { michael@0: assertion = JSON.parse(atob(idMatch[1])); michael@0: } catch (e) { michael@0: this.reportError("validation", michael@0: "invalid identity assertion: " + e); michael@0: } // for JSON.parse michael@0: if (typeof assertion.idp === "object" && michael@0: typeof assertion.idp.domain === "string" && michael@0: typeof assertion.assertion === "string") { michael@0: return assertion; michael@0: } michael@0: michael@0: this.reportError("validation", "assertion missing" + michael@0: " idp/idp.domain/assertion"); michael@0: } michael@0: // undefined! michael@0: }, michael@0: michael@0: /** michael@0: * Queues a task to verify the a=identity line the given SDP contains, if any. michael@0: * If the verification succeeds callback is called with the message from the michael@0: * IdP proxy as parameter, else (verification failed OR no a=identity line in michael@0: * SDP at all) null is passed to callback. michael@0: */ michael@0: verifyIdentityFromSDP: function(sdp, callback) { michael@0: let identity = this._getIdentityFromSdp(sdp); michael@0: let fingerprint = this._getFingerprintFromSdp(sdp); michael@0: // it's safe to use the fingerprint we got from the SDP here, michael@0: // only because we ensure that there is only one michael@0: if (!fingerprint || !identity) { michael@0: callback(null); michael@0: return; michael@0: } michael@0: michael@0: this.setIdentityProvider(identity.idp.domain, identity.idp.protocol); michael@0: this._verifyIdentity(identity.assertion, fingerprint, callback); michael@0: }, michael@0: michael@0: /** michael@0: * Checks that the name in the identity provided by the IdP is OK. michael@0: * michael@0: * @param name (string) the name to validate michael@0: * @returns (string) an error message, iff the name isn't good michael@0: */ michael@0: _validateName: function(name) { michael@0: if (typeof name !== "string") { michael@0: return "name not a string"; michael@0: } michael@0: let atIdx = name.indexOf("@"); michael@0: if (atIdx > 0) { michael@0: // no third party assertions... for now michael@0: let tail = name.substring(atIdx + 1); michael@0: michael@0: // strip the port number, if present michael@0: let provider = this.provider; michael@0: let providerPortIdx = provider.indexOf(":"); michael@0: if (providerPortIdx > 0) { michael@0: provider = provider.substring(0, providerPortIdx); michael@0: } michael@0: let idnService = Components.classes["@mozilla.org/network/idn-service;1"]. michael@0: getService(Components.interfaces.nsIIDNService); michael@0: if (idnService.convertUTF8toACE(tail) !== michael@0: idnService.convertUTF8toACE(provider)) { michael@0: return "name '" + identity.name + michael@0: "' doesn't match IdP: '" + this.provider + "'"; michael@0: } michael@0: return null; michael@0: } michael@0: return "missing authority in name from IdP"; michael@0: }, michael@0: michael@0: // we are very defensive here when handling the message from the IdP michael@0: // proxy so that broken IdPs can only do as little harm as possible. michael@0: _checkVerifyResponse: function(message, fingerprint) { michael@0: let warn = function(msg) { michael@0: this.reportError("validation", michael@0: "assertion validation failure: " + msg); michael@0: }.bind(this); michael@0: michael@0: try { michael@0: let contents = JSON.parse(message.contents); michael@0: if (typeof contents.fingerprint !== "object") { michael@0: warn("fingerprint is not an object"); michael@0: } else if (contents.fingerprint.digest !== fingerprint.digest || michael@0: contents.fingerprint.algorithm !== fingerprint.algorithm) { michael@0: warn("fingerprint does not match"); michael@0: } else { michael@0: let error = this._validateName(message.identity); michael@0: if (error) { michael@0: warn(error); michael@0: } else { michael@0: return true; michael@0: } michael@0: } michael@0: } catch(e) { michael@0: warn("invalid JSON in content"); michael@0: } michael@0: return false; michael@0: }, michael@0: michael@0: /** michael@0: * Asks the IdP proxy to verify an identity. michael@0: */ michael@0: _verifyIdentity: function( michael@0: assertion, fingerprint, callback) { michael@0: function onVerification(message) { michael@0: if (message && this._checkVerifyResponse(message, fingerprint)) { michael@0: callback(message); michael@0: } else { michael@0: this._warning("RTC identity: assertion validation failure", null, 0); michael@0: callback(null); michael@0: } michael@0: } michael@0: michael@0: let request = { michael@0: type: "VERIFY", michael@0: message: assertion michael@0: }; michael@0: this._sendToIdp(request, "validation", onVerification.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Asks the IdP proxy for an identity assertion and, on success, enriches the michael@0: * given SDP with an a=identity line and calls callback with the new SDP as michael@0: * parameter. If no IdP is configured the original SDP (without a=identity michael@0: * line) is passed to the callback. michael@0: */ michael@0: appendIdentityToSDP: function(sdp, fingerprint, callback) { michael@0: let onAssertion = function() { michael@0: callback(this.wrapSdp(sdp), this.assertion); michael@0: }.bind(this); michael@0: michael@0: if (!this._idpchannel || this.assertion) { michael@0: onAssertion(); michael@0: return; michael@0: } michael@0: michael@0: this._getIdentityAssertion(fingerprint, onAssertion); michael@0: }, michael@0: michael@0: /** michael@0: * Inserts an identity assertion into the given SDP. michael@0: */ michael@0: wrapSdp: function(sdp) { michael@0: if (!this.assertion) { michael@0: return sdp; michael@0: } michael@0: michael@0: // yes, we assume that this matches; if it doesn't something is *wrong* michael@0: let match = sdp.match(PeerConnectionIdp._mLinePattern); michael@0: return sdp.substring(0, match.index) + michael@0: "a=identity:" + this.assertion + "\r\n" + michael@0: sdp.substring(match.index); michael@0: }, michael@0: michael@0: getIdentityAssertion: function(fingerprint, callback) { michael@0: if (!this._idpchannel) { michael@0: this.reportError("assertion", "IdP not set"); michael@0: callback(null); michael@0: return; michael@0: } michael@0: michael@0: this._getIdentityAssertion(fingerprint, callback); michael@0: }, michael@0: michael@0: _getIdentityAssertion: function(fingerprint, callback) { michael@0: let [algorithm, digest] = fingerprint.split(" "); michael@0: let message = { michael@0: fingerprint: { michael@0: algorithm: algorithm, michael@0: digest: digest michael@0: } michael@0: }; michael@0: let request = { michael@0: type: "SIGN", michael@0: message: JSON.stringify(message), michael@0: username: this.username michael@0: }; michael@0: michael@0: // catch the assertion, clean it up, warn if absent michael@0: function trapAssertion(assertion) { michael@0: if (!assertion) { michael@0: this._warning("RTC identity: assertion generation failure", null, 0); michael@0: this.assertion = null; michael@0: } else { michael@0: this.assertion = btoa(JSON.stringify(assertion)); michael@0: } michael@0: callback(this.assertion); michael@0: } michael@0: michael@0: this._sendToIdp(request, "assertion", trapAssertion.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Packages a message and sends it to the IdP. michael@0: * @param request (dictionary) the message to send michael@0: * @param type (DOMString) the type of message (assertion/validation) michael@0: * @param callback (function) the function to call with the results michael@0: */ michael@0: _sendToIdp: function(request, type, callback) { michael@0: request.origin = Cu.getWebIDLCallerPrincipal().origin; michael@0: this._idpchannel.send(request, this._wrapCallback(type, callback)); michael@0: }, michael@0: michael@0: _reportIdpError: function(type, message) { michael@0: let args = {}; michael@0: let msg = ""; michael@0: if (message.type === "ERROR") { michael@0: msg = message.error; michael@0: } else { michael@0: msg = JSON.stringify(message.message); michael@0: if (message.type === "LOGINNEEDED") { michael@0: args.loginUrl = message.loginUrl; michael@0: } michael@0: } michael@0: this.reportError(type, "received response of type '" + michael@0: message.type + "' from IdP: " + msg, args); michael@0: }, michael@0: michael@0: /** michael@0: * Wraps a callback, adding a timeout and ensuring that the callback doesn't michael@0: * receive any message other than one where the IdP generated a "SUCCESS" michael@0: * response. michael@0: */ michael@0: _wrapCallback: function(type, callback) { michael@0: let timeout = this._win.setTimeout(function() { michael@0: this.reportError(type, "IdP timeout for " + this._idpchannel + " " + michael@0: (this._idpchannel.ready ? "[ready]" : "[not ready]")); michael@0: timeout = null; michael@0: callback(null); michael@0: }.bind(this), this._timeout); michael@0: michael@0: return function(message) { michael@0: if (!timeout) { michael@0: return; michael@0: } michael@0: this._win.clearTimeout(timeout); michael@0: timeout = null; michael@0: michael@0: let content = null; michael@0: if (message.type === "SUCCESS") { michael@0: content = message.message; michael@0: } else { michael@0: this._reportIdpError(type, message); michael@0: } michael@0: callback(content); michael@0: }.bind(this); michael@0: } michael@0: }; michael@0: michael@0: this.PeerConnectionIdp = PeerConnectionIdp;