1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/media/IdpProxy.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,271 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +this.EXPORTED_SYMBOLS = ["IdpProxy"]; 1.11 + 1.12 +const { 1.13 + classes: Cc, 1.14 + interfaces: Ci, 1.15 + utils: Cu, 1.16 + results: Cr 1.17 +} = Components; 1.18 + 1.19 +Cu.import("resource://gre/modules/Services.jsm"); 1.20 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.21 + 1.22 +XPCOMUtils.defineLazyModuleGetter(this, "Sandbox", 1.23 + "resource://gre/modules/identity/Sandbox.jsm"); 1.24 + 1.25 +/** 1.26 + * An invisible iframe for hosting the idp shim. 1.27 + * 1.28 + * There is no visible UX here, as we assume the user has already 1.29 + * logged in elsewhere (on a different screen in the web site hosting 1.30 + * the RTC functions). 1.31 + */ 1.32 +function IdpChannel(uri, messageCallback) { 1.33 + this.sandbox = null; 1.34 + this.messagechannel = null; 1.35 + this.source = uri; 1.36 + this.messageCallback = messageCallback; 1.37 +} 1.38 + 1.39 +IdpChannel.prototype = { 1.40 + /** 1.41 + * Create a hidden, sandboxed iframe for hosting the IdP's js shim. 1.42 + * 1.43 + * @param callback 1.44 + * (function) invoked when this completes, with an error 1.45 + * argument if there is a problem, no argument if everything is 1.46 + * ok 1.47 + */ 1.48 + open: function(callback) { 1.49 + if (this.sandbox) { 1.50 + return callback(new Error("IdP channel already open")); 1.51 + } 1.52 + 1.53 + let ready = this._sandboxReady.bind(this, callback); 1.54 + this.sandbox = new Sandbox(this.source, ready); 1.55 + }, 1.56 + 1.57 + _sandboxReady: function(aCallback, aSandbox) { 1.58 + // Inject a message channel into the subframe. 1.59 + try { 1.60 + this.messagechannel = new aSandbox._frame.contentWindow.MessageChannel(); 1.61 + Object.defineProperty( 1.62 + aSandbox._frame.contentWindow.wrappedJSObject, 1.63 + "rtcwebIdentityPort", 1.64 + { 1.65 + value: this.messagechannel.port2 1.66 + } 1.67 + ); 1.68 + } catch (e) { 1.69 + this.close(); 1.70 + aCallback(e); // oops, the IdP proxy overwrote this.. bad 1.71 + return; 1.72 + } 1.73 + this.messagechannel.port1.onmessage = function(msg) { 1.74 + this.messageCallback(msg.data); 1.75 + }.bind(this); 1.76 + this.messagechannel.port1.start(); 1.77 + aCallback(); 1.78 + }, 1.79 + 1.80 + send: function(msg) { 1.81 + this.messagechannel.port1.postMessage(msg); 1.82 + }, 1.83 + 1.84 + close: function IdpChannel_close() { 1.85 + if (this.sandbox) { 1.86 + if (this.messagechannel) { 1.87 + this.messagechannel.port1.close(); 1.88 + } 1.89 + this.sandbox.free(); 1.90 + } 1.91 + this.messagechannel = null; 1.92 + this.sandbox = null; 1.93 + } 1.94 +}; 1.95 + 1.96 +/** 1.97 + * A message channel between the RTC PeerConnection and a designated IdP Proxy. 1.98 + * 1.99 + * @param domain (string) the domain to load up 1.100 + * @param protocol (string) Optional string for the IdP protocol 1.101 + */ 1.102 +function IdpProxy(domain, protocol) { 1.103 + IdpProxy.validateDomain(domain); 1.104 + IdpProxy.validateProtocol(protocol); 1.105 + 1.106 + this.domain = domain; 1.107 + this.protocol = protocol || "default"; 1.108 + 1.109 + this._reset(); 1.110 +} 1.111 + 1.112 +/** 1.113 + * Checks that the domain is only a domain, and doesn't contain anything else. 1.114 + * Adds it to a URI, then checks that it matches perfectly. 1.115 + */ 1.116 +IdpProxy.validateDomain = function(domain) { 1.117 + let message = "Invalid domain for identity provider; "; 1.118 + if (!domain || typeof domain !== "string") { 1.119 + throw new Error(message + "must be a non-zero length string"); 1.120 + } 1.121 + 1.122 + message += "must only have a domain name and optionally a port"; 1.123 + try { 1.124 + let ioService = Components.classes["@mozilla.org/network/io-service;1"] 1.125 + .getService(Components.interfaces.nsIIOService); 1.126 + let uri = ioService.newURI('https://' + domain + '/', null, null); 1.127 + 1.128 + // this should trap errors 1.129 + // we could check uri.userPass, uri.path and uri.ref, but there is no need 1.130 + if (uri.hostPort !== domain) { 1.131 + throw new Error(message); 1.132 + } 1.133 + } catch (e if (e.result === Cr.NS_ERROR_MALFORMED_URI)) { 1.134 + throw new Error(message); 1.135 + } 1.136 +}; 1.137 + 1.138 +/** 1.139 + * Checks that the IdP protocol is sane. In particular, we don't want someone 1.140 + * adding relative paths (e.g., "../../myuri"), which could be used to move 1.141 + * outside of /.well-known/ and into space that they control. 1.142 + */ 1.143 +IdpProxy.validateProtocol = function(protocol) { 1.144 + if (!protocol) { 1.145 + return; // falsy values turn into "default", so they are OK 1.146 + } 1.147 + let message = "Invalid protocol for identity provider; "; 1.148 + if (typeof protocol !== "string") { 1.149 + throw new Error(message + "must be a string"); 1.150 + } 1.151 + if (decodeURIComponent(protocol).match(/[\/\\]/)) { 1.152 + throw new Error(message + "must not include '/' or '\\'"); 1.153 + } 1.154 +}; 1.155 + 1.156 +IdpProxy.prototype = { 1.157 + _reset: function() { 1.158 + this.channel = null; 1.159 + this.ready = false; 1.160 + 1.161 + this.counter = 0; 1.162 + this.tracking = {}; 1.163 + this.pending = []; 1.164 + }, 1.165 + 1.166 + isSame: function(domain, protocol) { 1.167 + return this.domain === domain && ((protocol || "default") === this.protocol); 1.168 + }, 1.169 + 1.170 + /** 1.171 + * Get a sandboxed iframe for hosting the idp-proxy's js. Create a message 1.172 + * channel down to the frame. 1.173 + * 1.174 + * @param errorCallback (function) a callback that will be invoked if there 1.175 + * is a fatal error starting the proxy 1.176 + */ 1.177 + start: function(errorCallback) { 1.178 + if (this.channel) { 1.179 + return; 1.180 + } 1.181 + let well_known = "https://" + this.domain; 1.182 + well_known += "/.well-known/idp-proxy/" + this.protocol; 1.183 + this.channel = new IdpChannel(well_known, this._messageReceived.bind(this)); 1.184 + this.channel.open(function(error) { 1.185 + if (error) { 1.186 + this.close(); 1.187 + if (typeof errorCallback === "function") { 1.188 + errorCallback(error); 1.189 + } 1.190 + } 1.191 + }.bind(this)); 1.192 + }, 1.193 + 1.194 + /** 1.195 + * Send a message up to the idp proxy. This should be an RTC "SIGN" or 1.196 + * "VERIFY" message. This method adds the tracking 'id' parameter 1.197 + * automatically to the message so that the callback is only invoked for the 1.198 + * response to the message. 1.199 + * 1.200 + * This enqueues the message to send if the IdP hasn't signaled that it is 1.201 + * "READY", and sends the message when it is. 1.202 + * 1.203 + * The caller is responsible for ensuring that a response is received. If the 1.204 + * IdP doesn't respond, the callback simply isn't invoked. 1.205 + */ 1.206 + send: function(message, callback) { 1.207 + this.start(); 1.208 + if (this.ready) { 1.209 + message.id = "" + (++this.counter); 1.210 + this.tracking[message.id] = callback; 1.211 + this.channel.send(message); 1.212 + } else { 1.213 + this.pending.push({ message: message, callback: callback }); 1.214 + } 1.215 + }, 1.216 + 1.217 + /** 1.218 + * Handle a message from the IdP. This automatically sends if the message is 1.219 + * 'READY' so there is no need to track readiness state outside of this obj. 1.220 + */ 1.221 + _messageReceived: function(message) { 1.222 + if (!message) { 1.223 + return; 1.224 + } 1.225 + if (!this.ready && message.type === "READY") { 1.226 + this.ready = true; 1.227 + this.pending.forEach(function(p) { 1.228 + this.send(p.message, p.callback); 1.229 + }, this); 1.230 + this.pending = []; 1.231 + } else if (this.tracking[message.id]) { 1.232 + var callback = this.tracking[message.id]; 1.233 + delete this.tracking[message.id]; 1.234 + callback(message); 1.235 + } else { 1.236 + let console = Cc["@mozilla.org/consoleservice;1"]. 1.237 + getService(Ci.nsIConsoleService); 1.238 + console.logStringMessage("Received bad message from IdP: " + 1.239 + message.id + ":" + message.type); 1.240 + } 1.241 + }, 1.242 + 1.243 + /** 1.244 + * Performs cleanup. The object should be OK to use again. 1.245 + */ 1.246 + close: function() { 1.247 + if (!this.channel) { 1.248 + return; 1.249 + } 1.250 + 1.251 + // clear out before letting others know in case they do something bad 1.252 + let trackingCopy = this.tracking; 1.253 + let pendingCopy = this.pending; 1.254 + 1.255 + this.channel.close(); 1.256 + this._reset(); 1.257 + 1.258 + // dump a message of type "ERROR" in response to all outstanding 1.259 + // messages to the IdP 1.260 + let error = { type: "ERROR", error: "IdP closed" }; 1.261 + Object.keys(trackingCopy).forEach(function(k) { 1.262 + trackingCopy[k](error); 1.263 + }); 1.264 + pendingCopy.forEach(function(p) { 1.265 + p.callback(error); 1.266 + }); 1.267 + }, 1.268 + 1.269 + toString: function() { 1.270 + return this.domain + '/.../' + this.protocol; 1.271 + } 1.272 +}; 1.273 + 1.274 +this.IdpProxy = IdpProxy;