tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

evaluate.sys.mjs (10142B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineESModuleGetters(lazy, {
     10  error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
     11 });
     12 
     13 const ARGUMENTS = "__webDriverArguments";
     14 const CALLBACK = "__webDriverCallback";
     15 const COMPLETE = "__webDriverComplete";
     16 const DEFAULT_TIMEOUT = 10000; // ms
     17 const FINISH = "finish";
     18 
     19 /** @namespace */
     20 export const evaluate = {};
     21 
     22 /**
     23 * Evaluate a script in given sandbox.
     24 *
     25 * The the provided `script` will be wrapped in an anonymous function
     26 * with the `args` argument applied.
     27 *
     28 * The arguments provided by the `args<` argument are exposed
     29 * through the `arguments` object available in the script context,
     30 * and if the script is executed asynchronously with the `async`
     31 * option, an additional last argument that is synonymous to the
     32 * name `resolve` is appended, and can be accessed
     33 * through `arguments[arguments.length - 1]`.
     34 *
     35 * The `timeout` option specifies the duration for how long the
     36 * script should be allowed to run before it is interrupted and aborted.
     37 * An interrupted script will cause a {@link ScriptTimeoutError} to occur.
     38 *
     39 * The `async` option indicates that the script will not return
     40 * until the `resolve` callback is invoked,
     41 * which is analogous to the last argument of the `arguments` object.
     42 *
     43 * The `file` option is used in error messages to provide information
     44 * on the origin script file in the local end.
     45 *
     46 * The `line` option is used in error messages, along with `filename`,
     47 * to provide the line number in the origin script file on the local end.
     48 *
     49 * @param {nsISandbox} sb
     50 *     Sandbox the script will be evaluated in.
     51 * @param {string} script
     52 *     Script to evaluate.
     53 * @param {Array.<?>=} args
     54 *     A sequence of arguments to call the script with.
     55 * @param {object=} options
     56 * @param {boolean=} options.async
     57 *     Indicates if the script should return immediately or wait for
     58 *     the callback to be invoked before returning. Defaults to false.
     59 * @param {string=} options.file
     60 *     File location of the program in the client. Defaults to "dummy file".
     61 * @param {number=} options.line
     62 *     Line number of the program in the client. Defaults to 0.
     63 * @param {number=} options.timeout
     64 *     Duration in milliseconds before interrupting the script. Defaults to
     65 *     DEFAULT_TIMEOUT.
     66 *
     67 * @returns {Promise}
     68 *     A promise that when resolved will give you the return value from
     69 *     the script.  Note that the return value requires serialisation before
     70 *     it can be sent to the client.
     71 *
     72 * @throws {JavaScriptError}
     73 *   If an {@link Error} was thrown whilst evaluating the script.
     74 * @throws {ScriptTimeoutError}
     75 *   If the script was interrupted due to script timeout.
     76 */
     77 evaluate.sandbox = function (
     78  sb,
     79  script,
     80  args = [],
     81  {
     82    async = false,
     83    file = "dummy file",
     84    line = 0,
     85    timeout = DEFAULT_TIMEOUT,
     86  } = {}
     87 ) {
     88  let unloadHandler;
     89  let marionetteSandbox = sandbox.create(sb.window);
     90 
     91  // timeout handler
     92  let scriptTimeoutID, timeoutPromise;
     93  if (timeout !== null) {
     94    timeoutPromise = new Promise((resolve, reject) => {
     95      scriptTimeoutID = setTimeout(() => {
     96        reject(
     97          new lazy.error.ScriptTimeoutError(`Timed out after ${timeout} ms`)
     98        );
     99      }, timeout);
    100    });
    101  }
    102 
    103  let promise = new Promise((resolve, reject) => {
    104    let src = "";
    105    sb[COMPLETE] = resolve;
    106    sb[ARGUMENTS] = sandbox.cloneInto(args, sb);
    107 
    108    // callback function made private
    109    // so that introspection is possible
    110    // on the arguments object
    111    if (async) {
    112      sb[CALLBACK] = sb[COMPLETE];
    113      src += `${ARGUMENTS}.push(rv => ${CALLBACK}(rv));`;
    114    }
    115 
    116    src += `(function() {
    117      ${script}
    118    }).apply(null, ${ARGUMENTS})`;
    119 
    120    unloadHandler = sandbox.cloneInto(
    121      () => reject(new lazy.error.JavaScriptError("Document was unloaded")),
    122      marionetteSandbox
    123    );
    124    marionetteSandbox.window.addEventListener("unload", unloadHandler);
    125 
    126    let promises = [
    127      Cu.evalInSandbox(
    128        src,
    129        sb,
    130        "1.8",
    131        file,
    132        line,
    133        /* enforceFilenameRestrictions */ false
    134      ),
    135      timeoutPromise,
    136    ];
    137 
    138    // Wait for the immediate result of calling evalInSandbox, or a timeout.
    139    // Only resolve the promise if the scriptPromise was resolved and is not
    140    // async, because the latter has to call resolve() itself.
    141    Promise.race(promises).then(
    142      value => {
    143        if (!async) {
    144          resolve(value);
    145        }
    146      },
    147      err => {
    148        reject(err);
    149      }
    150    );
    151  });
    152 
    153  // This block is mainly for async scripts, which escape the inner promise
    154  // when calling resolve() on their own. The timeout promise will be reused
    155  // to break out after the initially setup timeout.
    156  return Promise.race([promise, timeoutPromise])
    157    .catch(err => {
    158      // Only raise valid errors for both the sync and async scripts.
    159      if (err instanceof lazy.error.ScriptTimeoutError) {
    160        throw err;
    161      }
    162      throw new lazy.error.JavaScriptError(err);
    163    })
    164    .finally(() => {
    165      clearTimeout(scriptTimeoutID);
    166      marionetteSandbox.window.removeEventListener("unload", unloadHandler);
    167    });
    168 };
    169 
    170 /**
    171 * `Cu.isDeadWrapper` does not return true for a dead sandbox that
    172 * was associated with and extension popup.  This provides a way to
    173 * still test for a dead object.
    174 *
    175 * @param {object} obj
    176 *     A potentially dead object.
    177 * @param {string} prop
    178 *     Name of a property on the object.
    179 *
    180 * @returns {boolean}
    181 *     True if <var>obj</var> is dead, false otherwise.
    182 */
    183 evaluate.isDead = function (obj, prop) {
    184  try {
    185    obj[prop];
    186  } catch (e) {
    187    if (e.message.includes("dead object")) {
    188      return true;
    189    }
    190    throw e;
    191  }
    192  return false;
    193 };
    194 
    195 export const sandbox = {};
    196 
    197 /**
    198 * Provides a safe way to take an object defined in a privileged scope and
    199 * create a structured clone of it in a less-privileged scope.  It returns
    200 * a reference to the clone.
    201 *
    202 * Unlike for {@link Components.utils.cloneInto}, `obj` may contain
    203 * functions and DOM elements.
    204 */
    205 sandbox.cloneInto = function (obj, sb) {
    206  return Cu.cloneInto(obj, sb, { cloneFunctions: true, wrapReflectors: true });
    207 };
    208 
    209 /**
    210 * Augment given sandbox by an adapter that has an `exports` map
    211 * property, or a normal map, of function names and function references.
    212 *
    213 * @param {Sandbox} sb
    214 *     The sandbox to augment.
    215 * @param {object} adapter
    216 *     Object that holds an `exports` property, or a map, of function
    217 *     names and function references.
    218 *
    219 * @returns {Sandbox}
    220 *     The augmented sandbox.
    221 */
    222 sandbox.augment = function (sb, adapter) {
    223  function* entries(obj) {
    224    for (let key of Object.keys(obj)) {
    225      yield [key, obj[key]];
    226    }
    227  }
    228 
    229  let funcs = adapter.exports || entries(adapter);
    230  for (let [name, func] of funcs) {
    231    sb[name] = func;
    232  }
    233 
    234  return sb;
    235 };
    236 
    237 /**
    238 * Creates a sandbox.
    239 *
    240 * @param {Window} win
    241 *     The DOM Window object.
    242 * @param {nsIPrincipal=} principal
    243 *     An optional, custom principal to prefer over the Window.  Useful if
    244 *     you need elevated security permissions.
    245 *
    246 * @returns {Sandbox}
    247 *     The created sandbox.
    248 */
    249 sandbox.create = function (win, principal = null, opts = {}) {
    250  let p = principal || win;
    251  opts = Object.assign(
    252    {
    253      sameZoneAs: win,
    254      sandboxPrototype: win,
    255      wantComponents: true,
    256      wantXrays: true,
    257      wantGlobalProperties: ["ChromeUtils"],
    258    },
    259    opts
    260  );
    261  return new Cu.Sandbox(p, opts);
    262 };
    263 
    264 /**
    265 * Creates a mutable sandbox, where changes to the global scope
    266 * will have lasting side-effects.
    267 *
    268 * @param {Window} win
    269 *     The DOM Window object.
    270 *
    271 * @returns {Sandbox}
    272 *     The created sandbox.
    273 */
    274 sandbox.createMutable = function (win) {
    275  let opts = {
    276    wantComponents: false,
    277    wantXrays: false,
    278  };
    279  // Note: We waive Xrays here to match potentially-accidental old behavior.
    280  return Cu.waiveXrays(sandbox.create(win, null, opts));
    281 };
    282 
    283 sandbox.createSystemPrincipal = function (win) {
    284  let principal = Cc["@mozilla.org/systemprincipal;1"].createInstance(
    285    Ci.nsIPrincipal
    286  );
    287  return sandbox.create(win, principal);
    288 };
    289 
    290 sandbox.createSimpleTest = function (win, harness) {
    291  let sb = sandbox.create(win);
    292  sb = sandbox.augment(sb, harness);
    293  sb[FINISH] = () => sb[COMPLETE](harness.generate_results());
    294  return sb;
    295 };
    296 
    297 /**
    298 * Sandbox storage.  When the user requests a sandbox by a specific name,
    299 * if one exists in the storage this will be used as long as its window
    300 * reference is still valid.
    301 *
    302 * @memberof evaluate
    303 */
    304 export class Sandboxes {
    305  /**
    306   * @param {function(): Window} windowFn
    307   *     A function that returns the references to the current Window
    308   *     object.
    309   */
    310  constructor(windowFn) {
    311    this.windowFn_ = windowFn;
    312    this.boxes_ = new Map();
    313  }
    314 
    315  get window_() {
    316    return this.windowFn_();
    317  }
    318 
    319  /**
    320   * Factory function for getting a sandbox by name, or failing that,
    321   * creating a new one.
    322   *
    323   * If the sandbox' window does not match the provided window, a new one
    324   * will be created.
    325   *
    326   * @param {string} name
    327   *     The name of the sandbox to get or create.
    328   * @param {boolean=} [fresh=false] fresh
    329   *     Remove old sandbox by name first, if it exists.
    330   *
    331   * @returns {Sandbox}
    332   *     A used or fresh sandbox.
    333   */
    334  get(name = "default", fresh = false) {
    335    let sb = this.boxes_.get(name);
    336    if (sb) {
    337      if (fresh || evaluate.isDead(sb, "window") || sb.window != this.window_) {
    338        this.boxes_.delete(name);
    339        return this.get(name, false);
    340      }
    341    } else {
    342      if (name == "system") {
    343        sb = sandbox.createSystemPrincipal(this.window_);
    344      } else {
    345        sb = sandbox.create(this.window_);
    346      }
    347      this.boxes_.set(name, sb);
    348    }
    349    return sb;
    350  }
    351 
    352  /** Clears cache of sandboxes. */
    353  clear() {
    354    this.boxes_.clear();
    355  }
    356 }