dom/media/IdpProxy.jsm

changeset 0
6474c204b198
     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;

mercurial