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