|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 'use strict'; |
|
5 |
|
6 module.metadata = { |
|
7 'stability': 'unstable' |
|
8 }; |
|
9 |
|
10 const { Class } = require('../core/heritage'); |
|
11 const { EventTarget } = require('../event/target'); |
|
12 const { on, off, emit } = require('../event/core'); |
|
13 const { requiresAddonGlobal } = require('./utils'); |
|
14 const { delay: async } = require('../lang/functional'); |
|
15 const { Ci, Cu, Cc } = require('chrome'); |
|
16 const timer = require('../timers'); |
|
17 const { URL } = require('../url'); |
|
18 const { sandbox, evaluate, load } = require('../loader/sandbox'); |
|
19 const { merge } = require('../util/object'); |
|
20 const { getTabForContentWindow } = require('../tabs/utils'); |
|
21 const { getInnerId } = require('../window/utils'); |
|
22 const { PlainTextConsole } = require('../console/plain-text'); |
|
23 |
|
24 // WeakMap of sandboxes so we can access private values |
|
25 const sandboxes = new WeakMap(); |
|
26 |
|
27 /* Trick the linker in order to ensure shipping these files in the XPI. |
|
28 require('./content-worker.js'); |
|
29 Then, retrieve URL of these files in the XPI: |
|
30 */ |
|
31 let prefix = module.uri.split('sandbox.js')[0]; |
|
32 const CONTENT_WORKER_URL = prefix + 'content-worker.js'; |
|
33 const metadata = require('@loader/options').metadata; |
|
34 |
|
35 // Fetch additional list of domains to authorize access to for each content |
|
36 // script. It is stored in manifest `metadata` field which contains |
|
37 // package.json data. This list is originaly defined by authors in |
|
38 // `permissions` attribute of their package.json addon file. |
|
39 const permissions = (metadata && metadata['permissions']) || {}; |
|
40 const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || []; |
|
41 |
|
42 const waiveSecurityMembrane = !!permissions['unsafe-content-script']; |
|
43 |
|
44 const nsIScriptSecurityManager = Ci.nsIScriptSecurityManager; |
|
45 const secMan = Cc["@mozilla.org/scriptsecuritymanager;1"]. |
|
46 getService(Ci.nsIScriptSecurityManager); |
|
47 |
|
48 const JS_VERSION = '1.8'; |
|
49 |
|
50 const WorkerSandbox = Class({ |
|
51 implements: [ EventTarget ], |
|
52 |
|
53 /** |
|
54 * Emit a message to the worker content sandbox |
|
55 */ |
|
56 emit: function emit(type, ...args) { |
|
57 // JSON.stringify is buggy with cross-sandbox values, |
|
58 // it may return "{}" on functions. Use a replacer to match them correctly. |
|
59 let replacer = (k, v) => |
|
60 typeof(v) === "function" |
|
61 ? (type === "console" ? Function.toString.call(v) : void(0)) |
|
62 : v; |
|
63 |
|
64 // Ensure having an asynchronous behavior |
|
65 async(() => |
|
66 emitToContent(this, JSON.stringify([type, ...args], replacer)) |
|
67 ); |
|
68 }, |
|
69 |
|
70 /** |
|
71 * Synchronous version of `emit`. |
|
72 * /!\ Should only be used when it is strictly mandatory /!\ |
|
73 * Doesn't ensure passing only JSON values. |
|
74 * Mainly used by context-menu in order to avoid breaking it. |
|
75 */ |
|
76 emitSync: function emitSync(...args) { |
|
77 return emitToContent(this, args); |
|
78 }, |
|
79 |
|
80 /** |
|
81 * Tells if content script has at least one listener registered for one event, |
|
82 * through `self.on('xxx', ...)`. |
|
83 * /!\ Shouldn't be used. Implemented to avoid breaking context-menu API. |
|
84 */ |
|
85 hasListenerFor: function hasListenerFor(name) { |
|
86 return modelFor(this).hasListenerFor(name); |
|
87 }, |
|
88 |
|
89 /** |
|
90 * Configures sandbox and loads content scripts into it. |
|
91 * @param {Worker} worker |
|
92 * content worker |
|
93 */ |
|
94 initialize: function WorkerSandbox(worker, window) { |
|
95 let model = {}; |
|
96 sandboxes.set(this, model); |
|
97 model.worker = worker; |
|
98 // We receive a wrapped window, that may be an xraywrapper if it's content |
|
99 let proto = window; |
|
100 |
|
101 // TODO necessary? |
|
102 // Ensure that `emit` has always the right `this` |
|
103 this.emit = this.emit.bind(this); |
|
104 this.emitSync = this.emitSync.bind(this); |
|
105 |
|
106 // Use expanded principal for content-script if the content is a |
|
107 // regular web content for better isolation. |
|
108 // (This behavior can be turned off for now with the unsafe-content-script |
|
109 // flag to give addon developers time for making the necessary changes) |
|
110 // But prevent it when the Worker isn't used for a content script but for |
|
111 // injecting `addon` object into a Panel, Widget, ... scope. |
|
112 // That's because: |
|
113 // 1/ It is useless to use multiple domains as the worker is only used |
|
114 // to communicate with the addon, |
|
115 // 2/ By using it it would prevent the document to have access to any JS |
|
116 // value of the worker. As JS values coming from multiple domain principals |
|
117 // can't be accessed by 'mono-principals' (principal with only one domain). |
|
118 // Even if this principal is for a domain that is specified in the multiple |
|
119 // domain principal. |
|
120 let principals = window; |
|
121 let wantGlobalProperties = []; |
|
122 let isSystemPrincipal = secMan.isSystemPrincipal( |
|
123 window.document.nodePrincipal); |
|
124 if (!isSystemPrincipal && !requiresAddonGlobal(worker)) { |
|
125 if (EXPANDED_PRINCIPALS.length > 0) { |
|
126 // We have to replace XHR constructor of the content document |
|
127 // with a custom cross origin one, automagically added by platform code: |
|
128 delete proto.XMLHttpRequest; |
|
129 wantGlobalProperties.push('XMLHttpRequest'); |
|
130 } |
|
131 if (!waiveSecurityMembrane) |
|
132 principals = EXPANDED_PRINCIPALS.concat(window); |
|
133 } |
|
134 |
|
135 // Instantiate trusted code in another Sandbox in order to prevent content |
|
136 // script from messing with standard classes used by proxy and API code. |
|
137 let apiSandbox = sandbox(principals, { wantXrays: true, sameZoneAs: window }); |
|
138 apiSandbox.console = console; |
|
139 |
|
140 // Create the sandbox and bind it to window in order for content scripts to |
|
141 // have access to all standard globals (window, document, ...) |
|
142 let content = sandbox(principals, { |
|
143 sandboxPrototype: proto, |
|
144 wantXrays: true, |
|
145 wantGlobalProperties: wantGlobalProperties, |
|
146 wantExportHelpers: true, |
|
147 sameZoneAs: window, |
|
148 metadata: { |
|
149 SDKContentScript: true, |
|
150 'inner-window-id': getInnerId(window) |
|
151 } |
|
152 }); |
|
153 model.sandbox = content; |
|
154 |
|
155 // We have to ensure that window.top and window.parent are the exact same |
|
156 // object than window object, i.e. the sandbox global object. But not |
|
157 // always, in case of iframes, top and parent are another window object. |
|
158 let top = window.top === window ? content : content.top; |
|
159 let parent = window.parent === window ? content : content.parent; |
|
160 merge(content, { |
|
161 // We need 'this === window === top' to be true in toplevel scope: |
|
162 get window() content, |
|
163 get top() top, |
|
164 get parent() parent, |
|
165 // Use the Greasemonkey naming convention to provide access to the |
|
166 // unwrapped window object so the content script can access document |
|
167 // JavaScript values. |
|
168 // NOTE: this functionality is experimental and may change or go away |
|
169 // at any time! |
|
170 get unsafeWindow() window.wrappedJSObject |
|
171 }); |
|
172 |
|
173 // Load trusted code that will inject content script API. |
|
174 // We need to expose JS objects defined in same principal in order to |
|
175 // avoid having any kind of wrapper. |
|
176 load(apiSandbox, CONTENT_WORKER_URL); |
|
177 |
|
178 // prepare a clean `self.options` |
|
179 let options = 'contentScriptOptions' in worker ? |
|
180 JSON.stringify(worker.contentScriptOptions) : |
|
181 undefined; |
|
182 |
|
183 // Then call `inject` method and communicate with this script |
|
184 // by trading two methods that allow to send events to the other side: |
|
185 // - `onEvent` called by content script |
|
186 // - `result.emitToContent` called by addon script |
|
187 // Bug 758203: We have to explicitely define `__exposedProps__` in order |
|
188 // to allow access to these chrome object attributes from this sandbox with |
|
189 // content priviledges |
|
190 // https://developer.mozilla.org/en/XPConnect_wrappers#Other_security_wrappers |
|
191 let onEvent = onContentEvent.bind(null, this); |
|
192 // `ContentWorker` is defined in CONTENT_WORKER_URL file |
|
193 let chromeAPI = createChromeAPI(); |
|
194 let result = apiSandbox.ContentWorker.inject(content, chromeAPI, onEvent, options); |
|
195 |
|
196 // Merge `emitToContent` and `hasListenerFor` into our private |
|
197 // model of the WorkerSandbox so we can communicate with content |
|
198 // script |
|
199 merge(model, result); |
|
200 |
|
201 let console = new PlainTextConsole(null, getInnerId(window)); |
|
202 |
|
203 // Handle messages send by this script: |
|
204 setListeners(this, console); |
|
205 |
|
206 // Inject `addon` global into target document if document is trusted, |
|
207 // `addon` in document is equivalent to `self` in content script. |
|
208 if (requiresAddonGlobal(worker)) { |
|
209 Object.defineProperty(getUnsafeWindow(window), 'addon', { |
|
210 value: content.self |
|
211 } |
|
212 ); |
|
213 } |
|
214 |
|
215 // Inject our `console` into target document if worker doesn't have a tab |
|
216 // (e.g Panel, PageWorker, Widget). |
|
217 // `worker.tab` can't be used because bug 804935. |
|
218 if (!getTabForContentWindow(window)) { |
|
219 let win = getUnsafeWindow(window); |
|
220 |
|
221 // export our chrome console to content window, as described here: |
|
222 // https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn |
|
223 let con = Cu.createObjectIn(win); |
|
224 |
|
225 let genPropDesc = function genPropDesc(fun) { |
|
226 return { enumerable: true, configurable: true, writable: true, |
|
227 value: console[fun] }; |
|
228 } |
|
229 |
|
230 const properties = { |
|
231 log: genPropDesc('log'), |
|
232 info: genPropDesc('info'), |
|
233 warn: genPropDesc('warn'), |
|
234 error: genPropDesc('error'), |
|
235 debug: genPropDesc('debug'), |
|
236 trace: genPropDesc('trace'), |
|
237 dir: genPropDesc('dir'), |
|
238 group: genPropDesc('group'), |
|
239 groupCollapsed: genPropDesc('groupCollapsed'), |
|
240 groupEnd: genPropDesc('groupEnd'), |
|
241 time: genPropDesc('time'), |
|
242 timeEnd: genPropDesc('timeEnd'), |
|
243 profile: genPropDesc('profile'), |
|
244 profileEnd: genPropDesc('profileEnd'), |
|
245 __noSuchMethod__: { enumerable: true, configurable: true, writable: true, |
|
246 value: function() {} } |
|
247 }; |
|
248 |
|
249 Object.defineProperties(con, properties); |
|
250 Cu.makeObjectPropsNormal(con); |
|
251 |
|
252 win.console = con; |
|
253 }; |
|
254 |
|
255 // The order of `contentScriptFile` and `contentScript` evaluation is |
|
256 // intentional, so programs can load libraries like jQuery from script URLs |
|
257 // and use them in scripts. |
|
258 let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile |
|
259 : null, |
|
260 contentScript = ('contentScript' in worker) ? worker.contentScript : null; |
|
261 |
|
262 if (contentScriptFile) |
|
263 importScripts.apply(null, [this].concat(contentScriptFile)); |
|
264 if (contentScript) { |
|
265 evaluateIn( |
|
266 this, |
|
267 Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript |
|
268 ); |
|
269 } |
|
270 }, |
|
271 destroy: function destroy(reason) { |
|
272 if (typeof reason != 'string') |
|
273 reason = ''; |
|
274 this.emitSync('event', 'detach', reason); |
|
275 let model = modelFor(this); |
|
276 model.sandbox = null |
|
277 model.worker = null; |
|
278 }, |
|
279 |
|
280 }); |
|
281 |
|
282 exports.WorkerSandbox = WorkerSandbox; |
|
283 |
|
284 /** |
|
285 * Imports scripts to the sandbox by reading files under urls and |
|
286 * evaluating its source. If exception occurs during evaluation |
|
287 * `'error'` event is emitted on the worker. |
|
288 * This is actually an analog to the `importScript` method in web |
|
289 * workers but in our case it's not exposed even though content |
|
290 * scripts may be able to do it synchronously since IO operation |
|
291 * takes place in the UI process. |
|
292 */ |
|
293 function importScripts (workerSandbox, ...urls) { |
|
294 let { worker, sandbox } = modelFor(workerSandbox); |
|
295 for (let i in urls) { |
|
296 let contentScriptFile = urls[i]; |
|
297 try { |
|
298 let uri = URL(contentScriptFile); |
|
299 if (uri.scheme === 'resource') |
|
300 load(sandbox, String(uri)); |
|
301 else |
|
302 throw Error('Unsupported `contentScriptFile` url: ' + String(uri)); |
|
303 } |
|
304 catch(e) { |
|
305 emit(worker, 'error', e); |
|
306 } |
|
307 } |
|
308 } |
|
309 |
|
310 function setListeners (workerSandbox, console) { |
|
311 let { worker } = modelFor(workerSandbox); |
|
312 // console.xxx calls |
|
313 workerSandbox.on('console', function consoleListener (kind, ...args) { |
|
314 console[kind].apply(console, args); |
|
315 }); |
|
316 |
|
317 // self.postMessage calls |
|
318 workerSandbox.on('message', function postMessage(data) { |
|
319 // destroyed? |
|
320 if (worker) |
|
321 emit(worker, 'message', data); |
|
322 }); |
|
323 |
|
324 // self.port.emit calls |
|
325 workerSandbox.on('event', function portEmit (...eventArgs) { |
|
326 // If not destroyed, emit event information to worker |
|
327 // `eventArgs` has the event name as first element, |
|
328 // and remaining elements are additional arguments to pass |
|
329 if (worker) |
|
330 emit.apply(null, [worker.port].concat(eventArgs)); |
|
331 }); |
|
332 |
|
333 // unwrap, recreate and propagate async Errors thrown from content-script |
|
334 workerSandbox.on('error', function onError({instanceOfError, value}) { |
|
335 if (worker) { |
|
336 let error = value; |
|
337 if (instanceOfError) { |
|
338 error = new Error(value.message, value.fileName, value.lineNumber); |
|
339 error.stack = value.stack; |
|
340 error.name = value.name; |
|
341 } |
|
342 emit(worker, 'error', error); |
|
343 } |
|
344 }); |
|
345 } |
|
346 |
|
347 /** |
|
348 * Evaluates code in the sandbox. |
|
349 * @param {String} code |
|
350 * JavaScript source to evaluate. |
|
351 * @param {String} [filename='javascript:' + code] |
|
352 * Name of the file |
|
353 */ |
|
354 function evaluateIn (workerSandbox, code, filename) { |
|
355 let { worker, sandbox } = modelFor(workerSandbox); |
|
356 try { |
|
357 evaluate(sandbox, code, filename || 'javascript:' + code); |
|
358 } |
|
359 catch(e) { |
|
360 emit(worker, 'error', e); |
|
361 } |
|
362 } |
|
363 |
|
364 /** |
|
365 * Method called by the worker sandbox when it needs to send a message |
|
366 */ |
|
367 function onContentEvent (workerSandbox, args) { |
|
368 // As `emit`, we ensure having an asynchronous behavior |
|
369 async(function () { |
|
370 // We emit event to chrome/addon listeners |
|
371 emit.apply(null, [workerSandbox].concat(JSON.parse(args))); |
|
372 }); |
|
373 } |
|
374 |
|
375 |
|
376 function modelFor (workerSandbox) { |
|
377 return sandboxes.get(workerSandbox); |
|
378 } |
|
379 |
|
380 function getUnsafeWindow (win) { |
|
381 return win.wrappedJSObject || win; |
|
382 } |
|
383 |
|
384 function emitToContent (workerSandbox, args) { |
|
385 return modelFor(workerSandbox).emitToContent(args); |
|
386 } |
|
387 |
|
388 function createChromeAPI () { |
|
389 return { |
|
390 timers: { |
|
391 setTimeout: timer.setTimeout, |
|
392 setInterval: timer.setInterval, |
|
393 clearTimeout: timer.clearTimeout, |
|
394 clearInterval: timer.clearInterval, |
|
395 __exposedProps__: { |
|
396 setTimeout: 'r', |
|
397 setInterval: 'r', |
|
398 clearTimeout: 'r', |
|
399 clearInterval: 'r' |
|
400 }, |
|
401 }, |
|
402 sandbox: { |
|
403 evaluate: evaluate, |
|
404 __exposedProps__: { |
|
405 evaluate: 'r' |
|
406 } |
|
407 }, |
|
408 __exposedProps__: { |
|
409 timers: 'r', |
|
410 sandbox: 'r' |
|
411 } |
|
412 }; |
|
413 } |