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