michael@0: /* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80: */ 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 file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: */ michael@0: michael@0: "use strict"; michael@0: michael@0: // the singleton frameworker, available for (ab)use by tests. michael@0: let frameworker; michael@0: michael@0: (function () { // bug 673569 workaround :( michael@0: michael@0: /* michael@0: * This is an implementation of a "Shared Worker" using a remote michael@0: * element hosted in the hidden DOM window. This is the "content script" michael@0: * implementation - it runs in the child process but has chrome permissions. michael@0: * michael@0: * A set of new APIs that simulate a shared worker are introduced to a sandbox michael@0: * by cloning methods from the worker's JS origin. michael@0: */ michael@0: michael@0: const {classes: Cc, interfaces: Ci, utils: Cu} = Components; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/MessagePortBase.jsm"); michael@0: michael@0: function navigate(url) { michael@0: let webnav = docShell.QueryInterface(Ci.nsIWebNavigation); michael@0: webnav.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null, null); michael@0: } michael@0: michael@0: /** michael@0: * FrameWorker michael@0: * michael@0: * A FrameWorker is a element hosted by the hiddenWindow. michael@0: * It is constructed with the URL of some JavaScript that will be run in michael@0: * the context of the browser; the script does not have a full DOM but is michael@0: * instead run in a sandbox that has a select set of methods cloned from the michael@0: * URL's domain. michael@0: */ michael@0: function FrameWorker(url, name, origin, exposeLocalStorage) { michael@0: this.url = url; michael@0: this.name = name || url; michael@0: this.ports = new Map(); // all unclosed ports, including ones yet to be entangled michael@0: this.loaded = false; michael@0: this.origin = origin; michael@0: this._injectController = null; michael@0: this.exposeLocalStorage = exposeLocalStorage; michael@0: michael@0: this.load(); michael@0: } michael@0: michael@0: FrameWorker.prototype = { michael@0: load: function FrameWorker_loadWorker() { michael@0: this._injectController = function(doc, topic, data) { michael@0: if (!doc.defaultView || doc.defaultView != content) { michael@0: return; michael@0: } michael@0: this._maybeRemoveInjectController(); michael@0: try { michael@0: this.createSandbox(); michael@0: } catch (e) { michael@0: Cu.reportError("FrameWorker: failed to create sandbox for " + this.url + ". " + e); michael@0: } michael@0: }.bind(this); michael@0: michael@0: Services.obs.addObserver(this._injectController, "document-element-inserted", false); michael@0: navigate(this.url); michael@0: }, michael@0: michael@0: _maybeRemoveInjectController: function() { michael@0: if (this._injectController) { michael@0: Services.obs.removeObserver(this._injectController, "document-element-inserted"); michael@0: this._injectController = null; michael@0: } michael@0: }, michael@0: michael@0: createSandbox: function createSandbox() { michael@0: let workerWindow = content; michael@0: let sandbox = new Cu.Sandbox(workerWindow); michael@0: michael@0: // copy the window apis onto the sandbox namespace only functions or michael@0: // objects that are naturally a part of an iframe, I'm assuming they are michael@0: // safe to import this way michael@0: let workerAPI = ['WebSocket', 'atob', 'btoa', michael@0: 'clearInterval', 'clearTimeout', 'dump', michael@0: 'setInterval', 'setTimeout', 'XMLHttpRequest', michael@0: 'FileReader', 'Blob', 'EventSource', 'indexedDB', michael@0: 'location', 'Worker']; michael@0: michael@0: // Only expose localStorage if the caller opted-in michael@0: if (this.exposeLocalStorage) { michael@0: workerAPI.push('localStorage'); michael@0: } michael@0: michael@0: // Bug 798660 - XHR, WebSocket and Worker have issues in a sandbox and need michael@0: // to be unwrapped to work michael@0: let needsWaive = ['XMLHttpRequest', 'WebSocket', 'Worker']; michael@0: // Methods need to be bound with the proper |this|. michael@0: let needsBind = ['atob', 'btoa', 'dump', 'setInterval', 'clearInterval', michael@0: 'setTimeout', 'clearTimeout']; michael@0: workerAPI.forEach(function(fn) { michael@0: try { michael@0: if (needsWaive.indexOf(fn) != -1) michael@0: sandbox[fn] = XPCNativeWrapper.unwrap(workerWindow)[fn]; michael@0: else if (needsBind.indexOf(fn) != -1) michael@0: sandbox[fn] = workerWindow[fn].bind(workerWindow); michael@0: else michael@0: sandbox[fn] = workerWindow[fn]; michael@0: } michael@0: catch(e) { michael@0: Cu.reportError("FrameWorker: failed to import API "+fn+"\n"+e+"\n"); michael@0: } michael@0: }); michael@0: // the "navigator" object in a worker is a subset of the full navigator; michael@0: // specifically, just the interfaces 'NavigatorID' and 'NavigatorOnLine' michael@0: let navigator = { michael@0: __exposedProps__: { michael@0: "appName": "r", michael@0: "appVersion": "r", michael@0: "platform": "r", michael@0: "userAgent": "r", michael@0: "onLine": "r" michael@0: }, michael@0: // interface NavigatorID michael@0: appName: workerWindow.navigator.appName, michael@0: appVersion: workerWindow.navigator.appVersion, michael@0: platform: workerWindow.navigator.platform, michael@0: userAgent: workerWindow.navigator.userAgent, michael@0: // interface NavigatorOnLine michael@0: get onLine() workerWindow.navigator.onLine michael@0: }; michael@0: sandbox.navigator = navigator; michael@0: michael@0: // Our importScripts function needs to 'eval' the script code from inside michael@0: // a function, but using eval() directly means functions in the script michael@0: // don't end up in the global scope. michael@0: sandbox._evalInSandbox = function(s, url) { michael@0: let baseURI = Services.io.newURI(workerWindow.location.href, null, null); michael@0: Cu.evalInSandbox(s, sandbox, "1.8", michael@0: Services.io.newURI(url, null, baseURI).spec, 1); michael@0: }; michael@0: michael@0: // and we delegate ononline and onoffline events to the worker. michael@0: // See http://www.whatwg.org/specs/web-apps/current-work/multipage/workers.html#workerglobalscope michael@0: workerWindow.addEventListener('offline', function fw_onoffline(event) { michael@0: Cu.evalInSandbox("onoffline();", sandbox); michael@0: }, false); michael@0: workerWindow.addEventListener('online', function fw_ononline(event) { michael@0: Cu.evalInSandbox("ononline();", sandbox); michael@0: }, false); michael@0: michael@0: sandbox._postMessage = function fw_postMessage(d, o) { michael@0: workerWindow.postMessage(d, o) michael@0: }; michael@0: sandbox._addEventListener = function fw_addEventListener(t, l, c) { michael@0: workerWindow.addEventListener(t, l, c) michael@0: }; michael@0: michael@0: // Note we don't need to stash |sandbox| in |this| as the unload handler michael@0: // has a reference in its closure, so it can't die until that handler is michael@0: // removed - at which time we've explicitly killed it anyway. michael@0: let worker = this; michael@0: michael@0: workerWindow.addEventListener("DOMContentLoaded", function loadListener() { michael@0: workerWindow.removeEventListener("DOMContentLoaded", loadListener); michael@0: michael@0: // no script, error out now rather than creating ports, etc michael@0: let scriptText = workerWindow.document.body.textContent.trim(); michael@0: if (!scriptText) { michael@0: Cu.reportError("FrameWorker: Empty worker script received"); michael@0: notifyWorkerError(); michael@0: return; michael@0: } michael@0: michael@0: // now that we've got the script text, remove it from the DOM; michael@0: // no need for it to keep occupying memory there michael@0: workerWindow.document.body.textContent = ""; michael@0: michael@0: // the content has loaded the js file as text - first inject the magic michael@0: // port-handling code into the sandbox. michael@0: try { michael@0: Services.scriptloader.loadSubScript("resource://gre/modules/MessagePortBase.jsm", sandbox); michael@0: Services.scriptloader.loadSubScript("resource://gre/modules/MessagePortWorker.js", sandbox); michael@0: } michael@0: catch (e) { michael@0: Cu.reportError("FrameWorker: Error injecting port code into content side of the worker: " + e + "\n" + e.stack); michael@0: notifyWorkerError(); michael@0: return; michael@0: } michael@0: michael@0: // and wire up the client message handling. michael@0: try { michael@0: initClientMessageHandler(); michael@0: } michael@0: catch (e) { michael@0: Cu.reportError("FrameWorker: Error setting up event listener for chrome side of the worker: " + e + "\n" + e.stack); michael@0: notifyWorkerError(); michael@0: return; michael@0: } michael@0: michael@0: // Now get the worker js code and eval it into the sandbox michael@0: try { michael@0: Cu.evalInSandbox(scriptText, sandbox, "1.8", workerWindow.location.href, 1); michael@0: } catch (e) { michael@0: Cu.reportError("FrameWorker: Error evaluating worker script for " + worker.name + ": " + e + "; " + michael@0: (e.lineNumber ? ("Line #" + e.lineNumber) : "") + michael@0: (e.stack ? ("\n" + e.stack) : "")); michael@0: notifyWorkerError(); michael@0: return; michael@0: } michael@0: michael@0: // so finally we are ready to roll - dequeue all the pending connects michael@0: worker.loaded = true; michael@0: for (let [,port] of worker.ports) { // enumeration is in insertion order michael@0: if (!port._entangled) { michael@0: try { michael@0: port._createWorkerAndEntangle(worker); michael@0: } michael@0: catch(e) { michael@0: Cu.reportError("FrameWorker: Failed to entangle worker port: " + e + "\n" + e.stack); michael@0: } michael@0: } michael@0: } michael@0: }); michael@0: michael@0: // the 'unload' listener cleans up the worker and the sandbox. This michael@0: // will be triggered by the window unloading as part of shutdown or reload. michael@0: workerWindow.addEventListener("unload", function unloadListener() { michael@0: workerWindow.removeEventListener("unload", unloadListener); michael@0: worker.loaded = false; michael@0: // No need to close ports - the worker probably wont see a michael@0: // social.port-closing message and certainly isn't going to have time to michael@0: // do anything if it did see it. michael@0: worker.ports.clear(); michael@0: if (sandbox) { michael@0: Cu.nukeSandbox(sandbox); michael@0: sandbox = null; michael@0: } michael@0: }); michael@0: }, michael@0: }; michael@0: michael@0: const FrameWorkerManager = { michael@0: init: function() { michael@0: // first, setup the docShell to disable some types of content michael@0: docShell.allowAuth = false; michael@0: docShell.allowPlugins = false; michael@0: docShell.allowImages = false; michael@0: docShell.allowMedia = false; michael@0: docShell.allowWindowControl = false; michael@0: michael@0: addMessageListener("frameworker:init", this._onInit); michael@0: addMessageListener("frameworker:connect", this._onConnect); michael@0: addMessageListener("frameworker:port-message", this._onPortMessage); michael@0: addMessageListener("frameworker:cookie-get", this._onCookieGet); michael@0: }, michael@0: michael@0: // This new frameworker is being created. This should only be called once. michael@0: _onInit: function(msg) { michael@0: let {url, name, origin, exposeLocalStorage} = msg.data; michael@0: frameworker = new FrameWorker(url, name, origin, exposeLocalStorage); michael@0: }, michael@0: michael@0: // A new port is being established for this frameworker. michael@0: _onConnect: function(msg) { michael@0: let port = new ClientPort(msg.data.portId); michael@0: frameworker.ports.set(msg.data.portId, port); michael@0: if (frameworker.loaded && !frameworker.reloading) michael@0: port._createWorkerAndEntangle(frameworker); michael@0: }, michael@0: michael@0: // A message related to a port. michael@0: _onPortMessage: function(msg) { michael@0: // find the "client" port for this message and have it post it into michael@0: // the worker. michael@0: let port = frameworker.ports.get(msg.data.portId); michael@0: port._dopost(msg.data); michael@0: }, michael@0: michael@0: _onCookieGet: function(msg) { michael@0: sendAsyncMessage("frameworker:cookie-get-response", content.document.cookie); michael@0: }, michael@0: michael@0: }; michael@0: michael@0: FrameWorkerManager.init(); michael@0: michael@0: // This is the message listener for the chrome side of the world - ie, the michael@0: // port that exists with chrome permissions inside the (ie, in the michael@0: // content process if a remote browser is used). michael@0: function initClientMessageHandler() { michael@0: function _messageHandler(event) { michael@0: // We will ignore all messages destined for otherType. michael@0: let data = event.data; michael@0: let portid = data.portId; michael@0: let port; michael@0: if (!data.portFromType || data.portFromType !== "worker") { michael@0: // this is a message posted by ourself so ignore it. michael@0: return; michael@0: } michael@0: switch (data.portTopic) { michael@0: // No "port-create" here - client ports are created explicitly. michael@0: case "port-connection-error": michael@0: // onconnect failed, we cannot connect the port, the worker has michael@0: // become invalid michael@0: notifyWorkerError(); michael@0: break; michael@0: case "port-close": michael@0: // the worker side of the port was closed, so close this side too. michael@0: port = frameworker.ports.get(portid); michael@0: if (!port) { michael@0: // port already closed (which will happen when we call port.close() michael@0: // below - the worker side will send us this message but we've michael@0: // already closed it.) michael@0: return; michael@0: } michael@0: frameworker.ports.delete(portid); michael@0: port.close(); michael@0: break; michael@0: michael@0: case "port-message": michael@0: // the client posted a message to this worker port. michael@0: port = frameworker.ports.get(portid); michael@0: if (!port) { michael@0: return; michael@0: } michael@0: port._onmessage(data.data); michael@0: break; michael@0: michael@0: default: michael@0: break; michael@0: } michael@0: } michael@0: // this can probably go once debugged and working correctly! michael@0: function messageHandler(event) { michael@0: try { michael@0: _messageHandler(event); michael@0: } catch (ex) { michael@0: Cu.reportError("FrameWorker: Error handling client port control message: " + ex + "\n" + ex.stack); michael@0: } michael@0: } michael@0: content.addEventListener('message', messageHandler); michael@0: } michael@0: michael@0: /** michael@0: * ClientPort michael@0: * michael@0: * Client side of the entangled ports. This is just a shim that sends messages michael@0: * back to the "parent" port living in the chrome process. michael@0: * michael@0: * constructor: michael@0: * @param {integer} portid michael@0: */ michael@0: function ClientPort(portid) { michael@0: // messages posted to the worker before the worker has loaded. michael@0: this._pendingMessagesOutgoing = []; michael@0: AbstractPort.call(this, portid); michael@0: } michael@0: michael@0: ClientPort.prototype = { michael@0: __proto__: AbstractPort.prototype, michael@0: _portType: "client", michael@0: // _entangled records if the port has ever been entangled (although may be michael@0: // reset during a reload). michael@0: _entangled: false, michael@0: michael@0: _createWorkerAndEntangle: function fw_ClientPort_createWorkerAndEntangle(worker) { michael@0: this._entangled = true; michael@0: this._postControlMessage("port-create"); michael@0: for (let message of this._pendingMessagesOutgoing) { michael@0: this._dopost(message); michael@0: } michael@0: this._pendingMessagesOutgoing = []; michael@0: // The client side of the port might have been closed before it was michael@0: // "entangled" with the worker, in which case we need to disentangle it michael@0: if (this._closed) { michael@0: worker.ports.delete(this._portid); michael@0: } michael@0: }, michael@0: michael@0: _dopost: function fw_ClientPort_dopost(data) { michael@0: if (!this._entangled) { michael@0: this._pendingMessagesOutgoing.push(data); michael@0: } else { michael@0: content.postMessage(data, "*"); michael@0: } michael@0: }, michael@0: michael@0: // we are just a "shim" - any messages we get are just forwarded back to michael@0: // the chrome parent process. michael@0: _onmessage: function(data) { michael@0: sendAsyncMessage("frameworker:port-message", {portId: this._portid, data: data}); michael@0: }, michael@0: michael@0: _onerror: function fw_ClientPort_onerror(err) { michael@0: Cu.reportError("FrameWorker: Port " + this + " handler failed: " + err + "\n" + err.stack); michael@0: }, michael@0: michael@0: close: function fw_ClientPort_close() { michael@0: if (this._closed) { michael@0: return; // already closed. michael@0: } michael@0: // a leaky abstraction due to the worker spec not specifying how the michael@0: // other end of a port knows it is closing. michael@0: this.postMessage({topic: "social.port-closing"}); michael@0: AbstractPort.prototype.close.call(this); michael@0: // this._pendingMessagesOutgoing should still be drained, as a closed michael@0: // port will still get "entangled" quickly enough to deliver the messages. michael@0: } michael@0: } michael@0: michael@0: function notifyWorkerError() { michael@0: sendAsyncMessage("frameworker:notify-worker-error", {origin: frameworker.origin}); michael@0: } michael@0: michael@0: }());