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 +}());