Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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;