1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/addon-sdk/source/lib/sdk/content/sandbox.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,413 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 +'use strict'; 1.8 + 1.9 +module.metadata = { 1.10 + 'stability': 'unstable' 1.11 +}; 1.12 + 1.13 +const { Class } = require('../core/heritage'); 1.14 +const { EventTarget } = require('../event/target'); 1.15 +const { on, off, emit } = require('../event/core'); 1.16 +const { requiresAddonGlobal } = require('./utils'); 1.17 +const { delay: async } = require('../lang/functional'); 1.18 +const { Ci, Cu, Cc } = require('chrome'); 1.19 +const timer = require('../timers'); 1.20 +const { URL } = require('../url'); 1.21 +const { sandbox, evaluate, load } = require('../loader/sandbox'); 1.22 +const { merge } = require('../util/object'); 1.23 +const { getTabForContentWindow } = require('../tabs/utils'); 1.24 +const { getInnerId } = require('../window/utils'); 1.25 +const { PlainTextConsole } = require('../console/plain-text'); 1.26 + 1.27 +// WeakMap of sandboxes so we can access private values 1.28 +const sandboxes = new WeakMap(); 1.29 + 1.30 +/* Trick the linker in order to ensure shipping these files in the XPI. 1.31 + require('./content-worker.js'); 1.32 + Then, retrieve URL of these files in the XPI: 1.33 +*/ 1.34 +let prefix = module.uri.split('sandbox.js')[0]; 1.35 +const CONTENT_WORKER_URL = prefix + 'content-worker.js'; 1.36 +const metadata = require('@loader/options').metadata; 1.37 + 1.38 +// Fetch additional list of domains to authorize access to for each content 1.39 +// script. It is stored in manifest `metadata` field which contains 1.40 +// package.json data. This list is originaly defined by authors in 1.41 +// `permissions` attribute of their package.json addon file. 1.42 +const permissions = (metadata && metadata['permissions']) || {}; 1.43 +const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || []; 1.44 + 1.45 +const waiveSecurityMembrane = !!permissions['unsafe-content-script']; 1.46 + 1.47 +const nsIScriptSecurityManager = Ci.nsIScriptSecurityManager; 1.48 +const secMan = Cc["@mozilla.org/scriptsecuritymanager;1"]. 1.49 + getService(Ci.nsIScriptSecurityManager); 1.50 + 1.51 +const JS_VERSION = '1.8'; 1.52 + 1.53 +const WorkerSandbox = Class({ 1.54 + implements: [ EventTarget ], 1.55 + 1.56 + /** 1.57 + * Emit a message to the worker content sandbox 1.58 + */ 1.59 + emit: function emit(type, ...args) { 1.60 + // JSON.stringify is buggy with cross-sandbox values, 1.61 + // it may return "{}" on functions. Use a replacer to match them correctly. 1.62 + let replacer = (k, v) => 1.63 + typeof(v) === "function" 1.64 + ? (type === "console" ? Function.toString.call(v) : void(0)) 1.65 + : v; 1.66 + 1.67 + // Ensure having an asynchronous behavior 1.68 + async(() => 1.69 + emitToContent(this, JSON.stringify([type, ...args], replacer)) 1.70 + ); 1.71 + }, 1.72 + 1.73 + /** 1.74 + * Synchronous version of `emit`. 1.75 + * /!\ Should only be used when it is strictly mandatory /!\ 1.76 + * Doesn't ensure passing only JSON values. 1.77 + * Mainly used by context-menu in order to avoid breaking it. 1.78 + */ 1.79 + emitSync: function emitSync(...args) { 1.80 + return emitToContent(this, args); 1.81 + }, 1.82 + 1.83 + /** 1.84 + * Tells if content script has at least one listener registered for one event, 1.85 + * through `self.on('xxx', ...)`. 1.86 + * /!\ Shouldn't be used. Implemented to avoid breaking context-menu API. 1.87 + */ 1.88 + hasListenerFor: function hasListenerFor(name) { 1.89 + return modelFor(this).hasListenerFor(name); 1.90 + }, 1.91 + 1.92 + /** 1.93 + * Configures sandbox and loads content scripts into it. 1.94 + * @param {Worker} worker 1.95 + * content worker 1.96 + */ 1.97 + initialize: function WorkerSandbox(worker, window) { 1.98 + let model = {}; 1.99 + sandboxes.set(this, model); 1.100 + model.worker = worker; 1.101 + // We receive a wrapped window, that may be an xraywrapper if it's content 1.102 + let proto = window; 1.103 + 1.104 + // TODO necessary? 1.105 + // Ensure that `emit` has always the right `this` 1.106 + this.emit = this.emit.bind(this); 1.107 + this.emitSync = this.emitSync.bind(this); 1.108 + 1.109 + // Use expanded principal for content-script if the content is a 1.110 + // regular web content for better isolation. 1.111 + // (This behavior can be turned off for now with the unsafe-content-script 1.112 + // flag to give addon developers time for making the necessary changes) 1.113 + // But prevent it when the Worker isn't used for a content script but for 1.114 + // injecting `addon` object into a Panel, Widget, ... scope. 1.115 + // That's because: 1.116 + // 1/ It is useless to use multiple domains as the worker is only used 1.117 + // to communicate with the addon, 1.118 + // 2/ By using it it would prevent the document to have access to any JS 1.119 + // value of the worker. As JS values coming from multiple domain principals 1.120 + // can't be accessed by 'mono-principals' (principal with only one domain). 1.121 + // Even if this principal is for a domain that is specified in the multiple 1.122 + // domain principal. 1.123 + let principals = window; 1.124 + let wantGlobalProperties = []; 1.125 + let isSystemPrincipal = secMan.isSystemPrincipal( 1.126 + window.document.nodePrincipal); 1.127 + if (!isSystemPrincipal && !requiresAddonGlobal(worker)) { 1.128 + if (EXPANDED_PRINCIPALS.length > 0) { 1.129 + // We have to replace XHR constructor of the content document 1.130 + // with a custom cross origin one, automagically added by platform code: 1.131 + delete proto.XMLHttpRequest; 1.132 + wantGlobalProperties.push('XMLHttpRequest'); 1.133 + } 1.134 + if (!waiveSecurityMembrane) 1.135 + principals = EXPANDED_PRINCIPALS.concat(window); 1.136 + } 1.137 + 1.138 + // Instantiate trusted code in another Sandbox in order to prevent content 1.139 + // script from messing with standard classes used by proxy and API code. 1.140 + let apiSandbox = sandbox(principals, { wantXrays: true, sameZoneAs: window }); 1.141 + apiSandbox.console = console; 1.142 + 1.143 + // Create the sandbox and bind it to window in order for content scripts to 1.144 + // have access to all standard globals (window, document, ...) 1.145 + let content = sandbox(principals, { 1.146 + sandboxPrototype: proto, 1.147 + wantXrays: true, 1.148 + wantGlobalProperties: wantGlobalProperties, 1.149 + wantExportHelpers: true, 1.150 + sameZoneAs: window, 1.151 + metadata: { 1.152 + SDKContentScript: true, 1.153 + 'inner-window-id': getInnerId(window) 1.154 + } 1.155 + }); 1.156 + model.sandbox = content; 1.157 + 1.158 + // We have to ensure that window.top and window.parent are the exact same 1.159 + // object than window object, i.e. the sandbox global object. But not 1.160 + // always, in case of iframes, top and parent are another window object. 1.161 + let top = window.top === window ? content : content.top; 1.162 + let parent = window.parent === window ? content : content.parent; 1.163 + merge(content, { 1.164 + // We need 'this === window === top' to be true in toplevel scope: 1.165 + get window() content, 1.166 + get top() top, 1.167 + get parent() parent, 1.168 + // Use the Greasemonkey naming convention to provide access to the 1.169 + // unwrapped window object so the content script can access document 1.170 + // JavaScript values. 1.171 + // NOTE: this functionality is experimental and may change or go away 1.172 + // at any time! 1.173 + get unsafeWindow() window.wrappedJSObject 1.174 + }); 1.175 + 1.176 + // Load trusted code that will inject content script API. 1.177 + // We need to expose JS objects defined in same principal in order to 1.178 + // avoid having any kind of wrapper. 1.179 + load(apiSandbox, CONTENT_WORKER_URL); 1.180 + 1.181 + // prepare a clean `self.options` 1.182 + let options = 'contentScriptOptions' in worker ? 1.183 + JSON.stringify(worker.contentScriptOptions) : 1.184 + undefined; 1.185 + 1.186 + // Then call `inject` method and communicate with this script 1.187 + // by trading two methods that allow to send events to the other side: 1.188 + // - `onEvent` called by content script 1.189 + // - `result.emitToContent` called by addon script 1.190 + // Bug 758203: We have to explicitely define `__exposedProps__` in order 1.191 + // to allow access to these chrome object attributes from this sandbox with 1.192 + // content priviledges 1.193 + // https://developer.mozilla.org/en/XPConnect_wrappers#Other_security_wrappers 1.194 + let onEvent = onContentEvent.bind(null, this); 1.195 + // `ContentWorker` is defined in CONTENT_WORKER_URL file 1.196 + let chromeAPI = createChromeAPI(); 1.197 + let result = apiSandbox.ContentWorker.inject(content, chromeAPI, onEvent, options); 1.198 + 1.199 + // Merge `emitToContent` and `hasListenerFor` into our private 1.200 + // model of the WorkerSandbox so we can communicate with content 1.201 + // script 1.202 + merge(model, result); 1.203 + 1.204 + let console = new PlainTextConsole(null, getInnerId(window)); 1.205 + 1.206 + // Handle messages send by this script: 1.207 + setListeners(this, console); 1.208 + 1.209 + // Inject `addon` global into target document if document is trusted, 1.210 + // `addon` in document is equivalent to `self` in content script. 1.211 + if (requiresAddonGlobal(worker)) { 1.212 + Object.defineProperty(getUnsafeWindow(window), 'addon', { 1.213 + value: content.self 1.214 + } 1.215 + ); 1.216 + } 1.217 + 1.218 + // Inject our `console` into target document if worker doesn't have a tab 1.219 + // (e.g Panel, PageWorker, Widget). 1.220 + // `worker.tab` can't be used because bug 804935. 1.221 + if (!getTabForContentWindow(window)) { 1.222 + let win = getUnsafeWindow(window); 1.223 + 1.224 + // export our chrome console to content window, as described here: 1.225 + // https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn 1.226 + let con = Cu.createObjectIn(win); 1.227 + 1.228 + let genPropDesc = function genPropDesc(fun) { 1.229 + return { enumerable: true, configurable: true, writable: true, 1.230 + value: console[fun] }; 1.231 + } 1.232 + 1.233 + const properties = { 1.234 + log: genPropDesc('log'), 1.235 + info: genPropDesc('info'), 1.236 + warn: genPropDesc('warn'), 1.237 + error: genPropDesc('error'), 1.238 + debug: genPropDesc('debug'), 1.239 + trace: genPropDesc('trace'), 1.240 + dir: genPropDesc('dir'), 1.241 + group: genPropDesc('group'), 1.242 + groupCollapsed: genPropDesc('groupCollapsed'), 1.243 + groupEnd: genPropDesc('groupEnd'), 1.244 + time: genPropDesc('time'), 1.245 + timeEnd: genPropDesc('timeEnd'), 1.246 + profile: genPropDesc('profile'), 1.247 + profileEnd: genPropDesc('profileEnd'), 1.248 + __noSuchMethod__: { enumerable: true, configurable: true, writable: true, 1.249 + value: function() {} } 1.250 + }; 1.251 + 1.252 + Object.defineProperties(con, properties); 1.253 + Cu.makeObjectPropsNormal(con); 1.254 + 1.255 + win.console = con; 1.256 + }; 1.257 + 1.258 + // The order of `contentScriptFile` and `contentScript` evaluation is 1.259 + // intentional, so programs can load libraries like jQuery from script URLs 1.260 + // and use them in scripts. 1.261 + let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile 1.262 + : null, 1.263 + contentScript = ('contentScript' in worker) ? worker.contentScript : null; 1.264 + 1.265 + if (contentScriptFile) 1.266 + importScripts.apply(null, [this].concat(contentScriptFile)); 1.267 + if (contentScript) { 1.268 + evaluateIn( 1.269 + this, 1.270 + Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript 1.271 + ); 1.272 + } 1.273 + }, 1.274 + destroy: function destroy(reason) { 1.275 + if (typeof reason != 'string') 1.276 + reason = ''; 1.277 + this.emitSync('event', 'detach', reason); 1.278 + let model = modelFor(this); 1.279 + model.sandbox = null 1.280 + model.worker = null; 1.281 + }, 1.282 + 1.283 +}); 1.284 + 1.285 +exports.WorkerSandbox = WorkerSandbox; 1.286 + 1.287 +/** 1.288 + * Imports scripts to the sandbox by reading files under urls and 1.289 + * evaluating its source. If exception occurs during evaluation 1.290 + * `'error'` event is emitted on the worker. 1.291 + * This is actually an analog to the `importScript` method in web 1.292 + * workers but in our case it's not exposed even though content 1.293 + * scripts may be able to do it synchronously since IO operation 1.294 + * takes place in the UI process. 1.295 + */ 1.296 +function importScripts (workerSandbox, ...urls) { 1.297 + let { worker, sandbox } = modelFor(workerSandbox); 1.298 + for (let i in urls) { 1.299 + let contentScriptFile = urls[i]; 1.300 + try { 1.301 + let uri = URL(contentScriptFile); 1.302 + if (uri.scheme === 'resource') 1.303 + load(sandbox, String(uri)); 1.304 + else 1.305 + throw Error('Unsupported `contentScriptFile` url: ' + String(uri)); 1.306 + } 1.307 + catch(e) { 1.308 + emit(worker, 'error', e); 1.309 + } 1.310 + } 1.311 +} 1.312 + 1.313 +function setListeners (workerSandbox, console) { 1.314 + let { worker } = modelFor(workerSandbox); 1.315 + // console.xxx calls 1.316 + workerSandbox.on('console', function consoleListener (kind, ...args) { 1.317 + console[kind].apply(console, args); 1.318 + }); 1.319 + 1.320 + // self.postMessage calls 1.321 + workerSandbox.on('message', function postMessage(data) { 1.322 + // destroyed? 1.323 + if (worker) 1.324 + emit(worker, 'message', data); 1.325 + }); 1.326 + 1.327 + // self.port.emit calls 1.328 + workerSandbox.on('event', function portEmit (...eventArgs) { 1.329 + // If not destroyed, emit event information to worker 1.330 + // `eventArgs` has the event name as first element, 1.331 + // and remaining elements are additional arguments to pass 1.332 + if (worker) 1.333 + emit.apply(null, [worker.port].concat(eventArgs)); 1.334 + }); 1.335 + 1.336 + // unwrap, recreate and propagate async Errors thrown from content-script 1.337 + workerSandbox.on('error', function onError({instanceOfError, value}) { 1.338 + if (worker) { 1.339 + let error = value; 1.340 + if (instanceOfError) { 1.341 + error = new Error(value.message, value.fileName, value.lineNumber); 1.342 + error.stack = value.stack; 1.343 + error.name = value.name; 1.344 + } 1.345 + emit(worker, 'error', error); 1.346 + } 1.347 + }); 1.348 +} 1.349 + 1.350 +/** 1.351 + * Evaluates code in the sandbox. 1.352 + * @param {String} code 1.353 + * JavaScript source to evaluate. 1.354 + * @param {String} [filename='javascript:' + code] 1.355 + * Name of the file 1.356 + */ 1.357 +function evaluateIn (workerSandbox, code, filename) { 1.358 + let { worker, sandbox } = modelFor(workerSandbox); 1.359 + try { 1.360 + evaluate(sandbox, code, filename || 'javascript:' + code); 1.361 + } 1.362 + catch(e) { 1.363 + emit(worker, 'error', e); 1.364 + } 1.365 +} 1.366 + 1.367 +/** 1.368 + * Method called by the worker sandbox when it needs to send a message 1.369 + */ 1.370 +function onContentEvent (workerSandbox, args) { 1.371 + // As `emit`, we ensure having an asynchronous behavior 1.372 + async(function () { 1.373 + // We emit event to chrome/addon listeners 1.374 + emit.apply(null, [workerSandbox].concat(JSON.parse(args))); 1.375 + }); 1.376 +} 1.377 + 1.378 + 1.379 +function modelFor (workerSandbox) { 1.380 + return sandboxes.get(workerSandbox); 1.381 +} 1.382 + 1.383 +function getUnsafeWindow (win) { 1.384 + return win.wrappedJSObject || win; 1.385 +} 1.386 + 1.387 +function emitToContent (workerSandbox, args) { 1.388 + return modelFor(workerSandbox).emitToContent(args); 1.389 +} 1.390 + 1.391 +function createChromeAPI () { 1.392 + return { 1.393 + timers: { 1.394 + setTimeout: timer.setTimeout, 1.395 + setInterval: timer.setInterval, 1.396 + clearTimeout: timer.clearTimeout, 1.397 + clearInterval: timer.clearInterval, 1.398 + __exposedProps__: { 1.399 + setTimeout: 'r', 1.400 + setInterval: 'r', 1.401 + clearTimeout: 'r', 1.402 + clearInterval: 'r' 1.403 + }, 1.404 + }, 1.405 + sandbox: { 1.406 + evaluate: evaluate, 1.407 + __exposedProps__: { 1.408 + evaluate: 'r' 1.409 + } 1.410 + }, 1.411 + __exposedProps__: { 1.412 + timers: 'r', 1.413 + sandbox: 'r' 1.414 + } 1.415 + }; 1.416 +}