|
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/. */ |
|
5 |
|
6 this.EXPORTED_SYMBOLS = ["PeerConnectionIdp"]; |
|
7 |
|
8 const {classes: Cc, interfaces: Ci, utils: Cu} = Components; |
|
9 |
|
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"); |
|
14 |
|
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; |
|
28 |
|
29 this.assertion = null; |
|
30 this.provider = null; |
|
31 } |
|
32 |
|
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 })(); |
|
41 |
|
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 }, |
|
55 |
|
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 }, |
|
64 |
|
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 }, |
|
83 |
|
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); |
|
101 |
|
102 let sessionLevel = attributes.shift(); |
|
103 attributes = attributes.map(function(sectionLevel) { |
|
104 return sectionLevel || sessionLevel; |
|
105 }); |
|
106 |
|
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 } |
|
113 |
|
114 if (typeof first === "object" && attributes.every(sameAsFirst)) { |
|
115 return first; |
|
116 } |
|
117 // undefined! |
|
118 }, |
|
119 |
|
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 } |
|
138 |
|
139 this.reportError("validation", "assertion missing" + |
|
140 " idp/idp.domain/assertion"); |
|
141 } |
|
142 // undefined! |
|
143 }, |
|
144 |
|
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 } |
|
160 |
|
161 this.setIdentityProvider(identity.idp.domain, identity.idp.protocol); |
|
162 this._verifyIdentity(identity.assertion, fingerprint, callback); |
|
163 }, |
|
164 |
|
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); |
|
179 |
|
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 }, |
|
197 |
|
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); |
|
205 |
|
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 }, |
|
226 |
|
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 } |
|
240 |
|
241 let request = { |
|
242 type: "VERIFY", |
|
243 message: assertion |
|
244 }; |
|
245 this._sendToIdp(request, "validation", onVerification.bind(this)); |
|
246 }, |
|
247 |
|
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); |
|
258 |
|
259 if (!this._idpchannel || this.assertion) { |
|
260 onAssertion(); |
|
261 return; |
|
262 } |
|
263 |
|
264 this._getIdentityAssertion(fingerprint, onAssertion); |
|
265 }, |
|
266 |
|
267 /** |
|
268 * Inserts an identity assertion into the given SDP. |
|
269 */ |
|
270 wrapSdp: function(sdp) { |
|
271 if (!this.assertion) { |
|
272 return sdp; |
|
273 } |
|
274 |
|
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 }, |
|
281 |
|
282 getIdentityAssertion: function(fingerprint, callback) { |
|
283 if (!this._idpchannel) { |
|
284 this.reportError("assertion", "IdP not set"); |
|
285 callback(null); |
|
286 return; |
|
287 } |
|
288 |
|
289 this._getIdentityAssertion(fingerprint, callback); |
|
290 }, |
|
291 |
|
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 }; |
|
305 |
|
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 } |
|
316 |
|
317 this._sendToIdp(request, "assertion", trapAssertion.bind(this)); |
|
318 }, |
|
319 |
|
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 }, |
|
330 |
|
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 }, |
|
345 |
|
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); |
|
358 |
|
359 return function(message) { |
|
360 if (!timeout) { |
|
361 return; |
|
362 } |
|
363 this._win.clearTimeout(timeout); |
|
364 timeout = null; |
|
365 |
|
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 }; |
|
376 |
|
377 this.PeerConnectionIdp = PeerConnectionIdp; |