Bumping manifests a=b2g-bump
[gecko.git] / dom / media / PeerConnectionIdp.jsm
blob7e55e2f1878f4e8ad38d8d8a71ada22a94f50c8e
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;
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   _getFingerprintsFromSdp: function(sdp) {
85     let fingerprints = {};
86     let m = sdp.match(PeerConnectionIdp._fingerprintPattern);
87     while (m) {
88       fingerprints[m[0]] = { algorithm: m[1], digest: m[2] };
89       sdp = sdp.substring(m.index + m[0].length);
90       m = sdp.match(PeerConnectionIdp._fingerprintPattern);
91     }
93     return Object.keys(fingerprints).map(k => fingerprints[k]);
94   },
96   _getIdentityFromSdp: function(sdp) {
97     // a=identity is session level
98     let mLineMatch = sdp.match(PeerConnectionIdp._mLinePattern);
99     let sessionLevel = sdp.substring(0, mLineMatch.index);
100     let idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern);
101     if (idMatch) {
102       let assertion = {};
103       try {
104         assertion = JSON.parse(atob(idMatch[1]));
105       } catch (e) {
106         this.reportError("validation",
107                          "invalid identity assertion: " + e);
108       } // for JSON.parse
109       if (typeof assertion.idp === "object" &&
110           typeof assertion.idp.domain === "string" &&
111           typeof assertion.assertion === "string") {
112         return assertion;
113       }
115       this.reportError("validation", "assertion missing" +
116                        " idp/idp.domain/assertion");
117     }
118     // undefined!
119   },
121   /**
122    * Queues a task to verify the a=identity line the given SDP contains, if any.
123    * If the verification succeeds callback is called with the message from the
124    * IdP proxy as parameter, else (verification failed OR no a=identity line in
125    * SDP at all) null is passed to callback.
126    */
127   verifyIdentityFromSDP: function(sdp, callback) {
128     let identity = this._getIdentityFromSdp(sdp);
129     let fingerprints = this._getFingerprintsFromSdp(sdp);
130     // it's safe to use the fingerprint we got from the SDP here,
131     // only because we ensure that there is only one
132     if (!identity || fingerprints.length <= 0) {
133       callback(null);
134       return;
135     }
137     this.setIdentityProvider(identity.idp.domain, identity.idp.protocol);
138     this._verifyIdentity(identity.assertion, fingerprints, callback);
139   },
141   /**
142    * Checks that the name in the identity provided by the IdP is OK.
143    *
144    * @param name (string) the name to validate
145    * @returns (string) an error message, iff the name isn't good
146    */
147   _validateName: function(name) {
148     if (typeof name !== "string") {
149       return "name not a string";
150     }
151     let atIdx = name.indexOf("@");
152     if (atIdx > 0) {
153       // no third party assertions... for now
154       let tail = name.substring(atIdx + 1);
156       // strip the port number, if present
157       let provider = this.provider;
158       let providerPortIdx = provider.indexOf(":");
159       if (providerPortIdx > 0) {
160         provider = provider.substring(0, providerPortIdx);
161       }
162       let idnService = Components.classes["@mozilla.org/network/idn-service;1"].
163         getService(Components.interfaces.nsIIDNService);
164       if (idnService.convertUTF8toACE(tail) !==
165           idnService.convertUTF8toACE(provider)) {
166         return "name '" + identity.name +
167             "' doesn't match IdP: '" + this.provider + "'";
168       }
169       return null;
170     }
171     return "missing authority in name from IdP";
172   },
174   // we are very defensive here when handling the message from the IdP
175   // proxy so that broken IdPs can only do as little harm as possible.
176   _checkVerifyResponse: function(message, fingerprints) {
177     let warn = msg => {
178       this.reportError("validation",
179                        "assertion validation failure: " + msg);
180     };
182     let isSubsetOf = (outer, inner, cmp) => {
183       return inner.some(i => {
184         return !outer.some(o => cmp(i, o));
185       });
186     };
187     let compareFingerprints = (a, b) => {
188       return (a.digest === b.digest) && (a.algorithm === b.algorithm);
189     };
191     try {
192       let contents = JSON.parse(message.contents);
193       if (!Array.isArray(contents.fingerprint)) {
194         warn("fingerprint is not an array");
195       } else if (isSubsetOf(contents.fingerprint, fingerprints,
196                             compareFingerprints)) {
197         warn("fingerprints in SDP aren't a subset of those in the assertion");
198       } else {
199         let error = this._validateName(message.identity);
200         if (error) {
201           warn(error);
202         } else {
203           return true;
204         }
205       }
206     } catch(e) {
207       warn("invalid JSON in content");
208     }
209     return false;
210   },
212   /**
213    * Asks the IdP proxy to verify an identity.
214    */
215   _verifyIdentity: function(assertion, fingerprints, callback) {
216     function onVerification(message) {
217       if (message && this._checkVerifyResponse(message, fingerprints)) {
218         callback(message);
219       } else {
220         this._warning("RTC identity: assertion validation failure", null, 0);
221         callback(null);
222       }
223     }
225     let request = {
226       type: "VERIFY",
227       message: assertion
228     };
229     this._sendToIdp(request, "validation", onVerification.bind(this));
230   },
232   /**
233    * Asks the IdP proxy for an identity assertion and, on success, enriches the
234    * given SDP with an a=identity line and calls callback with the new SDP as
235    * parameter. If no IdP is configured the original SDP (without a=identity
236    * line) is passed to the callback.
237    */
238   appendIdentityToSDP: function(sdp, fingerprint, callback) {
239     let onAssertion = function() {
240       callback(this.wrapSdp(sdp), this.assertion);
241     }.bind(this);
243     if (!this._idpchannel || this.assertion) {
244       onAssertion();
245       return;
246     }
248     this._getIdentityAssertion(fingerprint, onAssertion);
249   },
251   /**
252    * Inserts an identity assertion into the given SDP.
253    */
254   wrapSdp: function(sdp) {
255     if (!this.assertion) {
256       return sdp;
257     }
259     // yes, we assume that this matches; if it doesn't something is *wrong*
260     let match = sdp.match(PeerConnectionIdp._mLinePattern);
261     return sdp.substring(0, match.index) +
262       "a=identity:" + this.assertion + "\r\n" +
263       sdp.substring(match.index);
264   },
266   getIdentityAssertion: function(fingerprint, callback) {
267     if (!this._idpchannel) {
268       this.reportError("assertion", "IdP not set");
269       callback(null);
270       return;
271     }
273     this._getIdentityAssertion(fingerprint, callback);
274   },
276   _getIdentityAssertion: function(fingerprint, callback) {
277     let [algorithm, digest] = fingerprint.split(" ", 2);
278     let message = {
279       fingerprint: [{
280         algorithm: algorithm,
281         digest: digest
282       }]
283     };
284     let request = {
285       type: "SIGN",
286       message: JSON.stringify(message),
287       username: this.username
288     };
290     // catch the assertion, clean it up, warn if absent
291     function trapAssertion(assertion) {
292       if (!assertion) {
293         this._warning("RTC identity: assertion generation failure", null, 0);
294         this.assertion = null;
295       } else {
296         this.assertion = btoa(JSON.stringify(assertion));
297       }
298       callback(this.assertion);
299     }
301     this._sendToIdp(request, "assertion", trapAssertion.bind(this));
302   },
304   /**
305    * Packages a message and sends it to the IdP.
306    * @param request (dictionary) the message to send
307    * @param type (DOMString) the type of message (assertion/validation)
308    * @param callback (function) the function to call with the results
309    */
310   _sendToIdp: function(request, type, callback) {
311     request.origin = Cu.getWebIDLCallerPrincipal().origin;
312     this._idpchannel.send(request, this._wrapCallback(type, callback));
313   },
315   _reportIdpError: function(type, message) {
316     let args = {};
317     let msg = "";
318     if (message.type === "ERROR") {
319       msg = message.error;
320     } else {
321       msg = JSON.stringify(message.message);
322       if (message.type === "LOGINNEEDED") {
323         args.loginUrl = message.loginUrl;
324       }
325     }
326     this.reportError(type, "received response of type '" +
327                      message.type + "' from IdP: " + msg, args);
328   },
330   /**
331    * Wraps a callback, adding a timeout and ensuring that the callback doesn't
332    * receive any message other than one where the IdP generated a "SUCCESS"
333    * response.
334    */
335   _wrapCallback: function(type, callback) {
336     let timeout = this._win.setTimeout(function() {
337       this.reportError(type, "IdP timeout for " + this._idpchannel + " " +
338                        (this._idpchannel.ready ? "[ready]" : "[not ready]"));
339       timeout = null;
340       callback(null);
341     }.bind(this), this._timeout);
343     return function(message) {
344       if (!timeout) {
345         return;
346       }
347       this._win.clearTimeout(timeout);
348       timeout = null;
350       let content = null;
351       if (message.type === "SUCCESS") {
352         content = message.message;
353       } else {
354         this._reportIdpError(type, message);
355       }
356       callback(content);
357     }.bind(this);
358   }
361 this.PeerConnectionIdp = PeerConnectionIdp;