1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/media/PeerConnectionIdp.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,377 @@ 1.4 +/* jshint moz:true, browser:true */ 1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +this.EXPORTED_SYMBOLS = ["PeerConnectionIdp"]; 1.10 + 1.11 +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; 1.12 + 1.13 +Cu.import("resource://gre/modules/Services.jsm"); 1.14 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.15 +XPCOMUtils.defineLazyModuleGetter(this, "IdpProxy", 1.16 + "resource://gre/modules/media/IdpProxy.jsm"); 1.17 + 1.18 +/** 1.19 + * Creates an IdP helper. 1.20 + * 1.21 + * @param window (object) the window object to use for miscellaneous goodies 1.22 + * @param timeout (int) the timeout in milliseconds 1.23 + * @param warningFunc (function) somewhere to dump warning messages 1.24 + * @param dispatchEventFunc (function) somewhere to dump error events 1.25 + */ 1.26 +function PeerConnectionIdp(window, timeout, warningFunc, dispatchEventFunc) { 1.27 + this._win = window; 1.28 + this._timeout = timeout || 5000; 1.29 + this._warning = warningFunc; 1.30 + this._dispatchEvent = dispatchEventFunc; 1.31 + 1.32 + this.assertion = null; 1.33 + this.provider = null; 1.34 +} 1.35 + 1.36 +(function() { 1.37 + PeerConnectionIdp._mLinePattern = new RegExp("^m=", "m"); 1.38 + // attributes are funny, the 'a' is case sensitive, the name isn't 1.39 + let pattern = "^a=[iI][dD][eE][nN][tT][iI][tT][yY]:(\\S+)"; 1.40 + PeerConnectionIdp._identityPattern = new RegExp(pattern, "m"); 1.41 + pattern = "^a=[fF][iI][nN][gG][eE][rR][pP][rR][iI][nN][tT]:(\\S+) (\\S+)"; 1.42 + PeerConnectionIdp._fingerprintPattern = new RegExp(pattern, "m"); 1.43 +})(); 1.44 + 1.45 +PeerConnectionIdp.prototype = { 1.46 + setIdentityProvider: function(provider, protocol, username) { 1.47 + this.provider = provider; 1.48 + this.protocol = protocol; 1.49 + this.username = username; 1.50 + if (this._idpchannel) { 1.51 + if (this._idpchannel.isSame(provider, protocol)) { 1.52 + return; 1.53 + } 1.54 + this._idpchannel.close(); 1.55 + } 1.56 + this._idpchannel = new IdpProxy(provider, protocol); 1.57 + }, 1.58 + 1.59 + close: function() { 1.60 + this.assertion = null; 1.61 + this.provider = null; 1.62 + if (this._idpchannel) { 1.63 + this._idpchannel.close(); 1.64 + this._idpchannel = null; 1.65 + } 1.66 + }, 1.67 + 1.68 + /** 1.69 + * Generate an error event of the identified type; 1.70 + * and put a little more precise information in the console. 1.71 + */ 1.72 + reportError: function(type, message, extra) { 1.73 + let args = { 1.74 + idp: this.provider, 1.75 + protocol: this.protocol 1.76 + }; 1.77 + if (extra) { 1.78 + Object.keys(extra).forEach(function(k) { 1.79 + args[k] = extra[k]; 1.80 + }); 1.81 + } 1.82 + this._warning("RTC identity: " + message, null, 0); 1.83 + let ev = new this._win.RTCPeerConnectionIdentityErrorEvent('idp' + type + 'error', args); 1.84 + this._dispatchEvent(ev); 1.85 + }, 1.86 + 1.87 + _getFingerprintFromSdp: function(sdp) { 1.88 + let sections = sdp.split(PeerConnectionIdp._mLinePattern); 1.89 + let attributes = sections.map(function(sect) { 1.90 + let m = sect.match(PeerConnectionIdp._fingerprintPattern); 1.91 + if (m) { 1.92 + let remainder = sect.substring(m.index + m[0].length); 1.93 + if (!remainder.match(PeerConnectionIdp._fingerprintPattern)) { 1.94 + return { algorithm: m[1], digest: m[2] }; 1.95 + } 1.96 + this.reportError("validation", "two fingerprint values" + 1.97 + " in same media section are not supported"); 1.98 + // we have to return non-falsy here so that a media section doesn't 1.99 + // accidentally fall back to the session-level stuff (which is bad) 1.100 + return "error"; 1.101 + } 1.102 + // return undefined unless there is exactly one match 1.103 + }, this); 1.104 + 1.105 + let sessionLevel = attributes.shift(); 1.106 + attributes = attributes.map(function(sectionLevel) { 1.107 + return sectionLevel || sessionLevel; 1.108 + }); 1.109 + 1.110 + let first = attributes.shift(); 1.111 + function sameAsFirst(attr) { 1.112 + return typeof attr === "object" && 1.113 + first.algorithm === attr.algorithm && 1.114 + first.digest === attr.digest; 1.115 + } 1.116 + 1.117 + if (typeof first === "object" && attributes.every(sameAsFirst)) { 1.118 + return first; 1.119 + } 1.120 + // undefined! 1.121 + }, 1.122 + 1.123 + _getIdentityFromSdp: function(sdp) { 1.124 + // a=identity is session level 1.125 + let mLineMatch = sdp.match(PeerConnectionIdp._mLinePattern); 1.126 + let sessionLevel = sdp.substring(0, mLineMatch.index); 1.127 + let idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern); 1.128 + if (idMatch) { 1.129 + let assertion = {}; 1.130 + try { 1.131 + assertion = JSON.parse(atob(idMatch[1])); 1.132 + } catch (e) { 1.133 + this.reportError("validation", 1.134 + "invalid identity assertion: " + e); 1.135 + } // for JSON.parse 1.136 + if (typeof assertion.idp === "object" && 1.137 + typeof assertion.idp.domain === "string" && 1.138 + typeof assertion.assertion === "string") { 1.139 + return assertion; 1.140 + } 1.141 + 1.142 + this.reportError("validation", "assertion missing" + 1.143 + " idp/idp.domain/assertion"); 1.144 + } 1.145 + // undefined! 1.146 + }, 1.147 + 1.148 + /** 1.149 + * Queues a task to verify the a=identity line the given SDP contains, if any. 1.150 + * If the verification succeeds callback is called with the message from the 1.151 + * IdP proxy as parameter, else (verification failed OR no a=identity line in 1.152 + * SDP at all) null is passed to callback. 1.153 + */ 1.154 + verifyIdentityFromSDP: function(sdp, callback) { 1.155 + let identity = this._getIdentityFromSdp(sdp); 1.156 + let fingerprint = this._getFingerprintFromSdp(sdp); 1.157 + // it's safe to use the fingerprint we got from the SDP here, 1.158 + // only because we ensure that there is only one 1.159 + if (!fingerprint || !identity) { 1.160 + callback(null); 1.161 + return; 1.162 + } 1.163 + 1.164 + this.setIdentityProvider(identity.idp.domain, identity.idp.protocol); 1.165 + this._verifyIdentity(identity.assertion, fingerprint, callback); 1.166 + }, 1.167 + 1.168 + /** 1.169 + * Checks that the name in the identity provided by the IdP is OK. 1.170 + * 1.171 + * @param name (string) the name to validate 1.172 + * @returns (string) an error message, iff the name isn't good 1.173 + */ 1.174 + _validateName: function(name) { 1.175 + if (typeof name !== "string") { 1.176 + return "name not a string"; 1.177 + } 1.178 + let atIdx = name.indexOf("@"); 1.179 + if (atIdx > 0) { 1.180 + // no third party assertions... for now 1.181 + let tail = name.substring(atIdx + 1); 1.182 + 1.183 + // strip the port number, if present 1.184 + let provider = this.provider; 1.185 + let providerPortIdx = provider.indexOf(":"); 1.186 + if (providerPortIdx > 0) { 1.187 + provider = provider.substring(0, providerPortIdx); 1.188 + } 1.189 + let idnService = Components.classes["@mozilla.org/network/idn-service;1"]. 1.190 + getService(Components.interfaces.nsIIDNService); 1.191 + if (idnService.convertUTF8toACE(tail) !== 1.192 + idnService.convertUTF8toACE(provider)) { 1.193 + return "name '" + identity.name + 1.194 + "' doesn't match IdP: '" + this.provider + "'"; 1.195 + } 1.196 + return null; 1.197 + } 1.198 + return "missing authority in name from IdP"; 1.199 + }, 1.200 + 1.201 + // we are very defensive here when handling the message from the IdP 1.202 + // proxy so that broken IdPs can only do as little harm as possible. 1.203 + _checkVerifyResponse: function(message, fingerprint) { 1.204 + let warn = function(msg) { 1.205 + this.reportError("validation", 1.206 + "assertion validation failure: " + msg); 1.207 + }.bind(this); 1.208 + 1.209 + try { 1.210 + let contents = JSON.parse(message.contents); 1.211 + if (typeof contents.fingerprint !== "object") { 1.212 + warn("fingerprint is not an object"); 1.213 + } else if (contents.fingerprint.digest !== fingerprint.digest || 1.214 + contents.fingerprint.algorithm !== fingerprint.algorithm) { 1.215 + warn("fingerprint does not match"); 1.216 + } else { 1.217 + let error = this._validateName(message.identity); 1.218 + if (error) { 1.219 + warn(error); 1.220 + } else { 1.221 + return true; 1.222 + } 1.223 + } 1.224 + } catch(e) { 1.225 + warn("invalid JSON in content"); 1.226 + } 1.227 + return false; 1.228 + }, 1.229 + 1.230 + /** 1.231 + * Asks the IdP proxy to verify an identity. 1.232 + */ 1.233 + _verifyIdentity: function( 1.234 + assertion, fingerprint, callback) { 1.235 + function onVerification(message) { 1.236 + if (message && this._checkVerifyResponse(message, fingerprint)) { 1.237 + callback(message); 1.238 + } else { 1.239 + this._warning("RTC identity: assertion validation failure", null, 0); 1.240 + callback(null); 1.241 + } 1.242 + } 1.243 + 1.244 + let request = { 1.245 + type: "VERIFY", 1.246 + message: assertion 1.247 + }; 1.248 + this._sendToIdp(request, "validation", onVerification.bind(this)); 1.249 + }, 1.250 + 1.251 + /** 1.252 + * Asks the IdP proxy for an identity assertion and, on success, enriches the 1.253 + * given SDP with an a=identity line and calls callback with the new SDP as 1.254 + * parameter. If no IdP is configured the original SDP (without a=identity 1.255 + * line) is passed to the callback. 1.256 + */ 1.257 + appendIdentityToSDP: function(sdp, fingerprint, callback) { 1.258 + let onAssertion = function() { 1.259 + callback(this.wrapSdp(sdp), this.assertion); 1.260 + }.bind(this); 1.261 + 1.262 + if (!this._idpchannel || this.assertion) { 1.263 + onAssertion(); 1.264 + return; 1.265 + } 1.266 + 1.267 + this._getIdentityAssertion(fingerprint, onAssertion); 1.268 + }, 1.269 + 1.270 + /** 1.271 + * Inserts an identity assertion into the given SDP. 1.272 + */ 1.273 + wrapSdp: function(sdp) { 1.274 + if (!this.assertion) { 1.275 + return sdp; 1.276 + } 1.277 + 1.278 + // yes, we assume that this matches; if it doesn't something is *wrong* 1.279 + let match = sdp.match(PeerConnectionIdp._mLinePattern); 1.280 + return sdp.substring(0, match.index) + 1.281 + "a=identity:" + this.assertion + "\r\n" + 1.282 + sdp.substring(match.index); 1.283 + }, 1.284 + 1.285 + getIdentityAssertion: function(fingerprint, callback) { 1.286 + if (!this._idpchannel) { 1.287 + this.reportError("assertion", "IdP not set"); 1.288 + callback(null); 1.289 + return; 1.290 + } 1.291 + 1.292 + this._getIdentityAssertion(fingerprint, callback); 1.293 + }, 1.294 + 1.295 + _getIdentityAssertion: function(fingerprint, callback) { 1.296 + let [algorithm, digest] = fingerprint.split(" "); 1.297 + let message = { 1.298 + fingerprint: { 1.299 + algorithm: algorithm, 1.300 + digest: digest 1.301 + } 1.302 + }; 1.303 + let request = { 1.304 + type: "SIGN", 1.305 + message: JSON.stringify(message), 1.306 + username: this.username 1.307 + }; 1.308 + 1.309 + // catch the assertion, clean it up, warn if absent 1.310 + function trapAssertion(assertion) { 1.311 + if (!assertion) { 1.312 + this._warning("RTC identity: assertion generation failure", null, 0); 1.313 + this.assertion = null; 1.314 + } else { 1.315 + this.assertion = btoa(JSON.stringify(assertion)); 1.316 + } 1.317 + callback(this.assertion); 1.318 + } 1.319 + 1.320 + this._sendToIdp(request, "assertion", trapAssertion.bind(this)); 1.321 + }, 1.322 + 1.323 + /** 1.324 + * Packages a message and sends it to the IdP. 1.325 + * @param request (dictionary) the message to send 1.326 + * @param type (DOMString) the type of message (assertion/validation) 1.327 + * @param callback (function) the function to call with the results 1.328 + */ 1.329 + _sendToIdp: function(request, type, callback) { 1.330 + request.origin = Cu.getWebIDLCallerPrincipal().origin; 1.331 + this._idpchannel.send(request, this._wrapCallback(type, callback)); 1.332 + }, 1.333 + 1.334 + _reportIdpError: function(type, message) { 1.335 + let args = {}; 1.336 + let msg = ""; 1.337 + if (message.type === "ERROR") { 1.338 + msg = message.error; 1.339 + } else { 1.340 + msg = JSON.stringify(message.message); 1.341 + if (message.type === "LOGINNEEDED") { 1.342 + args.loginUrl = message.loginUrl; 1.343 + } 1.344 + } 1.345 + this.reportError(type, "received response of type '" + 1.346 + message.type + "' from IdP: " + msg, args); 1.347 + }, 1.348 + 1.349 + /** 1.350 + * Wraps a callback, adding a timeout and ensuring that the callback doesn't 1.351 + * receive any message other than one where the IdP generated a "SUCCESS" 1.352 + * response. 1.353 + */ 1.354 + _wrapCallback: function(type, callback) { 1.355 + let timeout = this._win.setTimeout(function() { 1.356 + this.reportError(type, "IdP timeout for " + this._idpchannel + " " + 1.357 + (this._idpchannel.ready ? "[ready]" : "[not ready]")); 1.358 + timeout = null; 1.359 + callback(null); 1.360 + }.bind(this), this._timeout); 1.361 + 1.362 + return function(message) { 1.363 + if (!timeout) { 1.364 + return; 1.365 + } 1.366 + this._win.clearTimeout(timeout); 1.367 + timeout = null; 1.368 + 1.369 + let content = null; 1.370 + if (message.type === "SUCCESS") { 1.371 + content = message.message; 1.372 + } else { 1.373 + this._reportIdpError(type, message); 1.374 + } 1.375 + callback(content); 1.376 + }.bind(this); 1.377 + } 1.378 +}; 1.379 + 1.380 +this.PeerConnectionIdp = PeerConnectionIdp;