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

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

mercurial