toolkit/components/social/FrameWorkerContent.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/components/social/FrameWorkerContent.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,419 @@
     1.4 +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     1.5 +/* vim: set ts=2 et sw=2 tw=80: */
     1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.8 + * You can obtain one at http://mozilla.org/MPL/2.0/.
     1.9 + */
    1.10 +
    1.11 +"use strict";
    1.12 +
    1.13 +// the singleton frameworker, available for (ab)use by tests.
    1.14 +let frameworker;
    1.15 +
    1.16 +(function () { // bug 673569 workaround :(
    1.17 +
    1.18 +/*
    1.19 + * This is an implementation of a "Shared Worker" using a remote <browser>
    1.20 + * element hosted in the hidden DOM window.  This is the "content script"
    1.21 + * implementation - it runs in the child process but has chrome permissions.
    1.22 + *
    1.23 + * A set of new APIs that simulate a shared worker are introduced to a sandbox
    1.24 + * by cloning methods from the worker's JS origin.
    1.25 + */
    1.26 +
    1.27 +const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
    1.28 +
    1.29 +Cu.import("resource://gre/modules/Services.jsm");
    1.30 +Cu.import("resource://gre/modules/MessagePortBase.jsm");
    1.31 +
    1.32 +function navigate(url) {
    1.33 +  let webnav = docShell.QueryInterface(Ci.nsIWebNavigation);
    1.34 +  webnav.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null, null);
    1.35 +}
    1.36 +
    1.37 +/**
    1.38 + * FrameWorker
    1.39 + *
    1.40 + * A FrameWorker is a <browser> element hosted by the hiddenWindow.
    1.41 + * It is constructed with the URL of some JavaScript that will be run in
    1.42 + * the context of the browser; the script does not have a full DOM but is
    1.43 + * instead run in a sandbox that has a select set of methods cloned from the
    1.44 + * URL's domain.
    1.45 + */
    1.46 +function FrameWorker(url, name, origin, exposeLocalStorage) {
    1.47 +  this.url = url;
    1.48 +  this.name = name || url;
    1.49 +  this.ports = new Map(); // all unclosed ports, including ones yet to be entangled
    1.50 +  this.loaded = false;
    1.51 +  this.origin = origin;
    1.52 +  this._injectController = null;
    1.53 +  this.exposeLocalStorage = exposeLocalStorage;
    1.54 +
    1.55 +  this.load();
    1.56 +}
    1.57 +
    1.58 +FrameWorker.prototype = {
    1.59 +  load: function FrameWorker_loadWorker() {
    1.60 +    this._injectController = function(doc, topic, data) {
    1.61 +      if (!doc.defaultView || doc.defaultView != content) {
    1.62 +        return;
    1.63 +      }
    1.64 +      this._maybeRemoveInjectController();
    1.65 +      try {
    1.66 +        this.createSandbox();
    1.67 +      } catch (e) {
    1.68 +        Cu.reportError("FrameWorker: failed to create sandbox for " + this.url + ". " + e);
    1.69 +      }
    1.70 +    }.bind(this);
    1.71 +
    1.72 +    Services.obs.addObserver(this._injectController, "document-element-inserted", false);
    1.73 +    navigate(this.url);
    1.74 +  },
    1.75 +
    1.76 +  _maybeRemoveInjectController: function() {
    1.77 +    if (this._injectController) {
    1.78 +      Services.obs.removeObserver(this._injectController, "document-element-inserted");
    1.79 +      this._injectController = null;
    1.80 +    }
    1.81 +  },
    1.82 +
    1.83 +  createSandbox: function createSandbox() {
    1.84 +    let workerWindow = content;
    1.85 +    let sandbox = new Cu.Sandbox(workerWindow);
    1.86 +
    1.87 +    // copy the window apis onto the sandbox namespace only functions or
    1.88 +    // objects that are naturally a part of an iframe, I'm assuming they are
    1.89 +    // safe to import this way
    1.90 +    let workerAPI = ['WebSocket', 'atob', 'btoa',
    1.91 +                     'clearInterval', 'clearTimeout', 'dump',
    1.92 +                     'setInterval', 'setTimeout', 'XMLHttpRequest',
    1.93 +                     'FileReader', 'Blob', 'EventSource', 'indexedDB',
    1.94 +                     'location', 'Worker'];
    1.95 +
    1.96 +    // Only expose localStorage if the caller opted-in
    1.97 +    if (this.exposeLocalStorage) {
    1.98 +      workerAPI.push('localStorage');
    1.99 +    }
   1.100 +
   1.101 +    // Bug 798660 - XHR, WebSocket and Worker have issues in a sandbox and need
   1.102 +    // to be unwrapped to work
   1.103 +    let needsWaive = ['XMLHttpRequest', 'WebSocket', 'Worker'];
   1.104 +    // Methods need to be bound with the proper |this|.
   1.105 +    let needsBind = ['atob', 'btoa', 'dump', 'setInterval', 'clearInterval',
   1.106 +                     'setTimeout', 'clearTimeout'];
   1.107 +    workerAPI.forEach(function(fn) {
   1.108 +      try {
   1.109 +        if (needsWaive.indexOf(fn) != -1)
   1.110 +          sandbox[fn] = XPCNativeWrapper.unwrap(workerWindow)[fn];
   1.111 +        else if (needsBind.indexOf(fn) != -1)
   1.112 +          sandbox[fn] = workerWindow[fn].bind(workerWindow);
   1.113 +        else
   1.114 +          sandbox[fn] = workerWindow[fn];
   1.115 +      }
   1.116 +      catch(e) {
   1.117 +        Cu.reportError("FrameWorker: failed to import API "+fn+"\n"+e+"\n");
   1.118 +      }
   1.119 +    });
   1.120 +    // the "navigator" object in a worker is a subset of the full navigator;
   1.121 +    // specifically, just the interfaces 'NavigatorID' and 'NavigatorOnLine'
   1.122 +    let navigator = {
   1.123 +      __exposedProps__: {
   1.124 +        "appName": "r",
   1.125 +        "appVersion": "r",
   1.126 +        "platform": "r",
   1.127 +        "userAgent": "r",
   1.128 +        "onLine": "r"
   1.129 +      },
   1.130 +      // interface NavigatorID
   1.131 +      appName: workerWindow.navigator.appName,
   1.132 +      appVersion: workerWindow.navigator.appVersion,
   1.133 +      platform: workerWindow.navigator.platform,
   1.134 +      userAgent: workerWindow.navigator.userAgent,
   1.135 +      // interface NavigatorOnLine
   1.136 +      get onLine() workerWindow.navigator.onLine
   1.137 +    };
   1.138 +    sandbox.navigator = navigator;
   1.139 +
   1.140 +    // Our importScripts function needs to 'eval' the script code from inside
   1.141 +    // a function, but using eval() directly means functions in the script
   1.142 +    // don't end up in the global scope.
   1.143 +    sandbox._evalInSandbox = function(s, url) {
   1.144 +      let baseURI = Services.io.newURI(workerWindow.location.href, null, null);
   1.145 +      Cu.evalInSandbox(s, sandbox, "1.8",
   1.146 +                       Services.io.newURI(url, null, baseURI).spec, 1);
   1.147 +    };
   1.148 +
   1.149 +    // and we delegate ononline and onoffline events to the worker.
   1.150 +    // See http://www.whatwg.org/specs/web-apps/current-work/multipage/workers.html#workerglobalscope
   1.151 +    workerWindow.addEventListener('offline', function fw_onoffline(event) {
   1.152 +      Cu.evalInSandbox("onoffline();", sandbox);
   1.153 +    }, false);
   1.154 +    workerWindow.addEventListener('online', function fw_ononline(event) {
   1.155 +      Cu.evalInSandbox("ononline();", sandbox);
   1.156 +    }, false);
   1.157 +
   1.158 +    sandbox._postMessage = function fw_postMessage(d, o) {
   1.159 +      workerWindow.postMessage(d, o)
   1.160 +    };
   1.161 +    sandbox._addEventListener = function fw_addEventListener(t, l, c) {
   1.162 +      workerWindow.addEventListener(t, l, c)
   1.163 +    };
   1.164 +
   1.165 +    // Note we don't need to stash |sandbox| in |this| as the unload handler
   1.166 +    // has a reference in its closure, so it can't die until that handler is
   1.167 +    // removed - at which time we've explicitly killed it anyway.
   1.168 +    let worker = this;
   1.169 +
   1.170 +    workerWindow.addEventListener("DOMContentLoaded", function loadListener() {
   1.171 +      workerWindow.removeEventListener("DOMContentLoaded", loadListener);
   1.172 +
   1.173 +      // no script, error out now rather than creating ports, etc
   1.174 +      let scriptText = workerWindow.document.body.textContent.trim();
   1.175 +      if (!scriptText) {
   1.176 +        Cu.reportError("FrameWorker: Empty worker script received");
   1.177 +        notifyWorkerError();
   1.178 +        return;
   1.179 +      }
   1.180 +
   1.181 +      // now that we've got the script text, remove it from the DOM;
   1.182 +      // no need for it to keep occupying memory there
   1.183 +      workerWindow.document.body.textContent = "";
   1.184 +
   1.185 +      // the content has loaded the js file as text - first inject the magic
   1.186 +      // port-handling code into the sandbox.
   1.187 +      try {
   1.188 +        Services.scriptloader.loadSubScript("resource://gre/modules/MessagePortBase.jsm", sandbox);
   1.189 +        Services.scriptloader.loadSubScript("resource://gre/modules/MessagePortWorker.js", sandbox);
   1.190 +      }
   1.191 +      catch (e) {
   1.192 +        Cu.reportError("FrameWorker: Error injecting port code into content side of the worker: " + e + "\n" + e.stack);
   1.193 +        notifyWorkerError();
   1.194 +        return;
   1.195 +      }
   1.196 +
   1.197 +      // and wire up the client message handling.
   1.198 +      try {
   1.199 +        initClientMessageHandler();
   1.200 +      }
   1.201 +      catch (e) {
   1.202 +        Cu.reportError("FrameWorker: Error setting up event listener for chrome side of the worker: " + e + "\n" + e.stack);
   1.203 +        notifyWorkerError();
   1.204 +        return;
   1.205 +      }
   1.206 +
   1.207 +      // Now get the worker js code and eval it into the sandbox
   1.208 +      try {
   1.209 +        Cu.evalInSandbox(scriptText, sandbox, "1.8", workerWindow.location.href, 1);
   1.210 +      } catch (e) {
   1.211 +        Cu.reportError("FrameWorker: Error evaluating worker script for " + worker.name + ": " + e + "; " +
   1.212 +            (e.lineNumber ? ("Line #" + e.lineNumber) : "") +
   1.213 +            (e.stack ? ("\n" + e.stack) : ""));
   1.214 +        notifyWorkerError();
   1.215 +        return;
   1.216 +      }
   1.217 +
   1.218 +      // so finally we are ready to roll - dequeue all the pending connects
   1.219 +      worker.loaded = true;
   1.220 +      for (let [,port] of worker.ports) { // enumeration is in insertion order
   1.221 +        if (!port._entangled) {
   1.222 +          try {
   1.223 +            port._createWorkerAndEntangle(worker);
   1.224 +          }
   1.225 +          catch(e) {
   1.226 +            Cu.reportError("FrameWorker: Failed to entangle worker port: " + e + "\n" + e.stack);
   1.227 +          }
   1.228 +        }
   1.229 +      }
   1.230 +    });
   1.231 +
   1.232 +    // the 'unload' listener cleans up the worker and the sandbox.  This
   1.233 +    // will be triggered by the window unloading as part of shutdown or reload.
   1.234 +    workerWindow.addEventListener("unload", function unloadListener() {
   1.235 +      workerWindow.removeEventListener("unload", unloadListener);
   1.236 +      worker.loaded = false;
   1.237 +      // No need to close ports - the worker probably wont see a
   1.238 +      // social.port-closing message and certainly isn't going to have time to
   1.239 +      // do anything if it did see it.
   1.240 +      worker.ports.clear();
   1.241 +      if (sandbox) {
   1.242 +        Cu.nukeSandbox(sandbox);
   1.243 +        sandbox = null;
   1.244 +      }
   1.245 +    });
   1.246 +  },
   1.247 +};
   1.248 +
   1.249 +const FrameWorkerManager = {
   1.250 +  init: function() {
   1.251 +    // first, setup the docShell to disable some types of content
   1.252 +    docShell.allowAuth = false;
   1.253 +    docShell.allowPlugins = false;
   1.254 +    docShell.allowImages = false;
   1.255 +    docShell.allowMedia = false;
   1.256 +    docShell.allowWindowControl = false;
   1.257 +
   1.258 +    addMessageListener("frameworker:init", this._onInit);
   1.259 +    addMessageListener("frameworker:connect", this._onConnect);
   1.260 +    addMessageListener("frameworker:port-message", this._onPortMessage);
   1.261 +    addMessageListener("frameworker:cookie-get", this._onCookieGet);
   1.262 +  },
   1.263 +
   1.264 +  // This new frameworker is being created.  This should only be called once.
   1.265 +  _onInit: function(msg) {
   1.266 +    let {url, name, origin, exposeLocalStorage} = msg.data;
   1.267 +    frameworker = new FrameWorker(url, name, origin, exposeLocalStorage);
   1.268 +  },
   1.269 +
   1.270 +  // A new port is being established for this frameworker.
   1.271 +  _onConnect: function(msg) {
   1.272 +    let port = new ClientPort(msg.data.portId);
   1.273 +    frameworker.ports.set(msg.data.portId, port);
   1.274 +    if (frameworker.loaded && !frameworker.reloading)
   1.275 +      port._createWorkerAndEntangle(frameworker);
   1.276 +  },
   1.277 +
   1.278 +  // A message related to a port.
   1.279 +  _onPortMessage: function(msg) {
   1.280 +    // find the "client" port for this message and have it post it into
   1.281 +    // the worker.
   1.282 +    let port = frameworker.ports.get(msg.data.portId);
   1.283 +    port._dopost(msg.data);
   1.284 +  },
   1.285 +
   1.286 +  _onCookieGet: function(msg) {
   1.287 +    sendAsyncMessage("frameworker:cookie-get-response", content.document.cookie);
   1.288 +  },
   1.289 +
   1.290 +};
   1.291 +
   1.292 +FrameWorkerManager.init();
   1.293 +
   1.294 +// This is the message listener for the chrome side of the world - ie, the
   1.295 +// port that exists with chrome permissions inside the <browser/> (ie, in the
   1.296 +// content process if a remote browser is used).
   1.297 +function initClientMessageHandler() {
   1.298 +  function _messageHandler(event) {
   1.299 +    // We will ignore all messages destined for otherType.
   1.300 +    let data = event.data;
   1.301 +    let portid = data.portId;
   1.302 +    let port;
   1.303 +    if (!data.portFromType || data.portFromType !== "worker") {
   1.304 +      // this is a message posted by ourself so ignore it.
   1.305 +      return;
   1.306 +    }
   1.307 +    switch (data.portTopic) {
   1.308 +      // No "port-create" here - client ports are created explicitly.
   1.309 +      case "port-connection-error":
   1.310 +        // onconnect failed, we cannot connect the port, the worker has
   1.311 +        // become invalid
   1.312 +        notifyWorkerError();
   1.313 +        break;
   1.314 +      case "port-close":
   1.315 +        // the worker side of the port was closed, so close this side too.
   1.316 +        port = frameworker.ports.get(portid);
   1.317 +        if (!port) {
   1.318 +          // port already closed (which will happen when we call port.close()
   1.319 +          // below - the worker side will send us this message but we've
   1.320 +          // already closed it.)
   1.321 +          return;
   1.322 +        }
   1.323 +        frameworker.ports.delete(portid);
   1.324 +        port.close();
   1.325 +        break;
   1.326 +
   1.327 +      case "port-message":
   1.328 +        // the client posted a message to this worker port.
   1.329 +        port = frameworker.ports.get(portid);
   1.330 +        if (!port) {
   1.331 +          return;
   1.332 +        }
   1.333 +        port._onmessage(data.data);
   1.334 +        break;
   1.335 +
   1.336 +      default:
   1.337 +        break;
   1.338 +    }
   1.339 +  }
   1.340 +  // this can probably go once debugged and working correctly!
   1.341 +  function messageHandler(event) {
   1.342 +    try {
   1.343 +      _messageHandler(event);
   1.344 +    } catch (ex) {
   1.345 +      Cu.reportError("FrameWorker: Error handling client port control message: " + ex + "\n" + ex.stack);
   1.346 +    }
   1.347 +  }
   1.348 +  content.addEventListener('message', messageHandler);
   1.349 +}
   1.350 +
   1.351 +/**
   1.352 + * ClientPort
   1.353 + *
   1.354 + * Client side of the entangled ports. This is just a shim that sends messages
   1.355 + * back to the "parent" port living in the chrome process.
   1.356 + *
   1.357 + * constructor:
   1.358 + * @param {integer} portid
   1.359 + */
   1.360 +function ClientPort(portid) {
   1.361 +  // messages posted to the worker before the worker has loaded.
   1.362 +  this._pendingMessagesOutgoing = [];
   1.363 +  AbstractPort.call(this, portid);
   1.364 +}
   1.365 +
   1.366 +ClientPort.prototype = {
   1.367 +  __proto__: AbstractPort.prototype,
   1.368 +  _portType: "client",
   1.369 +  // _entangled records if the port has ever been entangled (although may be
   1.370 +  // reset during a reload).
   1.371 +  _entangled: false,
   1.372 +
   1.373 +  _createWorkerAndEntangle: function fw_ClientPort_createWorkerAndEntangle(worker) {
   1.374 +    this._entangled = true;
   1.375 +    this._postControlMessage("port-create");
   1.376 +    for (let message of this._pendingMessagesOutgoing) {
   1.377 +      this._dopost(message);
   1.378 +    }
   1.379 +    this._pendingMessagesOutgoing = [];
   1.380 +    // The client side of the port might have been closed before it was
   1.381 +    // "entangled" with the worker, in which case we need to disentangle it
   1.382 +    if (this._closed) {
   1.383 +      worker.ports.delete(this._portid);
   1.384 +    }
   1.385 +  },
   1.386 +
   1.387 +  _dopost: function fw_ClientPort_dopost(data) {
   1.388 +    if (!this._entangled) {
   1.389 +      this._pendingMessagesOutgoing.push(data);
   1.390 +    } else {
   1.391 +      content.postMessage(data, "*");
   1.392 +    }
   1.393 +  },
   1.394 +
   1.395 +  // we are just a "shim" - any messages we get are just forwarded back to
   1.396 +  // the chrome parent process.
   1.397 +  _onmessage: function(data) {
   1.398 +    sendAsyncMessage("frameworker:port-message", {portId: this._portid, data: data});
   1.399 +  },
   1.400 +
   1.401 +  _onerror: function fw_ClientPort_onerror(err) {
   1.402 +    Cu.reportError("FrameWorker: Port " + this + " handler failed: " + err + "\n" + err.stack);
   1.403 +  },
   1.404 +
   1.405 +  close: function fw_ClientPort_close() {
   1.406 +    if (this._closed) {
   1.407 +      return; // already closed.
   1.408 +    }
   1.409 +    // a leaky abstraction due to the worker spec not specifying how the
   1.410 +    // other end of a port knows it is closing.
   1.411 +    this.postMessage({topic: "social.port-closing"});
   1.412 +    AbstractPort.prototype.close.call(this);
   1.413 +    // this._pendingMessagesOutgoing should still be drained, as a closed
   1.414 +    // port will still get "entangled" quickly enough to deliver the messages.
   1.415 +  }
   1.416 +}
   1.417 +
   1.418 +function notifyWorkerError() {
   1.419 +  sendAsyncMessage("frameworker:notify-worker-error", {origin: frameworker.origin});
   1.420 +}
   1.421 +
   1.422 +}());

mercurial