diff options
Diffstat (limited to 'toolkit/jetpack/sdk/content/sandbox.js')
-rw-r--r-- | toolkit/jetpack/sdk/content/sandbox.js | 426 |
1 files changed, 426 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/content/sandbox.js b/toolkit/jetpack/sdk/content/sandbox.js new file mode 100644 index 000000000..096ba5c87 --- /dev/null +++ b/toolkit/jetpack/sdk/content/sandbox.js @@ -0,0 +1,426 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +'use strict'; + +module.metadata = { + 'stability': 'unstable' +}; + +const { Class } = require('../core/heritage'); +const { EventTarget } = require('../event/target'); +const { on, off, emit } = require('../event/core'); +const { events } = require('./sandbox/events'); +const { requiresAddonGlobal } = require('./utils'); +const { delay: async } = require('../lang/functional'); +const { Ci, Cu, Cc } = require('chrome'); +const timer = require('../timers'); +const { URL } = require('../url'); +const { sandbox, evaluate, load } = require('../loader/sandbox'); +const { merge } = require('../util/object'); +const { getTabForContentWindowNoShim } = require('../tabs/utils'); +const { getInnerId } = require('../window/utils'); +const { PlainTextConsole } = require('../console/plain-text'); +const { data } = require('../self');const { isChildLoader } = require('../remote/core'); +// WeakMap of sandboxes so we can access private values +const sandboxes = new WeakMap(); + +/* Trick the linker in order to ensure shipping these files in the XPI. + require('./content-worker.js'); + Then, retrieve URL of these files in the XPI: +*/ +var prefix = module.uri.split('sandbox.js')[0]; +const CONTENT_WORKER_URL = prefix + 'content-worker.js'; +const metadata = require('@loader/options').metadata; + +// Fetch additional list of domains to authorize access to for each content +// script. It is stored in manifest `metadata` field which contains +// package.json data. This list is originaly defined by authors in +// `permissions` attribute of their package.json addon file. +const permissions = (metadata && metadata['permissions']) || {}; +const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || []; + +const waiveSecurityMembrane = !!permissions['unsafe-content-script']; + +const nsIScriptSecurityManager = Ci.nsIScriptSecurityManager; +const secMan = Cc["@mozilla.org/scriptsecuritymanager;1"]. + getService(Ci.nsIScriptSecurityManager); + +const JS_VERSION = '1.8'; + +// Tests whether this window is loaded in a tab +function isWindowInTab(window) { + if (isChildLoader) { + let { frames } = require('../remote/child'); + let frame = frames.getFrameForWindow(window.top); + return frame && frame.isTab; + } + else { + // The deprecated sync worker API still does everything in the main process + return getTabForContentWindowNoShim(window); + } +} + +const WorkerSandbox = Class({ + implements: [ EventTarget ], + + /** + * Emit a message to the worker content sandbox + */ + emit: function emit(type, ...args) { + // JSON.stringify is buggy with cross-sandbox values, + // it may return "{}" on functions. Use a replacer to match them correctly. + let replacer = (k, v) => + typeof(v) === "function" + ? (type === "console" ? Function.toString.call(v) : void(0)) + : v; + + // Ensure having an asynchronous behavior + async(() => + emitToContent(this, JSON.stringify([type, ...args], replacer)) + ); + }, + + /** + * Synchronous version of `emit`. + * /!\ Should only be used when it is strictly mandatory /!\ + * Doesn't ensure passing only JSON values. + * Mainly used by context-menu in order to avoid breaking it. + */ + emitSync: function emitSync(...args) { + // because the arguments could be also non JSONable values, + // we need to ensure the array instance is created from + // the content's sandbox + return emitToContent(this, new modelFor(this).sandbox.Array(...args)); + }, + + /** + * Configures sandbox and loads content scripts into it. + * @param {Worker} worker + * content worker + */ + initialize: function WorkerSandbox(worker, window) { + let model = {}; + sandboxes.set(this, model); + model.worker = worker; + // We receive a wrapped window, that may be an xraywrapper if it's content + let proto = window; + + // TODO necessary? + // Ensure that `emit` has always the right `this` + this.emit = this.emit.bind(this); + this.emitSync = this.emitSync.bind(this); + + // Use expanded principal for content-script if the content is a + // regular web content for better isolation. + // (This behavior can be turned off for now with the unsafe-content-script + // flag to give addon developers time for making the necessary changes) + // But prevent it when the Worker isn't used for a content script but for + // injecting `addon` object into a Panel scope, for example. + // That's because: + // 1/ It is useless to use multiple domains as the worker is only used + // to communicate with the addon, + // 2/ By using it it would prevent the document to have access to any JS + // value of the worker. As JS values coming from multiple domain principals + // can't be accessed by 'mono-principals' (principal with only one domain). + // Even if this principal is for a domain that is specified in the multiple + // domain principal. + let principals = window; + let wantGlobalProperties = []; + let isSystemPrincipal = secMan.isSystemPrincipal( + window.document.nodePrincipal); + if (!isSystemPrincipal && !requiresAddonGlobal(worker)) { + if (EXPANDED_PRINCIPALS.length > 0) { + // We have to replace XHR constructor of the content document + // with a custom cross origin one, automagically added by platform code: + delete proto.XMLHttpRequest; + wantGlobalProperties.push('XMLHttpRequest'); + } + if (!waiveSecurityMembrane) + principals = EXPANDED_PRINCIPALS.concat(window); + } + + // Create the sandbox and bind it to window in order for content scripts to + // have access to all standard globals (window, document, ...) + let content = sandbox(principals, { + sandboxPrototype: proto, + wantXrays: !requiresAddonGlobal(worker), + wantGlobalProperties: wantGlobalProperties, + wantExportHelpers: true, + sameZoneAs: window, + metadata: { + SDKContentScript: true, + 'inner-window-id': getInnerId(window) + } + }); + model.sandbox = content; + + // We have to ensure that window.top and window.parent are the exact same + // object than window object, i.e. the sandbox global object. But not + // always, in case of iframes, top and parent are another window object. + let top = window.top === window ? content : content.top; + let parent = window.parent === window ? content : content.parent; + merge(content, { + // We need 'this === window === top' to be true in toplevel scope: + get window() { + return content; + }, + get top() { + return top; + }, + get parent() { + return parent; + } + }); + + // Use the Greasemonkey naming convention to provide access to the + // unwrapped window object so the content script can access document + // JavaScript values. + // NOTE: this functionality is experimental and may change or go away + // at any time! + // + // Note that because waivers aren't propagated between origins, we + // need the unsafeWindow getter to live in the sandbox. + var unsafeWindowGetter = + new content.Function('return window.wrappedJSObject || window;'); + Object.defineProperty(content, 'unsafeWindow', {get: unsafeWindowGetter}); + + // Load trusted code that will inject content script API. + let ContentWorker = load(content, CONTENT_WORKER_URL); + + // prepare a clean `self.options` + let options = 'contentScriptOptions' in worker ? + JSON.stringify(worker.contentScriptOptions) : + undefined; + + // Then call `inject` method and communicate with this script + // by trading two methods that allow to send events to the other side: + // - `onEvent` called by content script + // - `result.emitToContent` called by addon script + let onEvent = Cu.exportFunction(onContentEvent.bind(null, this), ContentWorker); + let chromeAPI = createChromeAPI(ContentWorker); + let result = Cu.waiveXrays(ContentWorker).inject(content, chromeAPI, onEvent, options); + + // Merge `emitToContent` into our private model of the + // WorkerSandbox so we can communicate with content script + model.emitToContent = result; + + let console = new PlainTextConsole(null, getInnerId(window)); + + // Handle messages send by this script: + setListeners(this, console); + + // Inject `addon` global into target document if document is trusted, + // `addon` in document is equivalent to `self` in content script. + if (requiresAddonGlobal(worker)) { + Object.defineProperty(getUnsafeWindow(window), 'addon', { + value: content.self, + configurable: true + } + ); + } + + // Inject our `console` into target document if worker doesn't have a tab + // (e.g Panel, PageWorker). + // `worker.tab` can't be used because bug 804935. + if (!isWindowInTab(window)) { + let win = getUnsafeWindow(window); + + // export our chrome console to content window, as described here: + // https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn + let con = Cu.createObjectIn(win); + + let genPropDesc = function genPropDesc(fun) { + return { enumerable: true, configurable: true, writable: true, + value: console[fun] }; + } + + const properties = { + log: genPropDesc('log'), + info: genPropDesc('info'), + warn: genPropDesc('warn'), + error: genPropDesc('error'), + debug: genPropDesc('debug'), + trace: genPropDesc('trace'), + dir: genPropDesc('dir'), + group: genPropDesc('group'), + groupCollapsed: genPropDesc('groupCollapsed'), + groupEnd: genPropDesc('groupEnd'), + time: genPropDesc('time'), + timeEnd: genPropDesc('timeEnd'), + profile: genPropDesc('profile'), + profileEnd: genPropDesc('profileEnd'), + exception: genPropDesc('exception'), + assert: genPropDesc('assert'), + count: genPropDesc('count'), + table: genPropDesc('table'), + clear: genPropDesc('clear'), + dirxml: genPropDesc('dirxml'), + markTimeline: genPropDesc('markTimeline'), + timeline: genPropDesc('timeline'), + timelineEnd: genPropDesc('timelineEnd'), + timeStamp: genPropDesc('timeStamp'), + }; + + Object.defineProperties(con, properties); + Cu.makeObjectPropsNormal(con); + + win.console = con; + }; + + emit(events, "content-script-before-inserted", { + window: window, + worker: worker + }); + + // The order of `contentScriptFile` and `contentScript` evaluation is + // intentional, so programs can load libraries like jQuery from script URLs + // and use them in scripts. + let contentScriptFile = ('contentScriptFile' in worker) + ? worker.contentScriptFile + : null, + contentScript = ('contentScript' in worker) + ? worker.contentScript + : null; + + if (contentScriptFile) + importScripts.apply(null, [this].concat(contentScriptFile)); + + if (contentScript) { + evaluateIn( + this, + Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript + ); + } + }, + destroy: function destroy(reason) { + if (typeof reason != 'string') + reason = ''; + this.emitSync('event', 'detach', reason); + let model = modelFor(this); + model.sandbox = null + model.worker = null; + }, + +}); + +exports.WorkerSandbox = WorkerSandbox; + +/** + * Imports scripts to the sandbox by reading files under urls and + * evaluating its source. If exception occurs during evaluation + * `'error'` event is emitted on the worker. + * This is actually an analog to the `importScript` method in web + * workers but in our case it's not exposed even though content + * scripts may be able to do it synchronously since IO operation + * takes place in the UI process. + */ +function importScripts (workerSandbox, ...urls) { + let { worker, sandbox } = modelFor(workerSandbox); + for (let i in urls) { + let contentScriptFile = data.url(urls[i]); + + try { + let uri = URL(contentScriptFile); + if (uri.scheme === 'resource') + load(sandbox, String(uri)); + else + throw Error('Unsupported `contentScriptFile` url: ' + String(uri)); + } + catch(e) { + emit(worker, 'error', e); + } + } +} + +function setListeners (workerSandbox, console) { + let { worker } = modelFor(workerSandbox); + // console.xxx calls + workerSandbox.on('console', function consoleListener (kind, ...args) { + console[kind].apply(console, args); + }); + + // self.postMessage calls + workerSandbox.on('message', function postMessage(data) { + // destroyed? + if (worker) + emit(worker, 'message', data); + }); + + // self.port.emit calls + workerSandbox.on('event', function portEmit (...eventArgs) { + // If not destroyed, emit event information to worker + // `eventArgs` has the event name as first element, + // and remaining elements are additional arguments to pass + if (worker) + emit.apply(null, [worker.port].concat(eventArgs)); + }); + + // unwrap, recreate and propagate async Errors thrown from content-script + workerSandbox.on('error', function onError({instanceOfError, value}) { + if (worker) { + let error = value; + if (instanceOfError) { + error = new Error(value.message, value.fileName, value.lineNumber); + error.stack = value.stack; + error.name = value.name; + } + emit(worker, 'error', error); + } + }); +} + +/** + * Evaluates code in the sandbox. + * @param {String} code + * JavaScript source to evaluate. + * @param {String} [filename='javascript:' + code] + * Name of the file + */ +function evaluateIn (workerSandbox, code, filename) { + let { worker, sandbox } = modelFor(workerSandbox); + try { + evaluate(sandbox, code, filename || 'javascript:' + code); + } + catch(e) { + emit(worker, 'error', e); + } +} + +/** + * Method called by the worker sandbox when it needs to send a message + */ +function onContentEvent (workerSandbox, args) { + // As `emit`, we ensure having an asynchronous behavior + async(function () { + // We emit event to chrome/addon listeners + emit.apply(null, [workerSandbox].concat(JSON.parse(args))); + }); +} + + +function modelFor (workerSandbox) { + return sandboxes.get(workerSandbox); +} + +function getUnsafeWindow (win) { + return win.wrappedJSObject || win; +} + +function emitToContent (workerSandbox, args) { + return modelFor(workerSandbox).emitToContent(args); +} + +function createChromeAPI (scope) { + return Cu.cloneInto({ + timers: { + setTimeout: timer.setTimeout.bind(timer), + setInterval: timer.setInterval.bind(timer), + clearTimeout: timer.clearTimeout.bind(timer), + clearInterval: timer.clearInterval.bind(timer), + }, + sandbox: { + evaluate: evaluate, + }, + }, scope, {cloneFunctions: true}); +} |