dom/media/PeerConnectionIdp.jsm

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* jshint moz:true, browser:true */
michael@0 2 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 5
michael@0 6 this.EXPORTED_SYMBOLS = ["PeerConnectionIdp"];
michael@0 7
michael@0 8 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
michael@0 9
michael@0 10 Cu.import("resource://gre/modules/Services.jsm");
michael@0 11 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 12 XPCOMUtils.defineLazyModuleGetter(this, "IdpProxy",
michael@0 13 "resource://gre/modules/media/IdpProxy.jsm");
michael@0 14
michael@0 15 /**
michael@0 16 * Creates an IdP helper.
michael@0 17 *
michael@0 18 * @param window (object) the window object to use for miscellaneous goodies
michael@0 19 * @param timeout (int) the timeout in milliseconds
michael@0 20 * @param warningFunc (function) somewhere to dump warning messages
michael@0 21 * @param dispatchEventFunc (function) somewhere to dump error events
michael@0 22 */
michael@0 23 function PeerConnectionIdp(window, timeout, warningFunc, dispatchEventFunc) {
michael@0 24 this._win = window;
michael@0 25 this._timeout = timeout || 5000;
michael@0 26 this._warning = warningFunc;
michael@0 27 this._dispatchEvent = dispatchEventFunc;
michael@0 28
michael@0 29 this.assertion = null;
michael@0 30 this.provider = null;
michael@0 31 }
michael@0 32
michael@0 33 (function() {
michael@0 34 PeerConnectionIdp._mLinePattern = new RegExp("^m=", "m");
michael@0 35 // attributes are funny, the 'a' is case sensitive, the name isn't
michael@0 36 let pattern = "^a=[iI][dD][eE][nN][tT][iI][tT][yY]:(\\S+)";
michael@0 37 PeerConnectionIdp._identityPattern = new RegExp(pattern, "m");
michael@0 38 pattern = "^a=[fF][iI][nN][gG][eE][rR][pP][rR][iI][nN][tT]:(\\S+) (\\S+)";
michael@0 39 PeerConnectionIdp._fingerprintPattern = new RegExp(pattern, "m");
michael@0 40 })();
michael@0 41
michael@0 42 PeerConnectionIdp.prototype = {
michael@0 43 setIdentityProvider: function(provider, protocol, username) {
michael@0 44 this.provider = provider;
michael@0 45 this.protocol = protocol;
michael@0 46 this.username = username;
michael@0 47 if (this._idpchannel) {
michael@0 48 if (this._idpchannel.isSame(provider, protocol)) {
michael@0 49 return;
michael@0 50 }
michael@0 51 this._idpchannel.close();
michael@0 52 }
michael@0 53 this._idpchannel = new IdpProxy(provider, protocol);
michael@0 54 },
michael@0 55
michael@0 56 close: function() {
michael@0 57 this.assertion = null;
michael@0 58 this.provider = null;
michael@0 59 if (this._idpchannel) {
michael@0 60 this._idpchannel.close();
michael@0 61 this._idpchannel = null;
michael@0 62 }
michael@0 63 },
michael@0 64
michael@0 65 /**
michael@0 66 * Generate an error event of the identified type;
michael@0 67 * and put a little more precise information in the console.
michael@0 68 */
michael@0 69 reportError: function(type, message, extra) {
michael@0 70 let args = {
michael@0 71 idp: this.provider,
michael@0 72 protocol: this.protocol
michael@0 73 };
michael@0 74 if (extra) {
michael@0 75 Object.keys(extra).forEach(function(k) {
michael@0 76 args[k] = extra[k];
michael@0 77 });
michael@0 78 }
michael@0 79 this._warning("RTC identity: " + message, null, 0);
michael@0 80 let ev = new this._win.RTCPeerConnectionIdentityErrorEvent('idp' + type + 'error', args);
michael@0 81 this._dispatchEvent(ev);
michael@0 82 },
michael@0 83
michael@0 84 _getFingerprintFromSdp: function(sdp) {
michael@0 85 let sections = sdp.split(PeerConnectionIdp._mLinePattern);
michael@0 86 let attributes = sections.map(function(sect) {
michael@0 87 let m = sect.match(PeerConnectionIdp._fingerprintPattern);
michael@0 88 if (m) {
michael@0 89 let remainder = sect.substring(m.index + m[0].length);
michael@0 90 if (!remainder.match(PeerConnectionIdp._fingerprintPattern)) {
michael@0 91 return { algorithm: m[1], digest: m[2] };
michael@0 92 }
michael@0 93 this.reportError("validation", "two fingerprint values" +
michael@0 94 " in same media section are not supported");
michael@0 95 // we have to return non-falsy here so that a media section doesn't
michael@0 96 // accidentally fall back to the session-level stuff (which is bad)
michael@0 97 return "error";
michael@0 98 }
michael@0 99 // return undefined unless there is exactly one match
michael@0 100 }, this);
michael@0 101
michael@0 102 let sessionLevel = attributes.shift();
michael@0 103 attributes = attributes.map(function(sectionLevel) {
michael@0 104 return sectionLevel || sessionLevel;
michael@0 105 });
michael@0 106
michael@0 107 let first = attributes.shift();
michael@0 108 function sameAsFirst(attr) {
michael@0 109 return typeof attr === "object" &&
michael@0 110 first.algorithm === attr.algorithm &&
michael@0 111 first.digest === attr.digest;
michael@0 112 }
michael@0 113
michael@0 114 if (typeof first === "object" && attributes.every(sameAsFirst)) {
michael@0 115 return first;
michael@0 116 }
michael@0 117 // undefined!
michael@0 118 },
michael@0 119
michael@0 120 _getIdentityFromSdp: function(sdp) {
michael@0 121 // a=identity is session level
michael@0 122 let mLineMatch = sdp.match(PeerConnectionIdp._mLinePattern);
michael@0 123 let sessionLevel = sdp.substring(0, mLineMatch.index);
michael@0 124 let idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern);
michael@0 125 if (idMatch) {
michael@0 126 let assertion = {};
michael@0 127 try {
michael@0 128 assertion = JSON.parse(atob(idMatch[1]));
michael@0 129 } catch (e) {
michael@0 130 this.reportError("validation",
michael@0 131 "invalid identity assertion: " + e);
michael@0 132 } // for JSON.parse
michael@0 133 if (typeof assertion.idp === "object" &&
michael@0 134 typeof assertion.idp.domain === "string" &&
michael@0 135 typeof assertion.assertion === "string") {
michael@0 136 return assertion;
michael@0 137 }
michael@0 138
michael@0 139 this.reportError("validation", "assertion missing" +
michael@0 140 " idp/idp.domain/assertion");
michael@0 141 }
michael@0 142 // undefined!
michael@0 143 },
michael@0 144
michael@0 145 /**
michael@0 146 * Queues a task to verify the a=identity line the given SDP contains, if any.
michael@0 147 * If the verification succeeds callback is called with the message from the
michael@0 148 * IdP proxy as parameter, else (verification failed OR no a=identity line in
michael@0 149 * SDP at all) null is passed to callback.
michael@0 150 */
michael@0 151 verifyIdentityFromSDP: function(sdp, callback) {
michael@0 152 let identity = this._getIdentityFromSdp(sdp);
michael@0 153 let fingerprint = this._getFingerprintFromSdp(sdp);
michael@0 154 // it's safe to use the fingerprint we got from the SDP here,
michael@0 155 // only because we ensure that there is only one
michael@0 156 if (!fingerprint || !identity) {
michael@0 157 callback(null);
michael@0 158 return;
michael@0 159 }
michael@0 160
michael@0 161 this.setIdentityProvider(identity.idp.domain, identity.idp.protocol);
michael@0 162 this._verifyIdentity(identity.assertion, fingerprint, callback);
michael@0 163 },
michael@0 164
michael@0 165 /**
michael@0 166 * Checks that the name in the identity provided by the IdP is OK.
michael@0 167 *
michael@0 168 * @param name (string) the name to validate
michael@0 169 * @returns (string) an error message, iff the name isn't good
michael@0 170 */
michael@0 171 _validateName: function(name) {
michael@0 172 if (typeof name !== "string") {
michael@0 173 return "name not a string";
michael@0 174 }
michael@0 175 let atIdx = name.indexOf("@");
michael@0 176 if (atIdx > 0) {
michael@0 177 // no third party assertions... for now
michael@0 178 let tail = name.substring(atIdx + 1);
michael@0 179
michael@0 180 // strip the port number, if present
michael@0 181 let provider = this.provider;
michael@0 182 let providerPortIdx = provider.indexOf(":");
michael@0 183 if (providerPortIdx > 0) {
michael@0 184 provider = provider.substring(0, providerPortIdx);
michael@0 185 }
michael@0 186 let idnService = Components.classes["@mozilla.org/network/idn-service;1"].
michael@0 187 getService(Components.interfaces.nsIIDNService);
michael@0 188 if (idnService.convertUTF8toACE(tail) !==
michael@0 189 idnService.convertUTF8toACE(provider)) {
michael@0 190 return "name '" + identity.name +
michael@0 191 "' doesn't match IdP: '" + this.provider + "'";
michael@0 192 }
michael@0 193 return null;
michael@0 194 }
michael@0 195 return "missing authority in name from IdP";
michael@0 196 },
michael@0 197
michael@0 198 // we are very defensive here when handling the message from the IdP
michael@0 199 // proxy so that broken IdPs can only do as little harm as possible.
michael@0 200 _checkVerifyResponse: function(message, fingerprint) {
michael@0 201 let warn = function(msg) {
michael@0 202 this.reportError("validation",
michael@0 203 "assertion validation failure: " + msg);
michael@0 204 }.bind(this);
michael@0 205
michael@0 206 try {
michael@0 207 let contents = JSON.parse(message.contents);
michael@0 208 if (typeof contents.fingerprint !== "object") {
michael@0 209 warn("fingerprint is not an object");
michael@0 210 } else if (contents.fingerprint.digest !== fingerprint.digest ||
michael@0 211 contents.fingerprint.algorithm !== fingerprint.algorithm) {
michael@0 212 warn("fingerprint does not match");
michael@0 213 } else {
michael@0 214 let error = this._validateName(message.identity);
michael@0 215 if (error) {
michael@0 216 warn(error);
michael@0 217 } else {
michael@0 218 return true;
michael@0 219 }
michael@0 220 }
michael@0 221 } catch(e) {
michael@0 222 warn("invalid JSON in content");
michael@0 223 }
michael@0 224 return false;
michael@0 225 },
michael@0 226
michael@0 227 /**
michael@0 228 * Asks the IdP proxy to verify an identity.
michael@0 229 */
michael@0 230 _verifyIdentity: function(
michael@0 231 assertion, fingerprint, callback) {
michael@0 232 function onVerification(message) {
michael@0 233 if (message && this._checkVerifyResponse(message, fingerprint)) {
michael@0 234 callback(message);
michael@0 235 } else {
michael@0 236 this._warning("RTC identity: assertion validation failure", null, 0);
michael@0 237 callback(null);
michael@0 238 }
michael@0 239 }
michael@0 240
michael@0 241 let request = {
michael@0 242 type: "VERIFY",
michael@0 243 message: assertion
michael@0 244 };
michael@0 245 this._sendToIdp(request, "validation", onVerification.bind(this));
michael@0 246 },
michael@0 247
michael@0 248 /**
michael@0 249 * Asks the IdP proxy for an identity assertion and, on success, enriches the
michael@0 250 * given SDP with an a=identity line and calls callback with the new SDP as
michael@0 251 * parameter. If no IdP is configured the original SDP (without a=identity
michael@0 252 * line) is passed to the callback.
michael@0 253 */
michael@0 254 appendIdentityToSDP: function(sdp, fingerprint, callback) {
michael@0 255 let onAssertion = function() {
michael@0 256 callback(this.wrapSdp(sdp), this.assertion);
michael@0 257 }.bind(this);
michael@0 258
michael@0 259 if (!this._idpchannel || this.assertion) {
michael@0 260 onAssertion();
michael@0 261 return;
michael@0 262 }
michael@0 263
michael@0 264 this._getIdentityAssertion(fingerprint, onAssertion);
michael@0 265 },
michael@0 266
michael@0 267 /**
michael@0 268 * Inserts an identity assertion into the given SDP.
michael@0 269 */
michael@0 270 wrapSdp: function(sdp) {
michael@0 271 if (!this.assertion) {
michael@0 272 return sdp;
michael@0 273 }
michael@0 274
michael@0 275 // yes, we assume that this matches; if it doesn't something is *wrong*
michael@0 276 let match = sdp.match(PeerConnectionIdp._mLinePattern);
michael@0 277 return sdp.substring(0, match.index) +
michael@0 278 "a=identity:" + this.assertion + "\r\n" +
michael@0 279 sdp.substring(match.index);
michael@0 280 },
michael@0 281
michael@0 282 getIdentityAssertion: function(fingerprint, callback) {
michael@0 283 if (!this._idpchannel) {
michael@0 284 this.reportError("assertion", "IdP not set");
michael@0 285 callback(null);
michael@0 286 return;
michael@0 287 }
michael@0 288
michael@0 289 this._getIdentityAssertion(fingerprint, callback);
michael@0 290 },
michael@0 291
michael@0 292 _getIdentityAssertion: function(fingerprint, callback) {
michael@0 293 let [algorithm, digest] = fingerprint.split(" ");
michael@0 294 let message = {
michael@0 295 fingerprint: {
michael@0 296 algorithm: algorithm,
michael@0 297 digest: digest
michael@0 298 }
michael@0 299 };
michael@0 300 let request = {
michael@0 301 type: "SIGN",
michael@0 302 message: JSON.stringify(message),
michael@0 303 username: this.username
michael@0 304 };
michael@0 305
michael@0 306 // catch the assertion, clean it up, warn if absent
michael@0 307 function trapAssertion(assertion) {
michael@0 308 if (!assertion) {
michael@0 309 this._warning("RTC identity: assertion generation failure", null, 0);
michael@0 310 this.assertion = null;
michael@0 311 } else {
michael@0 312 this.assertion = btoa(JSON.stringify(assertion));
michael@0 313 }
michael@0 314 callback(this.assertion);
michael@0 315 }
michael@0 316
michael@0 317 this._sendToIdp(request, "assertion", trapAssertion.bind(this));
michael@0 318 },
michael@0 319
michael@0 320 /**
michael@0 321 * Packages a message and sends it to the IdP.
michael@0 322 * @param request (dictionary) the message to send
michael@0 323 * @param type (DOMString) the type of message (assertion/validation)
michael@0 324 * @param callback (function) the function to call with the results
michael@0 325 */
michael@0 326 _sendToIdp: function(request, type, callback) {
michael@0 327 request.origin = Cu.getWebIDLCallerPrincipal().origin;
michael@0 328 this._idpchannel.send(request, this._wrapCallback(type, callback));
michael@0 329 },
michael@0 330
michael@0 331 _reportIdpError: function(type, message) {
michael@0 332 let args = {};
michael@0 333 let msg = "";
michael@0 334 if (message.type === "ERROR") {
michael@0 335 msg = message.error;
michael@0 336 } else {
michael@0 337 msg = JSON.stringify(message.message);
michael@0 338 if (message.type === "LOGINNEEDED") {
michael@0 339 args.loginUrl = message.loginUrl;
michael@0 340 }
michael@0 341 }
michael@0 342 this.reportError(type, "received response of type '" +
michael@0 343 message.type + "' from IdP: " + msg, args);
michael@0 344 },
michael@0 345
michael@0 346 /**
michael@0 347 * Wraps a callback, adding a timeout and ensuring that the callback doesn't
michael@0 348 * receive any message other than one where the IdP generated a "SUCCESS"
michael@0 349 * response.
michael@0 350 */
michael@0 351 _wrapCallback: function(type, callback) {
michael@0 352 let timeout = this._win.setTimeout(function() {
michael@0 353 this.reportError(type, "IdP timeout for " + this._idpchannel + " " +
michael@0 354 (this._idpchannel.ready ? "[ready]" : "[not ready]"));
michael@0 355 timeout = null;
michael@0 356 callback(null);
michael@0 357 }.bind(this), this._timeout);
michael@0 358
michael@0 359 return function(message) {
michael@0 360 if (!timeout) {
michael@0 361 return;
michael@0 362 }
michael@0 363 this._win.clearTimeout(timeout);
michael@0 364 timeout = null;
michael@0 365
michael@0 366 let content = null;
michael@0 367 if (message.type === "SUCCESS") {
michael@0 368 content = message.message;
michael@0 369 } else {
michael@0 370 this._reportIdpError(type, message);
michael@0 371 }
michael@0 372 callback(content);
michael@0 373 }.bind(this);
michael@0 374 }
michael@0 375 };
michael@0 376
michael@0 377 this.PeerConnectionIdp = PeerConnectionIdp;

mercurial