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

changeset 0
6474c204b198
     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 +}

mercurial