|
1 /* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ts=2 et sw=2 tw=80: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
5 * You can obtain one at http://mozilla.org/MPL/2.0/. |
|
6 */ |
|
7 |
|
8 "use strict"; |
|
9 |
|
10 // the singleton frameworker, available for (ab)use by tests. |
|
11 let frameworker; |
|
12 |
|
13 (function () { // bug 673569 workaround :( |
|
14 |
|
15 /* |
|
16 * This is an implementation of a "Shared Worker" using a remote <browser> |
|
17 * element hosted in the hidden DOM window. This is the "content script" |
|
18 * implementation - it runs in the child process but has chrome permissions. |
|
19 * |
|
20 * A set of new APIs that simulate a shared worker are introduced to a sandbox |
|
21 * by cloning methods from the worker's JS origin. |
|
22 */ |
|
23 |
|
24 const {classes: Cc, interfaces: Ci, utils: Cu} = Components; |
|
25 |
|
26 Cu.import("resource://gre/modules/Services.jsm"); |
|
27 Cu.import("resource://gre/modules/MessagePortBase.jsm"); |
|
28 |
|
29 function navigate(url) { |
|
30 let webnav = docShell.QueryInterface(Ci.nsIWebNavigation); |
|
31 webnav.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null, null); |
|
32 } |
|
33 |
|
34 /** |
|
35 * FrameWorker |
|
36 * |
|
37 * A FrameWorker is a <browser> element hosted by the hiddenWindow. |
|
38 * It is constructed with the URL of some JavaScript that will be run in |
|
39 * the context of the browser; the script does not have a full DOM but is |
|
40 * instead run in a sandbox that has a select set of methods cloned from the |
|
41 * URL's domain. |
|
42 */ |
|
43 function FrameWorker(url, name, origin, exposeLocalStorage) { |
|
44 this.url = url; |
|
45 this.name = name || url; |
|
46 this.ports = new Map(); // all unclosed ports, including ones yet to be entangled |
|
47 this.loaded = false; |
|
48 this.origin = origin; |
|
49 this._injectController = null; |
|
50 this.exposeLocalStorage = exposeLocalStorage; |
|
51 |
|
52 this.load(); |
|
53 } |
|
54 |
|
55 FrameWorker.prototype = { |
|
56 load: function FrameWorker_loadWorker() { |
|
57 this._injectController = function(doc, topic, data) { |
|
58 if (!doc.defaultView || doc.defaultView != content) { |
|
59 return; |
|
60 } |
|
61 this._maybeRemoveInjectController(); |
|
62 try { |
|
63 this.createSandbox(); |
|
64 } catch (e) { |
|
65 Cu.reportError("FrameWorker: failed to create sandbox for " + this.url + ". " + e); |
|
66 } |
|
67 }.bind(this); |
|
68 |
|
69 Services.obs.addObserver(this._injectController, "document-element-inserted", false); |
|
70 navigate(this.url); |
|
71 }, |
|
72 |
|
73 _maybeRemoveInjectController: function() { |
|
74 if (this._injectController) { |
|
75 Services.obs.removeObserver(this._injectController, "document-element-inserted"); |
|
76 this._injectController = null; |
|
77 } |
|
78 }, |
|
79 |
|
80 createSandbox: function createSandbox() { |
|
81 let workerWindow = content; |
|
82 let sandbox = new Cu.Sandbox(workerWindow); |
|
83 |
|
84 // copy the window apis onto the sandbox namespace only functions or |
|
85 // objects that are naturally a part of an iframe, I'm assuming they are |
|
86 // safe to import this way |
|
87 let workerAPI = ['WebSocket', 'atob', 'btoa', |
|
88 'clearInterval', 'clearTimeout', 'dump', |
|
89 'setInterval', 'setTimeout', 'XMLHttpRequest', |
|
90 'FileReader', 'Blob', 'EventSource', 'indexedDB', |
|
91 'location', 'Worker']; |
|
92 |
|
93 // Only expose localStorage if the caller opted-in |
|
94 if (this.exposeLocalStorage) { |
|
95 workerAPI.push('localStorage'); |
|
96 } |
|
97 |
|
98 // Bug 798660 - XHR, WebSocket and Worker have issues in a sandbox and need |
|
99 // to be unwrapped to work |
|
100 let needsWaive = ['XMLHttpRequest', 'WebSocket', 'Worker']; |
|
101 // Methods need to be bound with the proper |this|. |
|
102 let needsBind = ['atob', 'btoa', 'dump', 'setInterval', 'clearInterval', |
|
103 'setTimeout', 'clearTimeout']; |
|
104 workerAPI.forEach(function(fn) { |
|
105 try { |
|
106 if (needsWaive.indexOf(fn) != -1) |
|
107 sandbox[fn] = XPCNativeWrapper.unwrap(workerWindow)[fn]; |
|
108 else if (needsBind.indexOf(fn) != -1) |
|
109 sandbox[fn] = workerWindow[fn].bind(workerWindow); |
|
110 else |
|
111 sandbox[fn] = workerWindow[fn]; |
|
112 } |
|
113 catch(e) { |
|
114 Cu.reportError("FrameWorker: failed to import API "+fn+"\n"+e+"\n"); |
|
115 } |
|
116 }); |
|
117 // the "navigator" object in a worker is a subset of the full navigator; |
|
118 // specifically, just the interfaces 'NavigatorID' and 'NavigatorOnLine' |
|
119 let navigator = { |
|
120 __exposedProps__: { |
|
121 "appName": "r", |
|
122 "appVersion": "r", |
|
123 "platform": "r", |
|
124 "userAgent": "r", |
|
125 "onLine": "r" |
|
126 }, |
|
127 // interface NavigatorID |
|
128 appName: workerWindow.navigator.appName, |
|
129 appVersion: workerWindow.navigator.appVersion, |
|
130 platform: workerWindow.navigator.platform, |
|
131 userAgent: workerWindow.navigator.userAgent, |
|
132 // interface NavigatorOnLine |
|
133 get onLine() workerWindow.navigator.onLine |
|
134 }; |
|
135 sandbox.navigator = navigator; |
|
136 |
|
137 // Our importScripts function needs to 'eval' the script code from inside |
|
138 // a function, but using eval() directly means functions in the script |
|
139 // don't end up in the global scope. |
|
140 sandbox._evalInSandbox = function(s, url) { |
|
141 let baseURI = Services.io.newURI(workerWindow.location.href, null, null); |
|
142 Cu.evalInSandbox(s, sandbox, "1.8", |
|
143 Services.io.newURI(url, null, baseURI).spec, 1); |
|
144 }; |
|
145 |
|
146 // and we delegate ononline and onoffline events to the worker. |
|
147 // See http://www.whatwg.org/specs/web-apps/current-work/multipage/workers.html#workerglobalscope |
|
148 workerWindow.addEventListener('offline', function fw_onoffline(event) { |
|
149 Cu.evalInSandbox("onoffline();", sandbox); |
|
150 }, false); |
|
151 workerWindow.addEventListener('online', function fw_ononline(event) { |
|
152 Cu.evalInSandbox("ononline();", sandbox); |
|
153 }, false); |
|
154 |
|
155 sandbox._postMessage = function fw_postMessage(d, o) { |
|
156 workerWindow.postMessage(d, o) |
|
157 }; |
|
158 sandbox._addEventListener = function fw_addEventListener(t, l, c) { |
|
159 workerWindow.addEventListener(t, l, c) |
|
160 }; |
|
161 |
|
162 // Note we don't need to stash |sandbox| in |this| as the unload handler |
|
163 // has a reference in its closure, so it can't die until that handler is |
|
164 // removed - at which time we've explicitly killed it anyway. |
|
165 let worker = this; |
|
166 |
|
167 workerWindow.addEventListener("DOMContentLoaded", function loadListener() { |
|
168 workerWindow.removeEventListener("DOMContentLoaded", loadListener); |
|
169 |
|
170 // no script, error out now rather than creating ports, etc |
|
171 let scriptText = workerWindow.document.body.textContent.trim(); |
|
172 if (!scriptText) { |
|
173 Cu.reportError("FrameWorker: Empty worker script received"); |
|
174 notifyWorkerError(); |
|
175 return; |
|
176 } |
|
177 |
|
178 // now that we've got the script text, remove it from the DOM; |
|
179 // no need for it to keep occupying memory there |
|
180 workerWindow.document.body.textContent = ""; |
|
181 |
|
182 // the content has loaded the js file as text - first inject the magic |
|
183 // port-handling code into the sandbox. |
|
184 try { |
|
185 Services.scriptloader.loadSubScript("resource://gre/modules/MessagePortBase.jsm", sandbox); |
|
186 Services.scriptloader.loadSubScript("resource://gre/modules/MessagePortWorker.js", sandbox); |
|
187 } |
|
188 catch (e) { |
|
189 Cu.reportError("FrameWorker: Error injecting port code into content side of the worker: " + e + "\n" + e.stack); |
|
190 notifyWorkerError(); |
|
191 return; |
|
192 } |
|
193 |
|
194 // and wire up the client message handling. |
|
195 try { |
|
196 initClientMessageHandler(); |
|
197 } |
|
198 catch (e) { |
|
199 Cu.reportError("FrameWorker: Error setting up event listener for chrome side of the worker: " + e + "\n" + e.stack); |
|
200 notifyWorkerError(); |
|
201 return; |
|
202 } |
|
203 |
|
204 // Now get the worker js code and eval it into the sandbox |
|
205 try { |
|
206 Cu.evalInSandbox(scriptText, sandbox, "1.8", workerWindow.location.href, 1); |
|
207 } catch (e) { |
|
208 Cu.reportError("FrameWorker: Error evaluating worker script for " + worker.name + ": " + e + "; " + |
|
209 (e.lineNumber ? ("Line #" + e.lineNumber) : "") + |
|
210 (e.stack ? ("\n" + e.stack) : "")); |
|
211 notifyWorkerError(); |
|
212 return; |
|
213 } |
|
214 |
|
215 // so finally we are ready to roll - dequeue all the pending connects |
|
216 worker.loaded = true; |
|
217 for (let [,port] of worker.ports) { // enumeration is in insertion order |
|
218 if (!port._entangled) { |
|
219 try { |
|
220 port._createWorkerAndEntangle(worker); |
|
221 } |
|
222 catch(e) { |
|
223 Cu.reportError("FrameWorker: Failed to entangle worker port: " + e + "\n" + e.stack); |
|
224 } |
|
225 } |
|
226 } |
|
227 }); |
|
228 |
|
229 // the 'unload' listener cleans up the worker and the sandbox. This |
|
230 // will be triggered by the window unloading as part of shutdown or reload. |
|
231 workerWindow.addEventListener("unload", function unloadListener() { |
|
232 workerWindow.removeEventListener("unload", unloadListener); |
|
233 worker.loaded = false; |
|
234 // No need to close ports - the worker probably wont see a |
|
235 // social.port-closing message and certainly isn't going to have time to |
|
236 // do anything if it did see it. |
|
237 worker.ports.clear(); |
|
238 if (sandbox) { |
|
239 Cu.nukeSandbox(sandbox); |
|
240 sandbox = null; |
|
241 } |
|
242 }); |
|
243 }, |
|
244 }; |
|
245 |
|
246 const FrameWorkerManager = { |
|
247 init: function() { |
|
248 // first, setup the docShell to disable some types of content |
|
249 docShell.allowAuth = false; |
|
250 docShell.allowPlugins = false; |
|
251 docShell.allowImages = false; |
|
252 docShell.allowMedia = false; |
|
253 docShell.allowWindowControl = false; |
|
254 |
|
255 addMessageListener("frameworker:init", this._onInit); |
|
256 addMessageListener("frameworker:connect", this._onConnect); |
|
257 addMessageListener("frameworker:port-message", this._onPortMessage); |
|
258 addMessageListener("frameworker:cookie-get", this._onCookieGet); |
|
259 }, |
|
260 |
|
261 // This new frameworker is being created. This should only be called once. |
|
262 _onInit: function(msg) { |
|
263 let {url, name, origin, exposeLocalStorage} = msg.data; |
|
264 frameworker = new FrameWorker(url, name, origin, exposeLocalStorage); |
|
265 }, |
|
266 |
|
267 // A new port is being established for this frameworker. |
|
268 _onConnect: function(msg) { |
|
269 let port = new ClientPort(msg.data.portId); |
|
270 frameworker.ports.set(msg.data.portId, port); |
|
271 if (frameworker.loaded && !frameworker.reloading) |
|
272 port._createWorkerAndEntangle(frameworker); |
|
273 }, |
|
274 |
|
275 // A message related to a port. |
|
276 _onPortMessage: function(msg) { |
|
277 // find the "client" port for this message and have it post it into |
|
278 // the worker. |
|
279 let port = frameworker.ports.get(msg.data.portId); |
|
280 port._dopost(msg.data); |
|
281 }, |
|
282 |
|
283 _onCookieGet: function(msg) { |
|
284 sendAsyncMessage("frameworker:cookie-get-response", content.document.cookie); |
|
285 }, |
|
286 |
|
287 }; |
|
288 |
|
289 FrameWorkerManager.init(); |
|
290 |
|
291 // This is the message listener for the chrome side of the world - ie, the |
|
292 // port that exists with chrome permissions inside the <browser/> (ie, in the |
|
293 // content process if a remote browser is used). |
|
294 function initClientMessageHandler() { |
|
295 function _messageHandler(event) { |
|
296 // We will ignore all messages destined for otherType. |
|
297 let data = event.data; |
|
298 let portid = data.portId; |
|
299 let port; |
|
300 if (!data.portFromType || data.portFromType !== "worker") { |
|
301 // this is a message posted by ourself so ignore it. |
|
302 return; |
|
303 } |
|
304 switch (data.portTopic) { |
|
305 // No "port-create" here - client ports are created explicitly. |
|
306 case "port-connection-error": |
|
307 // onconnect failed, we cannot connect the port, the worker has |
|
308 // become invalid |
|
309 notifyWorkerError(); |
|
310 break; |
|
311 case "port-close": |
|
312 // the worker side of the port was closed, so close this side too. |
|
313 port = frameworker.ports.get(portid); |
|
314 if (!port) { |
|
315 // port already closed (which will happen when we call port.close() |
|
316 // below - the worker side will send us this message but we've |
|
317 // already closed it.) |
|
318 return; |
|
319 } |
|
320 frameworker.ports.delete(portid); |
|
321 port.close(); |
|
322 break; |
|
323 |
|
324 case "port-message": |
|
325 // the client posted a message to this worker port. |
|
326 port = frameworker.ports.get(portid); |
|
327 if (!port) { |
|
328 return; |
|
329 } |
|
330 port._onmessage(data.data); |
|
331 break; |
|
332 |
|
333 default: |
|
334 break; |
|
335 } |
|
336 } |
|
337 // this can probably go once debugged and working correctly! |
|
338 function messageHandler(event) { |
|
339 try { |
|
340 _messageHandler(event); |
|
341 } catch (ex) { |
|
342 Cu.reportError("FrameWorker: Error handling client port control message: " + ex + "\n" + ex.stack); |
|
343 } |
|
344 } |
|
345 content.addEventListener('message', messageHandler); |
|
346 } |
|
347 |
|
348 /** |
|
349 * ClientPort |
|
350 * |
|
351 * Client side of the entangled ports. This is just a shim that sends messages |
|
352 * back to the "parent" port living in the chrome process. |
|
353 * |
|
354 * constructor: |
|
355 * @param {integer} portid |
|
356 */ |
|
357 function ClientPort(portid) { |
|
358 // messages posted to the worker before the worker has loaded. |
|
359 this._pendingMessagesOutgoing = []; |
|
360 AbstractPort.call(this, portid); |
|
361 } |
|
362 |
|
363 ClientPort.prototype = { |
|
364 __proto__: AbstractPort.prototype, |
|
365 _portType: "client", |
|
366 // _entangled records if the port has ever been entangled (although may be |
|
367 // reset during a reload). |
|
368 _entangled: false, |
|
369 |
|
370 _createWorkerAndEntangle: function fw_ClientPort_createWorkerAndEntangle(worker) { |
|
371 this._entangled = true; |
|
372 this._postControlMessage("port-create"); |
|
373 for (let message of this._pendingMessagesOutgoing) { |
|
374 this._dopost(message); |
|
375 } |
|
376 this._pendingMessagesOutgoing = []; |
|
377 // The client side of the port might have been closed before it was |
|
378 // "entangled" with the worker, in which case we need to disentangle it |
|
379 if (this._closed) { |
|
380 worker.ports.delete(this._portid); |
|
381 } |
|
382 }, |
|
383 |
|
384 _dopost: function fw_ClientPort_dopost(data) { |
|
385 if (!this._entangled) { |
|
386 this._pendingMessagesOutgoing.push(data); |
|
387 } else { |
|
388 content.postMessage(data, "*"); |
|
389 } |
|
390 }, |
|
391 |
|
392 // we are just a "shim" - any messages we get are just forwarded back to |
|
393 // the chrome parent process. |
|
394 _onmessage: function(data) { |
|
395 sendAsyncMessage("frameworker:port-message", {portId: this._portid, data: data}); |
|
396 }, |
|
397 |
|
398 _onerror: function fw_ClientPort_onerror(err) { |
|
399 Cu.reportError("FrameWorker: Port " + this + " handler failed: " + err + "\n" + err.stack); |
|
400 }, |
|
401 |
|
402 close: function fw_ClientPort_close() { |
|
403 if (this._closed) { |
|
404 return; // already closed. |
|
405 } |
|
406 // a leaky abstraction due to the worker spec not specifying how the |
|
407 // other end of a port knows it is closing. |
|
408 this.postMessage({topic: "social.port-closing"}); |
|
409 AbstractPort.prototype.close.call(this); |
|
410 // this._pendingMessagesOutgoing should still be drained, as a closed |
|
411 // port will still get "entangled" quickly enough to deliver the messages. |
|
412 } |
|
413 } |
|
414 |
|
415 function notifyWorkerError() { |
|
416 sendAsyncMessage("frameworker:notify-worker-error", {origin: frameworker.origin}); |
|
417 } |
|
418 |
|
419 }()); |