|
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/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 this.EXPORTED_SYMBOLS = ["IdpProxy"]; |
|
8 |
|
9 const { |
|
10 classes: Cc, |
|
11 interfaces: Ci, |
|
12 utils: Cu, |
|
13 results: Cr |
|
14 } = Components; |
|
15 |
|
16 Cu.import("resource://gre/modules/Services.jsm"); |
|
17 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
18 |
|
19 XPCOMUtils.defineLazyModuleGetter(this, "Sandbox", |
|
20 "resource://gre/modules/identity/Sandbox.jsm"); |
|
21 |
|
22 /** |
|
23 * An invisible iframe for hosting the idp shim. |
|
24 * |
|
25 * There is no visible UX here, as we assume the user has already |
|
26 * logged in elsewhere (on a different screen in the web site hosting |
|
27 * the RTC functions). |
|
28 */ |
|
29 function IdpChannel(uri, messageCallback) { |
|
30 this.sandbox = null; |
|
31 this.messagechannel = null; |
|
32 this.source = uri; |
|
33 this.messageCallback = messageCallback; |
|
34 } |
|
35 |
|
36 IdpChannel.prototype = { |
|
37 /** |
|
38 * Create a hidden, sandboxed iframe for hosting the IdP's js shim. |
|
39 * |
|
40 * @param callback |
|
41 * (function) invoked when this completes, with an error |
|
42 * argument if there is a problem, no argument if everything is |
|
43 * ok |
|
44 */ |
|
45 open: function(callback) { |
|
46 if (this.sandbox) { |
|
47 return callback(new Error("IdP channel already open")); |
|
48 } |
|
49 |
|
50 let ready = this._sandboxReady.bind(this, callback); |
|
51 this.sandbox = new Sandbox(this.source, ready); |
|
52 }, |
|
53 |
|
54 _sandboxReady: function(aCallback, aSandbox) { |
|
55 // Inject a message channel into the subframe. |
|
56 try { |
|
57 this.messagechannel = new aSandbox._frame.contentWindow.MessageChannel(); |
|
58 Object.defineProperty( |
|
59 aSandbox._frame.contentWindow.wrappedJSObject, |
|
60 "rtcwebIdentityPort", |
|
61 { |
|
62 value: this.messagechannel.port2 |
|
63 } |
|
64 ); |
|
65 } catch (e) { |
|
66 this.close(); |
|
67 aCallback(e); // oops, the IdP proxy overwrote this.. bad |
|
68 return; |
|
69 } |
|
70 this.messagechannel.port1.onmessage = function(msg) { |
|
71 this.messageCallback(msg.data); |
|
72 }.bind(this); |
|
73 this.messagechannel.port1.start(); |
|
74 aCallback(); |
|
75 }, |
|
76 |
|
77 send: function(msg) { |
|
78 this.messagechannel.port1.postMessage(msg); |
|
79 }, |
|
80 |
|
81 close: function IdpChannel_close() { |
|
82 if (this.sandbox) { |
|
83 if (this.messagechannel) { |
|
84 this.messagechannel.port1.close(); |
|
85 } |
|
86 this.sandbox.free(); |
|
87 } |
|
88 this.messagechannel = null; |
|
89 this.sandbox = null; |
|
90 } |
|
91 }; |
|
92 |
|
93 /** |
|
94 * A message channel between the RTC PeerConnection and a designated IdP Proxy. |
|
95 * |
|
96 * @param domain (string) the domain to load up |
|
97 * @param protocol (string) Optional string for the IdP protocol |
|
98 */ |
|
99 function IdpProxy(domain, protocol) { |
|
100 IdpProxy.validateDomain(domain); |
|
101 IdpProxy.validateProtocol(protocol); |
|
102 |
|
103 this.domain = domain; |
|
104 this.protocol = protocol || "default"; |
|
105 |
|
106 this._reset(); |
|
107 } |
|
108 |
|
109 /** |
|
110 * Checks that the domain is only a domain, and doesn't contain anything else. |
|
111 * Adds it to a URI, then checks that it matches perfectly. |
|
112 */ |
|
113 IdpProxy.validateDomain = function(domain) { |
|
114 let message = "Invalid domain for identity provider; "; |
|
115 if (!domain || typeof domain !== "string") { |
|
116 throw new Error(message + "must be a non-zero length string"); |
|
117 } |
|
118 |
|
119 message += "must only have a domain name and optionally a port"; |
|
120 try { |
|
121 let ioService = Components.classes["@mozilla.org/network/io-service;1"] |
|
122 .getService(Components.interfaces.nsIIOService); |
|
123 let uri = ioService.newURI('https://' + domain + '/', null, null); |
|
124 |
|
125 // this should trap errors |
|
126 // we could check uri.userPass, uri.path and uri.ref, but there is no need |
|
127 if (uri.hostPort !== domain) { |
|
128 throw new Error(message); |
|
129 } |
|
130 } catch (e if (e.result === Cr.NS_ERROR_MALFORMED_URI)) { |
|
131 throw new Error(message); |
|
132 } |
|
133 }; |
|
134 |
|
135 /** |
|
136 * Checks that the IdP protocol is sane. In particular, we don't want someone |
|
137 * adding relative paths (e.g., "../../myuri"), which could be used to move |
|
138 * outside of /.well-known/ and into space that they control. |
|
139 */ |
|
140 IdpProxy.validateProtocol = function(protocol) { |
|
141 if (!protocol) { |
|
142 return; // falsy values turn into "default", so they are OK |
|
143 } |
|
144 let message = "Invalid protocol for identity provider; "; |
|
145 if (typeof protocol !== "string") { |
|
146 throw new Error(message + "must be a string"); |
|
147 } |
|
148 if (decodeURIComponent(protocol).match(/[\/\\]/)) { |
|
149 throw new Error(message + "must not include '/' or '\\'"); |
|
150 } |
|
151 }; |
|
152 |
|
153 IdpProxy.prototype = { |
|
154 _reset: function() { |
|
155 this.channel = null; |
|
156 this.ready = false; |
|
157 |
|
158 this.counter = 0; |
|
159 this.tracking = {}; |
|
160 this.pending = []; |
|
161 }, |
|
162 |
|
163 isSame: function(domain, protocol) { |
|
164 return this.domain === domain && ((protocol || "default") === this.protocol); |
|
165 }, |
|
166 |
|
167 /** |
|
168 * Get a sandboxed iframe for hosting the idp-proxy's js. Create a message |
|
169 * channel down to the frame. |
|
170 * |
|
171 * @param errorCallback (function) a callback that will be invoked if there |
|
172 * is a fatal error starting the proxy |
|
173 */ |
|
174 start: function(errorCallback) { |
|
175 if (this.channel) { |
|
176 return; |
|
177 } |
|
178 let well_known = "https://" + this.domain; |
|
179 well_known += "/.well-known/idp-proxy/" + this.protocol; |
|
180 this.channel = new IdpChannel(well_known, this._messageReceived.bind(this)); |
|
181 this.channel.open(function(error) { |
|
182 if (error) { |
|
183 this.close(); |
|
184 if (typeof errorCallback === "function") { |
|
185 errorCallback(error); |
|
186 } |
|
187 } |
|
188 }.bind(this)); |
|
189 }, |
|
190 |
|
191 /** |
|
192 * Send a message up to the idp proxy. This should be an RTC "SIGN" or |
|
193 * "VERIFY" message. This method adds the tracking 'id' parameter |
|
194 * automatically to the message so that the callback is only invoked for the |
|
195 * response to the message. |
|
196 * |
|
197 * This enqueues the message to send if the IdP hasn't signaled that it is |
|
198 * "READY", and sends the message when it is. |
|
199 * |
|
200 * The caller is responsible for ensuring that a response is received. If the |
|
201 * IdP doesn't respond, the callback simply isn't invoked. |
|
202 */ |
|
203 send: function(message, callback) { |
|
204 this.start(); |
|
205 if (this.ready) { |
|
206 message.id = "" + (++this.counter); |
|
207 this.tracking[message.id] = callback; |
|
208 this.channel.send(message); |
|
209 } else { |
|
210 this.pending.push({ message: message, callback: callback }); |
|
211 } |
|
212 }, |
|
213 |
|
214 /** |
|
215 * Handle a message from the IdP. This automatically sends if the message is |
|
216 * 'READY' so there is no need to track readiness state outside of this obj. |
|
217 */ |
|
218 _messageReceived: function(message) { |
|
219 if (!message) { |
|
220 return; |
|
221 } |
|
222 if (!this.ready && message.type === "READY") { |
|
223 this.ready = true; |
|
224 this.pending.forEach(function(p) { |
|
225 this.send(p.message, p.callback); |
|
226 }, this); |
|
227 this.pending = []; |
|
228 } else if (this.tracking[message.id]) { |
|
229 var callback = this.tracking[message.id]; |
|
230 delete this.tracking[message.id]; |
|
231 callback(message); |
|
232 } else { |
|
233 let console = Cc["@mozilla.org/consoleservice;1"]. |
|
234 getService(Ci.nsIConsoleService); |
|
235 console.logStringMessage("Received bad message from IdP: " + |
|
236 message.id + ":" + message.type); |
|
237 } |
|
238 }, |
|
239 |
|
240 /** |
|
241 * Performs cleanup. The object should be OK to use again. |
|
242 */ |
|
243 close: function() { |
|
244 if (!this.channel) { |
|
245 return; |
|
246 } |
|
247 |
|
248 // clear out before letting others know in case they do something bad |
|
249 let trackingCopy = this.tracking; |
|
250 let pendingCopy = this.pending; |
|
251 |
|
252 this.channel.close(); |
|
253 this._reset(); |
|
254 |
|
255 // dump a message of type "ERROR" in response to all outstanding |
|
256 // messages to the IdP |
|
257 let error = { type: "ERROR", error: "IdP closed" }; |
|
258 Object.keys(trackingCopy).forEach(function(k) { |
|
259 trackingCopy[k](error); |
|
260 }); |
|
261 pendingCopy.forEach(function(p) { |
|
262 p.callback(error); |
|
263 }); |
|
264 }, |
|
265 |
|
266 toString: function() { |
|
267 return this.domain + '/.../' + this.protocol; |
|
268 } |
|
269 }; |
|
270 |
|
271 this.IdpProxy = IdpProxy; |