tor-browser

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

worker-loader.sys.mjs (16120B)


      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 
      5 // A CommonJS module loader that is designed to run inside a worker debugger.
      6 // We can't simply use the SDK module loader, because it relies heavily on
      7 // Components, which isn't available in workers.
      8 //
      9 // In principle, the standard instance of the worker loader should provide the
     10 // same built-in modules as its devtools counterpart, so that both loaders are
     11 // interchangable on the main thread, making them easier to test.
     12 //
     13 // On the worker thread, some of these modules, in particular those that rely on
     14 // the use of Components, and for which the worker debugger doesn't provide an
     15 // alternative API, will be replaced by vacuous objects. Consequently, they can
     16 // still be required, but any attempts to use them will lead to an exception.
     17 //
     18 // Note: to see dump output when running inside the worker thread, you might
     19 // need to enable the browser.dom.window.dump.enabled pref.
     20 
     21 // Some notes on module ids and URLs:
     22 //
     23 // An id is either a relative id or an absolute id. An id is relative if and
     24 // only if it starts with a dot. An absolute id is a normalized id if and only
     25 // if it contains no redundant components.
     26 //
     27 // Every normalized id is a URL. A URL is either an absolute URL or a relative
     28 // URL. A URL is absolute if and only if it starts with a scheme name followed
     29 // by a colon and 2 or 3 slashes.
     30 
     31 /**
     32 * Convert the given relative id to an absolute id.
     33 *
     34 * @param String id
     35 *        The relative id to be resolved.
     36 * @param String baseId
     37 *        The absolute base id to resolve the relative id against.
     38 *
     39 * @return String
     40 *         An absolute id
     41 */
     42 function resolveId(id, baseId) {
     43  return baseId + "/../" + id;
     44 }
     45 
     46 /**
     47 * Convert the given absolute id to a normalized id.
     48 *
     49 * @param String id
     50 *        The absolute id to be normalized.
     51 *
     52 * @return String
     53 *         A normalized id.
     54 */
     55 function normalizeId(id) {
     56  // An id consists of an optional root and a path. A root consists of either
     57  // a scheme name followed by 2 or 3 slashes, or a single slash. Slashes in the
     58  // root are not used as separators, so only normalize the path.
     59  const [, root, path] = id.match(/^(\w+:\/\/\/?|\/)?(.*)/);
     60 
     61  const stack = [];
     62  path.split("/").forEach(function (component) {
     63    switch (component) {
     64      case "":
     65      case ".":
     66        break;
     67      case "..":
     68        if (stack.length === 0) {
     69          if (root !== undefined) {
     70            throw new Error("Can't normalize absolute id '" + id + "'!");
     71          } else {
     72            stack.push("..");
     73          }
     74        } else if (stack[stack.length - 1] == "..") {
     75          stack.push("..");
     76        } else {
     77          stack.pop();
     78        }
     79        break;
     80      default:
     81        stack.push(component);
     82        break;
     83    }
     84  });
     85 
     86  return (root ? root : "") + stack.join("/");
     87 }
     88 
     89 /**
     90 * Create a module object with the given normalized id.
     91 *
     92 * @param String
     93 *        The normalized id of the module to be created.
     94 *
     95 * @return Object
     96 *         A module with the given id.
     97 */
     98 function createModule(id) {
     99  return Object.create(null, {
    100    // CommonJS specifies the id property to be non-configurable and
    101    // non-writable.
    102    id: {
    103      configurable: false,
    104      enumerable: true,
    105      value: id,
    106      writable: false,
    107    },
    108 
    109    // CommonJS does not specify an exports property, so follow the NodeJS
    110    // convention, which is to make it non-configurable and writable.
    111    exports: {
    112      configurable: false,
    113      enumerable: true,
    114      value: Object.create(null),
    115      writable: true,
    116    },
    117  });
    118 }
    119 
    120 /**
    121 * Create a CommonJS loader with the following options:
    122 * - createSandbox:
    123 *     A function that will be used to create sandboxes. It should take the name
    124 *     and prototype of the sandbox to be created, and return the newly created
    125 *     sandbox as result. This option is required.
    126 * - globals:
    127 *     A map of names to built-in globals that will be exposed to every module.
    128 *     Defaults to the empty map.
    129 * - loadSubScript:
    130 *     A function that will be used to load scripts in sandboxes. It should take
    131 *     the URL from and the sandbox in which the script is to be loaded, and not
    132 *     return a result. This option is required.
    133 * - modules:
    134 *     A map from normalized ids to built-in modules that will be added to the
    135 *     module cache. Defaults to the empty map.
    136 * - paths:
    137 *     A map of paths to base URLs that will be used to resolve relative URLs to
    138 *     absolute URLS. Defaults to the empty map.
    139 * - resolve:
    140 *     A function that will be used to resolve relative ids to absolute ids. It
    141 *     should take the relative id of a module to be required and the absolute
    142 *     id of the requiring module as arguments, and return the absolute id of
    143 *     the module to be required as result. Defaults to resolveId above.
    144 */
    145 function WorkerDebuggerLoader(options) {
    146  /**
    147   * Convert the given relative URL to an absolute URL, using the map of paths
    148   * given below.
    149   *
    150   * @param String url
    151   *        The relative URL to be resolved.
    152   *
    153   * @return String
    154   *         An absolute URL.
    155   */
    156  function resolveURL(url) {
    157    let found = false;
    158    for (const [path, baseURL] of paths) {
    159      if (url.startsWith(path)) {
    160        found = true;
    161        url = url.replace(path, baseURL);
    162        break;
    163      }
    164    }
    165    if (!found) {
    166      throw new Error("Can't resolve relative URL '" + url + "'!");
    167    }
    168 
    169    // If the url has no extension, use ".js" by default.
    170    return url.endsWith(".js") ? url : url + ".js";
    171  }
    172 
    173  /**
    174   * Load the given module with the given url.
    175   *
    176   * @param Object module
    177   *        The module object to be loaded.
    178   * @param String url
    179   *        The URL to load the module from.
    180   */
    181  function loadModule(module, url) {
    182    // CommonJS specifies 3 free variables: require, exports, and module. These
    183    // must be exposed to every module, so define these as properties on the
    184    // sandbox prototype. Additional built-in globals are exposed by making
    185    // the map of built-in globals the prototype of the sandbox prototype.
    186    const prototype = Object.create(globals);
    187    prototype.Components = {};
    188    prototype.require = createRequire(module);
    189    prototype.exports = module.exports;
    190    prototype.module = module;
    191 
    192    const sandbox = createSandbox(url, prototype);
    193    try {
    194      loadSubScript(url, sandbox);
    195    } catch (error) {
    196      if (/^Error opening input stream/.test(String(error))) {
    197        throw new Error(
    198          "Can't load module '" + module.id + "' with url '" + url + "'!"
    199        );
    200      }
    201      throw error;
    202    }
    203 
    204    // The value of exports may have been changed by the module script, so
    205    // freeze it if and only if it is still an object.
    206    if (typeof module.exports === "object" && module.exports !== null) {
    207      Object.freeze(module.exports);
    208    }
    209  }
    210 
    211  /**
    212   * Create a require function for the given module. If no module is given,
    213   * create a require function for the top-level module instead.
    214   *
    215   * @param Object requirer
    216   *        The module for which the require function is to be created.
    217   *
    218   * @return Function
    219   *         A require function for the given module.
    220   */
    221  function createRequire(requirer) {
    222    return function require(id) {
    223      // Make sure an id was passed.
    224      if (id === undefined) {
    225        throw new Error("Can't require module without id!");
    226      }
    227 
    228      // Built-in modules are cached by id rather than URL, so try to find the
    229      // module to be required by id first.
    230      let module = modules[id];
    231      if (module === undefined) {
    232        // Failed to find the module to be required by id, so convert the id to
    233        // a URL and try again.
    234 
    235        // If the id is relative, convert it to an absolute id.
    236        if (id.startsWith(".")) {
    237          if (requirer === undefined) {
    238            throw new Error(
    239              "Can't require top-level module with relative id " +
    240                "'" +
    241                id +
    242                "'!"
    243            );
    244          }
    245          id = resolve(id, requirer.id);
    246        }
    247 
    248        // Convert the absolute id to a normalized id.
    249        id = normalizeId(id);
    250 
    251        // Convert the normalized id to a URL.
    252        let url = id;
    253 
    254        // If the URL is relative, resolve it to an absolute URL.
    255        if (url.match(/^\w+:\/\//) === null) {
    256          url = resolveURL(id);
    257        }
    258 
    259        // Try to find the module to be required by URL.
    260        module = modules[url];
    261        if (module === undefined) {
    262          // Failed to find the module to be required in the cache, so create
    263          // a new module, load it from the given URL, and add it to the cache.
    264 
    265          // Add modules to the cache early so that any recursive calls to
    266          // require for the same module will return the partially-loaded module
    267          // from the cache instead of triggering a new load.
    268          module = modules[url] = createModule(id);
    269 
    270          try {
    271            loadModule(module, url);
    272          } catch (error) {
    273            // If the module failed to load, remove it from the cache so that
    274            // subsequent calls to require for the same module will trigger a
    275            // new load, instead of returning a partially-loaded module from
    276            // the cache.
    277            delete modules[url];
    278            throw error;
    279          }
    280 
    281          Object.freeze(module);
    282        }
    283      }
    284 
    285      return module.exports;
    286    };
    287  }
    288 
    289  const createSandbox = options.createSandbox;
    290  const globals = options.globals || Object.create(null);
    291  const loadSubScript = options.loadSubScript;
    292 
    293  // Create the module cache, by converting each entry in the map from
    294  // normalized ids to built-in modules to a module object, with the exports
    295  // property of each module set to a frozen version of the original entry.
    296  const modules = options.modules || {};
    297  for (const id in modules) {
    298    const module = createModule(id);
    299    module.exports = Object.freeze(modules[id]);
    300    modules[id] = module;
    301  }
    302 
    303  // Convert the map of paths to base URLs into an array for use by resolveURL.
    304  // The array is sorted from longest to shortest path to ensure that the
    305  // longest path is always the first to be found.
    306  let paths = options.paths || Object.create(null);
    307  paths = Object.keys(paths)
    308    .sort((a, b) => b.length - a.length)
    309    .map(path => [path, paths[path]]);
    310 
    311  const resolve = options.resolve || resolveId;
    312 
    313  this.require = createRequire();
    314 }
    315 
    316 var loader = {
    317  // There is only one loader in the worker thread.
    318  // This will be used by DevToolsServer to build server prefix and actor IDs.
    319  id: 0,
    320 
    321  lazyGetter(object, name, lambda) {
    322    Object.defineProperty(object, name, {
    323      get() {
    324        delete object[name];
    325        object[name] = lambda.apply(object);
    326        return object[name];
    327      },
    328      configurable: true,
    329      enumerable: true,
    330    });
    331  },
    332  lazyServiceGetter() {
    333    throw new Error("Can't import XPCOM service from worker thread!");
    334  },
    335  lazyRequireGetter(obj, properties, module, destructure) {
    336    if (Array.isArray(properties) && !destructure) {
    337      throw new Error(
    338        "Pass destructure=true to call lazyRequireGetter with an array of properties"
    339      );
    340    }
    341 
    342    if (!Array.isArray(properties)) {
    343      properties = [properties];
    344    }
    345 
    346    for (const property of properties) {
    347      Object.defineProperty(obj, property, {
    348        get: () =>
    349          destructure
    350            ? worker.require(module)[property]
    351            : worker.require(module || property),
    352      });
    353    }
    354  },
    355 };
    356 
    357 // The following APIs are defined differently depending on whether we are on the
    358 // main thread or a worker thread. On the main thread, we use the Components
    359 // object to implement them. On worker threads, we use the APIs provided by
    360 // the worker debugger.
    361 
    362 /* eslint-disable no-shadow */
    363 var {
    364  Debugger,
    365  URL,
    366  createSandbox,
    367  dump,
    368  rpc,
    369  loadSubScript,
    370  setImmediate,
    371  xpcInspector,
    372 } = (function () {
    373  // Main thread
    374  if (typeof Components === "object") {
    375    const principal = Components.Constructor(
    376      "@mozilla.org/systemprincipal;1",
    377      "nsIPrincipal"
    378    )();
    379 
    380    // To ensure that the this passed to addDebuggerToGlobal is a global, the
    381    // Debugger object needs to be defined in a sandbox.
    382    const sandbox = Cu.Sandbox(principal, {
    383      wantGlobalProperties: ["ChromeUtils"],
    384    });
    385    Cu.evalInSandbox(
    386      `
    387 const { addDebuggerToGlobal } = ChromeUtils.importESModule(
    388  'resource://gre/modules/jsdebugger.sys.mjs'
    389 );
    390 addDebuggerToGlobal(globalThis);
    391 `,
    392      sandbox
    393    );
    394    const Debugger = sandbox.Debugger;
    395 
    396    const createSandbox = function (name, prototype) {
    397      return Cu.Sandbox(principal, {
    398        invisibleToDebugger: true,
    399        sandboxName: name,
    400        sandboxPrototype: prototype,
    401        wantComponents: false,
    402        wantXrays: false,
    403      });
    404    };
    405 
    406    const rpc = undefined;
    407 
    408    // eslint-disable-next-line mozilla/use-services
    409    const subScriptLoader = Cc[
    410      "@mozilla.org/moz/jssubscript-loader;1"
    411    ].getService(Ci.mozIJSSubScriptLoader);
    412 
    413    const loadSubScript = function (url, sandbox) {
    414      subScriptLoader.loadSubScript(url, sandbox);
    415    };
    416 
    417    const Timer = ChromeUtils.importESModule(
    418      "resource://gre/modules/Timer.sys.mjs"
    419    );
    420 
    421    const setImmediate = function (callback) {
    422      Timer.setTimeout(callback, 0);
    423    };
    424 
    425    const xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService(
    426      Ci.nsIJSInspector
    427    );
    428 
    429    const { URL } = Cu.Sandbox(principal, {
    430      wantGlobalProperties: ["URL"],
    431    });
    432 
    433    return {
    434      Debugger,
    435      URL,
    436      createSandbox,
    437      dump: globalThis.dump,
    438      rpc,
    439      loadSubScript,
    440      setImmediate,
    441      xpcInspector,
    442    };
    443  }
    444  // Worker thread
    445  const requestors = [];
    446 
    447  const scope = globalThis;
    448 
    449  const xpcInspector = {
    450    get eventLoopNestLevel() {
    451      return requestors.length;
    452    },
    453 
    454    get lastNestRequestor() {
    455      return requestors.length === 0 ? null : requestors[requestors.length - 1];
    456    },
    457 
    458    enterNestedEventLoop(requestor) {
    459      requestors.push(requestor);
    460      scope.enterEventLoop();
    461      return requestors.length;
    462    },
    463 
    464    exitNestedEventLoop() {
    465      requestors.pop();
    466      scope.leaveEventLoop();
    467      return requestors.length;
    468    },
    469  };
    470 
    471  return {
    472    Debugger: globalThis.Debugger,
    473    URL: globalThis.URL,
    474    createSandbox: globalThis.createSandbox,
    475    dump: globalThis.dump,
    476    rpc: globalThis.rpc,
    477    loadSubScript: globalThis.loadSubScript,
    478    setImmediate: globalThis.setImmediate,
    479    xpcInspector,
    480  };
    481 })();
    482 /* eslint-enable no-shadow */
    483 
    484 // Create the default instance of the worker loader, using the APIs we defined
    485 // above.
    486 
    487 export const worker = new WorkerDebuggerLoader({
    488  createSandbox,
    489  globals: {
    490    isWorker: true,
    491    dump,
    492    loader,
    493    rpc,
    494    URL,
    495    setImmediate,
    496    retrieveConsoleEvents: globalThis.retrieveConsoleEvents,
    497    setConsoleEventHandler: globalThis.setConsoleEventHandler,
    498    clearConsoleEvents: globalThis.clearConsoleEvents,
    499    console,
    500    btoa: globalThis.btoa,
    501    atob: globalThis.atob,
    502    Services: Object.create(null),
    503    ChromeUtils,
    504    DebuggerNotificationObserver,
    505 
    506    // The following APIs rely on the use of Components, and the worker debugger
    507    // does not provide alternative definitions for them. Consequently, they are
    508    // stubbed out both on the main thread and worker threads.
    509    Cc: undefined,
    510    ChromeWorker: undefined,
    511    Ci: undefined,
    512    Cu: undefined,
    513    Cr: undefined,
    514    Components: undefined,
    515  },
    516  loadSubScript,
    517  modules: {
    518    Debugger,
    519    xpcInspector,
    520  },
    521  paths: {
    522    // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
    523    devtools: "resource://devtools",
    524    // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
    525    "xpcshell-test": "resource://test",
    526    // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
    527  },
    528 });