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");
16 * Creates an IdP helper.
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
23 function PeerConnectionIdp(window, timeout, warningFunc, dispatchEventFunc) {
25 this._timeout = timeout || 5000;
26 this._warning = warningFunc;
27 this._dispatchEvent = dispatchEventFunc;
29 this.assertion = null;
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");
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)) {
51 this._idpchannel.close();
53 this._idpchannel = new IdpProxy(provider, protocol);
57 this.assertion = null;
59 if (this._idpchannel) {
60 this._idpchannel.close();
61 this._idpchannel = null;
66 * Generate an error event of the identified type;
67 * and put a little more precise information in the console.
69 reportError: function(type, message, extra) {
72 protocol: this.protocol
75 Object.keys(extra).forEach(function(k) {
79 this._warning("RTC identity: " + message, null, 0);
80 let ev = new this._win.RTCPeerConnectionIdentityErrorEvent('idp' + type + 'error', args);
81 this._dispatchEvent(ev);
84 _getFingerprintsFromSdp: function(sdp) {
85 let fingerprints = {};
86 let m = sdp.match(PeerConnectionIdp._fingerprintPattern);
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);
93 return Object.keys(fingerprints).map(k => fingerprints[k]);
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);
104 assertion = JSON.parse(atob(idMatch[1]));
106 this.reportError("validation",
107 "invalid identity assertion: " + e);
109 if (typeof assertion.idp === "object" &&
110 typeof assertion.idp.domain === "string" &&
111 typeof assertion.assertion === "string") {
115 this.reportError("validation", "assertion missing" +
116 " idp/idp.domain/assertion");
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.
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) {
137 this.setIdentityProvider(identity.idp.domain, identity.idp.protocol);
138 this._verifyIdentity(identity.assertion, fingerprints, callback);
142 * Checks that the name in the identity provided by the IdP is OK.
144 * @param name (string) the name to validate
145 * @returns (string) an error message, iff the name isn't good
147 _validateName: function(name) {
148 if (typeof name !== "string") {
149 return "name not a string";
151 let atIdx = name.indexOf("@");
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);
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 + "'";
171 return "missing authority in name from IdP";
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) {
178 this.reportError("validation",
179 "assertion validation failure: " + msg);
182 let isSubsetOf = (outer, inner, cmp) => {
183 return inner.some(i => {
184 return !outer.some(o => cmp(i, o));
187 let compareFingerprints = (a, b) => {
188 return (a.digest === b.digest) && (a.algorithm === b.algorithm);
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");
199 let error = this._validateName(message.identity);
207 warn("invalid JSON in content");
213 * Asks the IdP proxy to verify an identity.
215 _verifyIdentity: function(assertion, fingerprints, callback) {
216 function onVerification(message) {
217 if (message && this._checkVerifyResponse(message, fingerprints)) {
220 this._warning("RTC identity: assertion validation failure", null, 0);
229 this._sendToIdp(request, "validation", onVerification.bind(this));
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.
238 appendIdentityToSDP: function(sdp, fingerprint, callback) {
239 let onAssertion = function() {
240 callback(this.wrapSdp(sdp), this.assertion);
243 if (!this._idpchannel || this.assertion) {
248 this._getIdentityAssertion(fingerprint, onAssertion);
252 * Inserts an identity assertion into the given SDP.
254 wrapSdp: function(sdp) {
255 if (!this.assertion) {
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);
266 getIdentityAssertion: function(fingerprint, callback) {
267 if (!this._idpchannel) {
268 this.reportError("assertion", "IdP not set");
273 this._getIdentityAssertion(fingerprint, callback);
276 _getIdentityAssertion: function(fingerprint, callback) {
277 let [algorithm, digest] = fingerprint.split(" ", 2);
280 algorithm: algorithm,
286 message: JSON.stringify(message),
287 username: this.username
290 // catch the assertion, clean it up, warn if absent
291 function trapAssertion(assertion) {
293 this._warning("RTC identity: assertion generation failure", null, 0);
294 this.assertion = null;
296 this.assertion = btoa(JSON.stringify(assertion));
298 callback(this.assertion);
301 this._sendToIdp(request, "assertion", trapAssertion.bind(this));
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
310 _sendToIdp: function(request, type, callback) {
311 request.origin = Cu.getWebIDLCallerPrincipal().origin;
312 this._idpchannel.send(request, this._wrapCallback(type, callback));
315 _reportIdpError: function(type, message) {
318 if (message.type === "ERROR") {
321 msg = JSON.stringify(message.message);
322 if (message.type === "LOGINNEEDED") {
323 args.loginUrl = message.loginUrl;
326 this.reportError(type, "received response of type '" +
327 message.type + "' from IdP: " + msg, args);
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"
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]"));
341 }.bind(this), this._timeout);
343 return function(message) {
347 this._win.clearTimeout(timeout);
351 if (message.type === "SUCCESS") {
352 content = message.message;
354 this._reportIdpError(type, message);
361 this.PeerConnectionIdp = PeerConnectionIdp;