dom/media/IdpProxy.jsm

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:3f196e968c16
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;

mercurial