addon-sdk/source/lib/sdk/content/sandbox.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

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

mercurial