michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["IdpProxy"]; michael@0: michael@0: const { michael@0: classes: Cc, michael@0: interfaces: Ci, michael@0: utils: Cu, michael@0: results: Cr michael@0: } = Components; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Sandbox", michael@0: "resource://gre/modules/identity/Sandbox.jsm"); michael@0: michael@0: /** michael@0: * An invisible iframe for hosting the idp shim. michael@0: * michael@0: * There is no visible UX here, as we assume the user has already michael@0: * logged in elsewhere (on a different screen in the web site hosting michael@0: * the RTC functions). michael@0: */ michael@0: function IdpChannel(uri, messageCallback) { michael@0: this.sandbox = null; michael@0: this.messagechannel = null; michael@0: this.source = uri; michael@0: this.messageCallback = messageCallback; michael@0: } michael@0: michael@0: IdpChannel.prototype = { michael@0: /** michael@0: * Create a hidden, sandboxed iframe for hosting the IdP's js shim. michael@0: * michael@0: * @param callback michael@0: * (function) invoked when this completes, with an error michael@0: * argument if there is a problem, no argument if everything is michael@0: * ok michael@0: */ michael@0: open: function(callback) { michael@0: if (this.sandbox) { michael@0: return callback(new Error("IdP channel already open")); michael@0: } michael@0: michael@0: let ready = this._sandboxReady.bind(this, callback); michael@0: this.sandbox = new Sandbox(this.source, ready); michael@0: }, michael@0: michael@0: _sandboxReady: function(aCallback, aSandbox) { michael@0: // Inject a message channel into the subframe. michael@0: try { michael@0: this.messagechannel = new aSandbox._frame.contentWindow.MessageChannel(); michael@0: Object.defineProperty( michael@0: aSandbox._frame.contentWindow.wrappedJSObject, michael@0: "rtcwebIdentityPort", michael@0: { michael@0: value: this.messagechannel.port2 michael@0: } michael@0: ); michael@0: } catch (e) { michael@0: this.close(); michael@0: aCallback(e); // oops, the IdP proxy overwrote this.. bad michael@0: return; michael@0: } michael@0: this.messagechannel.port1.onmessage = function(msg) { michael@0: this.messageCallback(msg.data); michael@0: }.bind(this); michael@0: this.messagechannel.port1.start(); michael@0: aCallback(); michael@0: }, michael@0: michael@0: send: function(msg) { michael@0: this.messagechannel.port1.postMessage(msg); michael@0: }, michael@0: michael@0: close: function IdpChannel_close() { michael@0: if (this.sandbox) { michael@0: if (this.messagechannel) { michael@0: this.messagechannel.port1.close(); michael@0: } michael@0: this.sandbox.free(); michael@0: } michael@0: this.messagechannel = null; michael@0: this.sandbox = null; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * A message channel between the RTC PeerConnection and a designated IdP Proxy. michael@0: * michael@0: * @param domain (string) the domain to load up michael@0: * @param protocol (string) Optional string for the IdP protocol michael@0: */ michael@0: function IdpProxy(domain, protocol) { michael@0: IdpProxy.validateDomain(domain); michael@0: IdpProxy.validateProtocol(protocol); michael@0: michael@0: this.domain = domain; michael@0: this.protocol = protocol || "default"; michael@0: michael@0: this._reset(); michael@0: } michael@0: michael@0: /** michael@0: * Checks that the domain is only a domain, and doesn't contain anything else. michael@0: * Adds it to a URI, then checks that it matches perfectly. michael@0: */ michael@0: IdpProxy.validateDomain = function(domain) { michael@0: let message = "Invalid domain for identity provider; "; michael@0: if (!domain || typeof domain !== "string") { michael@0: throw new Error(message + "must be a non-zero length string"); michael@0: } michael@0: michael@0: message += "must only have a domain name and optionally a port"; michael@0: try { michael@0: let ioService = Components.classes["@mozilla.org/network/io-service;1"] michael@0: .getService(Components.interfaces.nsIIOService); michael@0: let uri = ioService.newURI('https://' + domain + '/', null, null); michael@0: michael@0: // this should trap errors michael@0: // we could check uri.userPass, uri.path and uri.ref, but there is no need michael@0: if (uri.hostPort !== domain) { michael@0: throw new Error(message); michael@0: } michael@0: } catch (e if (e.result === Cr.NS_ERROR_MALFORMED_URI)) { michael@0: throw new Error(message); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Checks that the IdP protocol is sane. In particular, we don't want someone michael@0: * adding relative paths (e.g., "../../myuri"), which could be used to move michael@0: * outside of /.well-known/ and into space that they control. michael@0: */ michael@0: IdpProxy.validateProtocol = function(protocol) { michael@0: if (!protocol) { michael@0: return; // falsy values turn into "default", so they are OK michael@0: } michael@0: let message = "Invalid protocol for identity provider; "; michael@0: if (typeof protocol !== "string") { michael@0: throw new Error(message + "must be a string"); michael@0: } michael@0: if (decodeURIComponent(protocol).match(/[\/\\]/)) { michael@0: throw new Error(message + "must not include '/' or '\\'"); michael@0: } michael@0: }; michael@0: michael@0: IdpProxy.prototype = { michael@0: _reset: function() { michael@0: this.channel = null; michael@0: this.ready = false; michael@0: michael@0: this.counter = 0; michael@0: this.tracking = {}; michael@0: this.pending = []; michael@0: }, michael@0: michael@0: isSame: function(domain, protocol) { michael@0: return this.domain === domain && ((protocol || "default") === this.protocol); michael@0: }, michael@0: michael@0: /** michael@0: * Get a sandboxed iframe for hosting the idp-proxy's js. Create a message michael@0: * channel down to the frame. michael@0: * michael@0: * @param errorCallback (function) a callback that will be invoked if there michael@0: * is a fatal error starting the proxy michael@0: */ michael@0: start: function(errorCallback) { michael@0: if (this.channel) { michael@0: return; michael@0: } michael@0: let well_known = "https://" + this.domain; michael@0: well_known += "/.well-known/idp-proxy/" + this.protocol; michael@0: this.channel = new IdpChannel(well_known, this._messageReceived.bind(this)); michael@0: this.channel.open(function(error) { michael@0: if (error) { michael@0: this.close(); michael@0: if (typeof errorCallback === "function") { michael@0: errorCallback(error); michael@0: } michael@0: } michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Send a message up to the idp proxy. This should be an RTC "SIGN" or michael@0: * "VERIFY" message. This method adds the tracking 'id' parameter michael@0: * automatically to the message so that the callback is only invoked for the michael@0: * response to the message. michael@0: * michael@0: * This enqueues the message to send if the IdP hasn't signaled that it is michael@0: * "READY", and sends the message when it is. michael@0: * michael@0: * The caller is responsible for ensuring that a response is received. If the michael@0: * IdP doesn't respond, the callback simply isn't invoked. michael@0: */ michael@0: send: function(message, callback) { michael@0: this.start(); michael@0: if (this.ready) { michael@0: message.id = "" + (++this.counter); michael@0: this.tracking[message.id] = callback; michael@0: this.channel.send(message); michael@0: } else { michael@0: this.pending.push({ message: message, callback: callback }); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Handle a message from the IdP. This automatically sends if the message is michael@0: * 'READY' so there is no need to track readiness state outside of this obj. michael@0: */ michael@0: _messageReceived: function(message) { michael@0: if (!message) { michael@0: return; michael@0: } michael@0: if (!this.ready && message.type === "READY") { michael@0: this.ready = true; michael@0: this.pending.forEach(function(p) { michael@0: this.send(p.message, p.callback); michael@0: }, this); michael@0: this.pending = []; michael@0: } else if (this.tracking[message.id]) { michael@0: var callback = this.tracking[message.id]; michael@0: delete this.tracking[message.id]; michael@0: callback(message); michael@0: } else { michael@0: let console = Cc["@mozilla.org/consoleservice;1"]. michael@0: getService(Ci.nsIConsoleService); michael@0: console.logStringMessage("Received bad message from IdP: " + michael@0: message.id + ":" + message.type); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Performs cleanup. The object should be OK to use again. michael@0: */ michael@0: close: function() { michael@0: if (!this.channel) { michael@0: return; michael@0: } michael@0: michael@0: // clear out before letting others know in case they do something bad michael@0: let trackingCopy = this.tracking; michael@0: let pendingCopy = this.pending; michael@0: michael@0: this.channel.close(); michael@0: this._reset(); michael@0: michael@0: // dump a message of type "ERROR" in response to all outstanding michael@0: // messages to the IdP michael@0: let error = { type: "ERROR", error: "IdP closed" }; michael@0: Object.keys(trackingCopy).forEach(function(k) { michael@0: trackingCopy[k](error); michael@0: }); michael@0: pendingCopy.forEach(function(p) { michael@0: p.callback(error); michael@0: }); michael@0: }, michael@0: michael@0: toString: function() { michael@0: return this.domain + '/.../' + this.protocol; michael@0: } michael@0: }; michael@0: michael@0: this.IdpProxy = IdpProxy;