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.

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

mercurial