Bug 1572460 - Refactor `selection` out of the `InspectorFront`. r=yulia
[gecko.git] / dom / media / PeerConnectionIdp.jsm
blobff123839ce06873e772db2f090b2d740e5576624
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 var EXPORTED_SYMBOLS = ["PeerConnectionIdp"];
8 ChromeUtils.defineModuleGetter(
9   this,
10   "IdpSandbox",
11   "resource://gre/modules/media/IdpSandbox.jsm"
14 /**
15  * Creates an IdP helper.
16  *
17  * @param win (object) the window we are working for
18  * @param timeout (int) the timeout in milliseconds
19  */
20 function PeerConnectionIdp(win, timeout) {
21   this._win = win;
22   this._timeout = timeout || 5000;
24   this.provider = null;
25   this._resetAssertion();
28 (function() {
29   PeerConnectionIdp._mLinePattern = new RegExp("^m=", "m");
30   // attributes are funny, the 'a' is case sensitive, the name isn't
31   let pattern = "^a=[iI][dD][eE][nN][tT][iI][tT][yY]:(\\S+)";
32   PeerConnectionIdp._identityPattern = new RegExp(pattern, "m");
33   pattern = "^a=[fF][iI][nN][gG][eE][rR][pP][rR][iI][nN][tT]:(\\S+) (\\S+)";
34   PeerConnectionIdp._fingerprintPattern = new RegExp(pattern, "m");
35 })();
37 PeerConnectionIdp.prototype = {
38   get enabled() {
39     return !!this._idp;
40   },
42   _resetAssertion() {
43     this.assertion = null;
44     this.idpLoginUrl = null;
45   },
47   setIdentityProvider(provider, protocol, usernameHint, peerIdentity) {
48     this._resetAssertion();
49     this.provider = provider;
50     this.protocol = protocol;
51     this.username = usernameHint;
52     this.peeridentity = peerIdentity;
53     if (this._idp) {
54       if (this._idp.isSame(provider, protocol)) {
55         return; // noop
56       }
57       this._idp.stop();
58     }
59     this._idp = new IdpSandbox(provider, protocol, this._win);
60   },
62   // start the IdP and do some error fixup
63   start() {
64     return this._idp.start().catch(e => {
65       throw new this._win.DOMException(e.message, "IdpError");
66     });
67   },
69   close() {
70     this._resetAssertion();
71     this.provider = null;
72     this.protocol = null;
73     this.username = null;
74     this.peeridentity = null;
75     if (this._idp) {
76       this._idp.stop();
77       this._idp = null;
78     }
79   },
81   _getFingerprintsFromSdp(sdp) {
82     let fingerprints = {};
83     let m = sdp.match(PeerConnectionIdp._fingerprintPattern);
84     while (m) {
85       fingerprints[m[0]] = { algorithm: m[1], digest: m[2] };
86       sdp = sdp.substring(m.index + m[0].length);
87       m = sdp.match(PeerConnectionIdp._fingerprintPattern);
88     }
90     return Object.keys(fingerprints).map(k => fingerprints[k]);
91   },
93   _isValidAssertion(assertion) {
94     return (
95       assertion &&
96       assertion.idp &&
97       typeof assertion.idp.domain === "string" &&
98       (!assertion.idp.protocol || typeof assertion.idp.protocol === "string") &&
99       typeof assertion.assertion === "string"
100     );
101   },
103   _getSessionLevelEnd(sdp) {
104     const match = sdp.match(PeerConnectionIdp._mLinePattern);
105     if (!match) {
106       return sdp.length;
107     }
108     return match.index;
109   },
111   _getIdentityFromSdp(sdp) {
112     // a=identity is session level
113     let idMatch;
114     const index = this._getSessionLevelEnd(sdp);
115     const sessionLevel = sdp.substring(0, index);
116     idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern);
117     if (!idMatch) {
118       return undefined; // undefined === no identity
119     }
121     let assertion;
122     try {
123       assertion = JSON.parse(atob(idMatch[1]));
124     } catch (e) {
125       throw new this._win.DOMException(
126         "invalid identity assertion: " + e,
127         "InvalidSessionDescriptionError"
128       );
129     }
130     if (!this._isValidAssertion(assertion)) {
131       throw new this._win.DOMException(
132         "assertion missing idp/idp.domain/assertion",
133         "InvalidSessionDescriptionError"
134       );
135     }
136     return assertion;
137   },
139   /**
140    * Verifies the a=identity line the given SDP contains, if any.
141    * If the verification succeeds callback is called with the message from the
142    * IdP proxy as parameter, else (verification failed OR no a=identity line in
143    * SDP at all) null is passed to callback.
144    *
145    * Note that this only verifies that the SDP is coherent.  We still rely on
146    * the fact that the RTCPeerConnection won't connect to a peer if the
147    * fingerprint of the certificate they offer doesn't appear in the SDP.
148    */
149   verifyIdentityFromSDP(sdp, origin) {
150     let identity = this._getIdentityFromSdp(sdp);
151     let fingerprints = this._getFingerprintsFromSdp(sdp);
152     if (!identity || fingerprints.length <= 0) {
153       return this._win.Promise.resolve(); // undefined result = no identity
154     }
156     this.setIdentityProvider(identity.idp.domain, identity.idp.protocol);
157     return this._verifyIdentity(identity.assertion, fingerprints, origin);
158   },
160   /**
161    * Checks that the name in the identity provided by the IdP is OK.
162    *
163    * @param name (string) the name to validate
164    * @throws if the name isn't valid
165    */
166   _validateName(name) {
167     let error = msg => {
168       throw new this._win.DOMException(
169         "assertion name error: " + msg,
170         "IdpError"
171       );
172     };
174     if (typeof name !== "string") {
175       error("name not a string");
176     }
177     let atIdx = name.indexOf("@");
178     if (atIdx <= 0) {
179       error("missing authority in name from IdP");
180     }
182     // no third party assertions... for now
183     let tail = name.substring(atIdx + 1);
185     // strip the port number, if present
186     let provider = this.provider;
187     let providerPortIdx = provider.indexOf(":");
188     if (providerPortIdx > 0) {
189       provider = provider.substring(0, providerPortIdx);
190     }
191     let idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
192       Ci.nsIIDNService
193     );
194     if (
195       idnService.convertUTF8toACE(tail) !==
196       idnService.convertUTF8toACE(provider)
197     ) {
198       error('name "' + name + '" doesn\'t match IdP: "' + this.provider + '"');
199     }
200   },
202   /**
203    * Check the validation response.  We are very defensive here when handling
204    * the message from the IdP proxy.  That way, broken IdPs aren't likely to
205    * cause catastrophic damage.
206    */
207   _checkValidation(validation, sdpFingerprints) {
208     let error = msg => {
209       throw new this._win.DOMException(
210         "IdP validation error: " + msg,
211         "IdpError"
212       );
213     };
215     if (!this.provider) {
216       error("IdP closed");
217     }
219     if (
220       typeof validation !== "object" ||
221       typeof validation.contents !== "string" ||
222       typeof validation.identity !== "string"
223     ) {
224       error("no payload in validation response");
225     }
227     let fingerprints;
228     try {
229       fingerprints = JSON.parse(validation.contents).fingerprint;
230     } catch (e) {
231       error("invalid JSON");
232     }
234     let isFingerprint = f =>
235       typeof f.digest === "string" && typeof f.algorithm === "string";
236     if (!Array.isArray(fingerprints) || !fingerprints.every(isFingerprint)) {
237       error(
238         "fingerprints must be an array of objects" +
239           " with digest and algorithm attributes"
240       );
241     }
243     // everything in `innerSet` is found in `outerSet`
244     let isSubsetOf = (outerSet, innerSet, comparator) => {
245       return innerSet.every(i => {
246         return outerSet.some(o => comparator(i, o));
247       });
248     };
249     let compareFingerprints = (a, b) => {
250       return a.digest === b.digest && a.algorithm === b.algorithm;
251     };
252     if (!isSubsetOf(fingerprints, sdpFingerprints, compareFingerprints)) {
253       error("the fingerprints must be covered by the assertion");
254     }
255     this._validateName(validation.identity);
256     return validation;
257   },
259   /**
260    * Asks the IdP proxy to verify an identity assertion.
261    */
262   _verifyIdentity(assertion, fingerprints, origin) {
263     let p = this.start()
264       .then(idp =>
265         this._wrapCrossCompartmentPromise(
266           idp.validateAssertion(assertion, origin)
267         )
268       )
269       .then(validation => this._checkValidation(validation, fingerprints));
271     return this._applyTimeout(p);
272   },
274   /**
275    * Enriches the given SDP with an `a=identity` line.  getIdentityAssertion()
276    * must have already run successfully, otherwise this does nothing to the sdp.
277    */
278   addIdentityAttribute(sdp) {
279     if (!this.assertion) {
280       return sdp;
281     }
283     const index = this._getSessionLevelEnd(sdp);
284     return (
285       sdp.substring(0, index) +
286       "a=identity:" +
287       this.assertion +
288       "\r\n" +
289       sdp.substring(index)
290     );
291   },
293   /**
294    * Asks the IdP proxy for an identity assertion.  Don't call this unless you
295    * have checked .enabled, or you really like exceptions.  Also, don't call
296    * this when another call is still running, because it's not certain which
297    * call will finish first and the final state will be similarly uncertain.
298    */
299   getIdentityAssertion(fingerprint, origin) {
300     if (!this.enabled) {
301       throw new this._win.DOMException(
302         "no IdP set, call setIdentityProvider() to set one",
303         "InvalidStateError"
304       );
305     }
307     let [algorithm, digest] = fingerprint.split(" ", 2);
308     let content = {
309       fingerprint: [
310         {
311           algorithm,
312           digest,
313         },
314       ],
315     };
317     this._resetAssertion();
318     let p = this.start()
319       .then(idp => {
320         let options = {
321           protocol: this.protocol,
322           usernameHint: this.username,
323           peerIdentity: this.peeridentity,
324         };
325         return this._wrapCrossCompartmentPromise(
326           idp.generateAssertion(JSON.stringify(content), origin, options)
327         );
328       })
329       .then(assertion => {
330         if (!this._isValidAssertion(assertion)) {
331           throw new this._win.DOMException(
332             "IdP generated invalid assertion",
333             "IdpError"
334           );
335         }
336         // save the base64+JSON assertion, since that is all that is used
337         this.assertion = btoa(JSON.stringify(assertion));
338         return this.assertion;
339       });
341     return this._applyTimeout(p);
342   },
344   /**
345    * Promises generated by the sandbox need to be very carefully treated so that
346    * they can chain into promises in the `this._win` compartment.  Results need
347    * to be cloned across; errors need to be converted.
348    */
349   _wrapCrossCompartmentPromise(sandboxPromise) {
350     return new this._win.Promise((resolve, reject) => {
351       sandboxPromise.then(
352         result => resolve(Cu.cloneInto(result, this._win)),
353         e => {
354           let message = "" + (e.message || JSON.stringify(e) || "IdP error");
355           if (e.name === "IdpLoginError") {
356             if (typeof e.loginUrl === "string") {
357               this.idpLoginUrl = e.loginUrl;
358             }
359             reject(new this._win.DOMException(message, "IdpLoginError"));
360           } else {
361             reject(new this._win.DOMException(message, "IdpError"));
362           }
363         }
364       );
365     });
366   },
368   /**
369    * Wraps a promise, adding a timeout guard on it so that it can't take longer
370    * than the specified time.  Returns a promise that rejects if the timeout
371    * elapses before `p` resolves.
372    */
373   _applyTimeout(p) {
374     let timeout = new this._win.Promise(r =>
375       this._win.setTimeout(r, this._timeout)
376     ).then(() => {
377       throw new this._win.DOMException("IdP timed out", "IdpError");
378     });
379     return this._win.Promise.race([timeout, p]);
380   },