tor-browser

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

base-loader.sys.mjs (19982B)


      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 /* exported Loader, resolveURI, Module, Require, unload */
      6 
      7 const systemPrincipal = Components.Constructor(
      8  "@mozilla.org/systemprincipal;1",
      9  "nsIPrincipal"
     10 )();
     11 
     12 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     13 
     14 const lazy = {};
     15 
     16 XPCOMUtils.defineLazyServiceGetter(
     17  lazy,
     18  "resProto",
     19  "@mozilla.org/network/protocol;1?name=resource",
     20  Ci.nsIResProtocolHandler
     21 );
     22 
     23 ChromeUtils.defineESModuleGetters(
     24  lazy,
     25  {
     26    NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
     27  },
     28  { global: "contextual" }
     29 );
     30 
     31 const VENDOR_URI = "resource://devtools/client/shared/vendor/";
     32 const REACT_ESM_MODULES = new Set([
     33  VENDOR_URI + "react-dev.js",
     34  VENDOR_URI + "react.js",
     35  VENDOR_URI + "react-dom-dev.js",
     36  VENDOR_URI + "react-dom.js",
     37  VENDOR_URI + "react-dom-factories.js",
     38  VENDOR_URI + "react-dom-server-dev.js",
     39  VENDOR_URI + "react-dom-server.js",
     40  VENDOR_URI + "react-prop-types-dev.js",
     41  VENDOR_URI + "react-prop-types.js",
     42  VENDOR_URI + "react-test-renderer.js",
     43 ]);
     44 
     45 // Define some shortcuts.
     46 function* getOwnIdentifiers(x) {
     47  yield* Object.getOwnPropertyNames(x);
     48  yield* Object.getOwnPropertySymbols(x);
     49 }
     50 
     51 function isJSONURI(uri) {
     52  return uri.endsWith(".json");
     53 }
     54 function isESMURI(uri) {
     55  return uri.endsWith(".mjs");
     56 }
     57 function isJSURI(uri) {
     58  return uri.endsWith(".js");
     59 }
     60 const AbsoluteRegExp = /^(resource|chrome|file|jar):/;
     61 function isAbsoluteURI(uri) {
     62  return AbsoluteRegExp.test(uri);
     63 }
     64 function isRelative(id) {
     65  return id.startsWith(".");
     66 }
     67 
     68 function readURI(uri) {
     69  const nsURI = lazy.NetUtil.newURI(uri);
     70  if (nsURI.scheme == "resource") {
     71    // Resolve to a real URI, this will catch any obvious bad paths without
     72    // logging assertions in debug builds, see bug 1135219
     73    uri = lazy.resProto.resolveURI(nsURI);
     74  }
     75 
     76  const stream = lazy.NetUtil.newChannel({
     77    uri: lazy.NetUtil.newURI(uri, "UTF-8"),
     78    loadUsingSystemPrincipal: true,
     79  }).open();
     80  const count = stream.available();
     81  const data = lazy.NetUtil.readInputStreamToString(stream, count, {
     82    charset: "UTF-8",
     83  });
     84 
     85  stream.close();
     86 
     87  return data;
     88 }
     89 
     90 // Combines all arguments into a resolved, normalized path
     91 function join(base, ...paths) {
     92  // If this is an absolute URL, we need to normalize only the path portion,
     93  // or we wind up stripping too many slashes and producing invalid URLs.
     94  const match = /^((?:resource|file|chrome)\:\/\/[^\/]*|jar:[^!]+!)(.*)/.exec(
     95    base
     96  );
     97  if (match) {
     98    return match[1] + normalize([match[2], ...paths].join("/"));
     99  }
    100 
    101  return normalize([base, ...paths].join("/"));
    102 }
    103 
    104 // Function takes set of options and returns a JS sandbox. Function may be
    105 // passed set of options:
    106 //  - `name`: A string value which identifies the sandbox in about:memory. Will
    107 //    throw exception if omitted.
    108 // - `prototype`: Ancestor for the sandbox that will be created. Defaults to
    109 //    `{}`.
    110 function Sandbox(options) {
    111  // Normalize options and rename to match `Cu.Sandbox` expectations.
    112  const sandboxOptions = {
    113    // This will allow exposing Components as well as Cu, Ci and Cr.
    114    wantComponents: true,
    115 
    116    // By default, Sandbox come with a very limited set of global.
    117    // The list of all available symbol names is available over there:
    118    // https://searchfox.org/mozilla-central/rev/31368c7795f44b7a15531d6c5e52dc97f82cf2d5/js/xpconnect/src/Sandbox.cpp#905-997
    119    // Request to expose all meaningful global here:
    120    wantGlobalProperties: [
    121      "AbortController",
    122      "atob",
    123      "btoa",
    124      "Blob",
    125      "crypto",
    126      "ChromeUtils",
    127      "CSS",
    128      "CSSPositionTryDescriptors",
    129      "CSSRule",
    130      "CustomStateSet",
    131      "DOMParser",
    132      "Element",
    133      "Event",
    134      "FileReader",
    135      "FormData",
    136      "Headers",
    137      "InspectorCSSParser",
    138      "InspectorUtils",
    139      "MIDIInputMap",
    140      "MIDIOutputMap",
    141      "Node",
    142      "TextDecoder",
    143      "TextEncoder",
    144      "TrustedHTML",
    145      "TrustedScript",
    146      "TrustedScriptURL",
    147      "URL",
    148      "URLSearchParams",
    149      "Window",
    150      "XMLHttpRequest",
    151    ],
    152 
    153    sandboxName: options.name,
    154    sandboxPrototype: "prototype" in options ? options.prototype : {},
    155    freshCompartment: options.freshCompartment || false,
    156  };
    157 
    158  return Cu.Sandbox(systemPrincipal, sandboxOptions);
    159 }
    160 
    161 // This allows defining some modules in AMD format while retaining CommonJS
    162 // compatibility with this loader by allowing the factory function to have
    163 // access to general CommonJS functions, e.g.
    164 //
    165 //   define(function(require, exports, module) {
    166 //     ... code ...
    167 //   });
    168 function define(factory) {
    169  factory(this.require, this.exports, this.module);
    170 }
    171 
    172 // Populates `exports` of the given CommonJS `module` object, in the context
    173 // of the given `loader` by evaluating code associated with it.
    174 function load(loader, module) {
    175  const require = Require(loader, module);
    176 
    177  // We expose set of properties defined by `CommonJS` specification via
    178  // prototype of the sandbox. Also globals are deeper in the prototype
    179  // chain so that each module has access to them as well.
    180  const properties = {
    181    require,
    182    module,
    183    exports: module.exports,
    184  };
    185  if (loader.supportAMDModules) {
    186    properties.define = define;
    187  }
    188 
    189  // Create a new object in the shared global of the loader, that will be used
    190  // as the scope object for this particular module.
    191  const scopeFromSharedGlobal = new loader.sharedGlobal.Object();
    192  Object.assign(scopeFromSharedGlobal, properties);
    193 
    194  const originalExports = module.exports;
    195  try {
    196    Services.scriptloader.loadSubScript(module.uri, scopeFromSharedGlobal);
    197  } catch (error) {
    198    // loadSubScript sometime throws string errors, which includes no stack.
    199    // At least provide the current stack by re-throwing a real Error object.
    200    if (typeof error == "string") {
    201      if (
    202        error.startsWith("Error creating URI") ||
    203        error.startsWith("Error opening input stream (invalid filename?)")
    204      ) {
    205        throw new Error(
    206          `Module \`${module.id}\` is not found at ${module.uri}`
    207        );
    208      }
    209      throw new Error(
    210        `Error while loading module \`${module.id}\` at ${module.uri}:` +
    211          "\n" +
    212          error
    213      );
    214    }
    215    // Otherwise just re-throw everything else which should have a stack
    216    throw error;
    217  }
    218 
    219  // Only freeze the exports object if we created it ourselves. Modules
    220  // which completely replace the exports object and still want it
    221  // frozen need to freeze it themselves.
    222  if (module.exports === originalExports) {
    223    Object.freeze(module.exports);
    224  }
    225 
    226  return module;
    227 }
    228 
    229 // Utility function to normalize module `uri`s so they have `.js` extension.
    230 function normalizeExt(uri) {
    231  if (isJSURI(uri) || isJSONURI(uri) || isESMURI(uri)) {
    232    return uri;
    233  }
    234  return uri + ".js";
    235 }
    236 
    237 // Utility function to join paths. In common case `base` is a
    238 // `requirer.uri` but in some cases it may be `baseURI`. In order to
    239 // avoid complexity we require `baseURI` with a trailing `/`.
    240 function resolve(id, base) {
    241  if (!isRelative(id)) {
    242    return id;
    243  }
    244 
    245  const baseDir = dirname(base);
    246 
    247  let resolved;
    248  if (baseDir.includes(":")) {
    249    resolved = join(baseDir, id);
    250  } else {
    251    resolved = normalize(`${baseDir}/${id}`);
    252  }
    253 
    254  // Joining and normalizing removes the "./" from relative files.
    255  // We need to ensure the resolution still has the root
    256  if (base.startsWith("./")) {
    257    resolved = "./" + resolved;
    258  }
    259 
    260  return resolved;
    261 }
    262 
    263 function compileMapping(paths) {
    264  // Make mapping array that is sorted from longest path to shortest path.
    265  const mapping = Object.keys(paths)
    266    .sort((a, b) => b.length - a.length)
    267    .map(path => [path, paths[path]]);
    268 
    269  const PATTERN = /([.\\?+*(){}[\]^$])/g;
    270  const escapeMeta = str => str.replace(PATTERN, "\\$1");
    271 
    272  const patterns = [];
    273  paths = {};
    274 
    275  for (let [path, uri] of mapping) {
    276    // Strip off any trailing slashes to make comparisons simpler
    277    if (path.endsWith("/")) {
    278      path = path.slice(0, -1);
    279      uri = uri.replace(/\/+$/, "");
    280    }
    281 
    282    paths[path] = uri;
    283 
    284    // We only want to match path segments explicitly. Examples:
    285    // * "foo/bar" matches for "foo/bar"
    286    // * "foo/bar" matches for "foo/bar/baz"
    287    // * "foo/bar" does not match for "foo/bar-1"
    288    // * "foo/bar/" does not match for "foo/bar"
    289    // * "foo/bar/" matches for "foo/bar/baz"
    290    //
    291    // Check for an empty path, an exact match, or a substring match
    292    // with the next character being a forward slash.
    293    if (path == "") {
    294      patterns.push("");
    295    } else {
    296      patterns.push(`${escapeMeta(path)}(?=$|/)`);
    297    }
    298  }
    299 
    300  const pattern = new RegExp(`^(${patterns.join("|")})`);
    301 
    302  // This will replace the longest matching path mapping at the start of
    303  // the ID string with its mapped value.
    304  return id => {
    305    return id.replace(pattern, (m0, m1) => paths[m1]);
    306  };
    307 }
    308 
    309 export function resolveURI(id, mapping) {
    310  // Do not resolve if already a resource URI
    311  if (isAbsoluteURI(id)) {
    312    return normalizeExt(id);
    313  }
    314 
    315  return normalizeExt(mapping(id));
    316 }
    317 
    318 // Creates version of `require` that will be exposed to the given `module`
    319 // in the context of the given `loader`. Each module gets own limited copy
    320 // of `require` that is allowed to load only a modules that are associated
    321 // with it during link time.
    322 export function Require(loader, requirer) {
    323  const { modules, mapping, mappingCache, requireHook } = loader;
    324 
    325  function require(id) {
    326    if (!id) {
    327      // Throw if `id` is not passed.
    328      throw Error(
    329        "You must provide a module name when calling require() from " +
    330          requirer.id,
    331        requirer.uri
    332      );
    333    }
    334 
    335    if (requireHook) {
    336      return requireHook(id, _require);
    337    }
    338 
    339    return _require(id);
    340  }
    341 
    342  function _require(id) {
    343    let { uri, requirement } = getRequirements(id);
    344 
    345    // Load all react modules as ES Modules, in the Browser Loader global.
    346    // For this we have to ensure using ChromeUtils.importESModule with `global:"current"`,
    347    // but executed from the Loader global scope. `syncImport` does that.
    348    if (REACT_ESM_MODULES.has(uri)) {
    349      // All CommonJS modules are still importing the .js/CommonJS version,
    350      // but we hack these require() call to load the ESM version.
    351      uri = uri.replace(/.js$/, ".mjs");
    352    }
    353 
    354    let module = null;
    355    // If module is already cached by loader then just use it.
    356    if (uri in modules) {
    357      module = modules[uri];
    358    } else if (isESMURI(uri)) {
    359      module = modules[uri] = Module(requirement, uri);
    360      const rv = ChromeUtils.importESModule(uri, {
    361        global: "contextual",
    362      });
    363      module.exports = rv.default || rv;
    364    } else if (isJSONURI(uri)) {
    365      let data;
    366 
    367      // First attempt to load and parse json uri
    368      // ex: `test.json`
    369      // If that doesn"t exist, check for `test.json.js`
    370      // for node parity
    371      try {
    372        data = JSON.parse(readURI(uri));
    373        module = modules[uri] = Module(requirement, uri);
    374        module.exports = data;
    375      } catch (err) {
    376        // If error thrown from JSON parsing, throw that, do not
    377        // attempt to find .json.js file
    378        if (err && /JSON\.parse/.test(err.message)) {
    379          throw err;
    380        }
    381        uri = uri + ".js";
    382      }
    383    }
    384 
    385    // If not yet cached, load and cache it.
    386    // We also freeze module to prevent it from further changes
    387    // at runtime.
    388    if (!(uri in modules)) {
    389      // Many of the loader's functionalities are dependent
    390      // on modules[uri] being set before loading, so we set it and
    391      // remove it if we have any errors.
    392      module = modules[uri] = Module(requirement, uri);
    393      try {
    394        Object.freeze(load(loader, module));
    395      } catch (e) {
    396        // Clear out modules cache so we can throw on a second invalid require
    397        delete modules[uri];
    398        throw e;
    399      }
    400    }
    401 
    402    return module.exports;
    403  }
    404 
    405  // Resolution function taking a module name/path and
    406  // returning a resourceURI and a `requirement` used by the loader.
    407  // Used by both `require` and `require.resolve`.
    408  function getRequirements(id) {
    409    if (!id) {
    410      // Throw if `id` is not passed.
    411      throw Error(
    412        "you must provide a module name when calling require() from " +
    413          requirer.id,
    414        requirer.uri
    415      );
    416    }
    417 
    418    let requirement, uri;
    419 
    420    if (modules[id]) {
    421      uri = requirement = id;
    422    } else if (requirer) {
    423      // Resolve `id` to its requirer if it's relative.
    424      requirement = resolve(id, requirer.id);
    425    } else {
    426      requirement = id;
    427    }
    428 
    429    // Resolves `uri` of module using loaders resolve function.
    430    if (!uri) {
    431      if (mappingCache.has(requirement)) {
    432        uri = mappingCache.get(requirement);
    433      } else {
    434        uri = resolveURI(requirement, mapping);
    435        mappingCache.set(requirement, uri);
    436      }
    437    }
    438 
    439    // Throw if `uri` can not be resolved.
    440    if (!uri) {
    441      throw Error(
    442        "Module: Can not resolve '" +
    443          id +
    444          "' module required by " +
    445          requirer.id +
    446          " located at " +
    447          requirer.uri,
    448        requirer.uri
    449      );
    450    }
    451 
    452    return { uri, requirement };
    453  }
    454 
    455  // Expose the `resolve` function for this `Require` instance
    456  require.resolve = _require.resolve = function (id) {
    457    const { uri } = getRequirements(id);
    458    return uri;
    459  };
    460 
    461  // This is like webpack's require.context.  It returns a new require
    462  // function that prepends the prefix to any requests.
    463  require.context = prefix => {
    464    return id => {
    465      return require(prefix + id);
    466    };
    467  };
    468 
    469  return require;
    470 }
    471 
    472 // Makes module object that is made available to CommonJS modules when they
    473 // are evaluated, along with `exports` and `require`.
    474 export function Module(id, uri) {
    475  return Object.create(null, {
    476    id: { enumerable: true, value: id },
    477    exports: {
    478      enumerable: true,
    479      writable: true,
    480      value: Object.create(null),
    481      configurable: true,
    482    },
    483    uri: { value: uri },
    484  });
    485 }
    486 
    487 // Takes `loader`, and unload `reason` string and notifies all observers that
    488 // they should cleanup after them-self.
    489 export function unload(loader, reason) {
    490  // subject is a unique object created per loader instance.
    491  // This allows any code to cleanup on loader unload regardless of how
    492  // it was loaded. To handle unload for specific loader subject may be
    493  // asserted against loader.destructor or require("@loader/unload")
    494  // Note: We don not destroy loader's module cache or sandboxes map as
    495  // some modules may do cleanup in subsequent turns of event loop. Destroying
    496  // cache may cause module identity problems in such cases.
    497  const subject = { wrappedJSObject: loader.destructor };
    498  Services.obs.notifyObservers(subject, "devtools:loader:destroy", reason);
    499 }
    500 
    501 // Function makes new loader that can be used to load CommonJS modules.
    502 // Loader takes following options:
    503 // - `paths`: Mandatory dictionary of require path mapped to absolute URIs.
    504 //   Object keys are path prefix used in require(), values are URIs where each
    505 //   prefix should be mapped to.
    506 // - `globals`: Optional map of globals, that all module scopes will inherit
    507 //   from. Map is also exposed under `globals` property of the returned loader
    508 //   so it can be extended further later. Defaults to `{}`.
    509 // - `sandboxName`: String, name of the sandbox displayed in about:memory.
    510 // - `sandboxPrototype`: Object used to define globals on all module's
    511 //   sandboxes.
    512 // - `requireHook`: Optional function used to replace native require function
    513 //   from loader. This function receive the module path as first argument,
    514 //   and native require method as second argument.
    515 export function Loader(options) {
    516  let { paths, globals } = options;
    517  if (!globals) {
    518    globals = {};
    519  }
    520 
    521  // We create an identity object that will be dispatched on an unload
    522  // event as subject. This way unload listeners will be able to assert
    523  // which loader is unloaded. Please note that we intentionally don"t
    524  // use `loader` as subject to prevent a loader access leakage through
    525  // observer notifications.
    526  const destructor = Object.create(null);
    527 
    528  const mapping = compileMapping(paths);
    529 
    530  // Define pseudo modules.
    531  const builtinModuleExports = {
    532    "@loader/unload": destructor,
    533    "@loader/options": options,
    534  };
    535 
    536  const modules = {};
    537  for (const id of Object.keys(builtinModuleExports)) {
    538    // We resolve `uri` from `id` since modules are cached by `uri`.
    539    const uri = resolveURI(id, mapping);
    540    const module = Module(id, uri);
    541 
    542    // Lazily expose built-in modules in order to
    543    // allow them to be loaded lazily.
    544    Object.defineProperty(module, "exports", {
    545      enumerable: true,
    546      get() {
    547        return builtinModuleExports[id];
    548      },
    549    });
    550 
    551    modules[uri] = module;
    552  }
    553 
    554  let sharedGlobal;
    555  if (options.sharedGlobal) {
    556    sharedGlobal = options.sharedGlobal;
    557  } else {
    558    // Create the unique sandbox we will be using for all modules,
    559    // so that we prevent creating a new compartment per module.
    560    // The side effect is that all modules will share the same
    561    // global objects.
    562    sharedGlobal = Sandbox({
    563      name: options.sandboxName || "DevTools",
    564      prototype: options.sandboxPrototype || globals,
    565      freshCompartment: options.freshCompartment,
    566    });
    567  }
    568 
    569  if (options.sharedGlobal || options.sandboxPrototype) {
    570    // If we were given a sharedGlobal or a sandboxPrototype, we have to define
    571    // the globals on the shared global directly. Note that this will not work
    572    // for callers who depend on being able to add globals after the loader was
    573    // created.
    574    for (const name of getOwnIdentifiers(globals)) {
    575      Object.defineProperty(
    576        sharedGlobal,
    577        name,
    578        Object.getOwnPropertyDescriptor(globals, name)
    579      );
    580    }
    581  }
    582 
    583  // Loader object is just a representation of a environment
    584  // state. We mark its properties non-enumerable
    585  // as they are pure implementation detail that no one should rely upon.
    586  const returnObj = {
    587    destructor: { enumerable: false, value: destructor },
    588    globals: { enumerable: false, value: globals },
    589    mapping: { enumerable: false, value: mapping },
    590    mappingCache: { enumerable: false, value: new Map() },
    591    // Map of module objects indexed by module URIs.
    592    modules: { enumerable: false, value: modules },
    593    sharedGlobal: { enumerable: false, value: sharedGlobal },
    594    supportAMDModules: {
    595      enumerable: false,
    596      value: options.supportAMDModules || false,
    597    },
    598    requireHook: {
    599      enumerable: false,
    600      writable: true,
    601      value: options.requireHook,
    602    },
    603  };
    604 
    605  return Object.create(null, returnObj);
    606 }
    607 
    608 // NB: These methods are from the UNIX implementation of OS.Path. Refactoring
    609 //     this module to not use path methods on stringly-typed URIs is
    610 //     non-trivial.
    611 function dirname(path) {
    612  let index = path.lastIndexOf("/");
    613  if (index == -1) {
    614    return ".";
    615  }
    616  while (index >= 0 && path[index] == "/") {
    617    --index;
    618  }
    619  return path.slice(0, index + 1);
    620 }
    621 
    622 function normalize(path) {
    623  const stack = [];
    624  let absolute;
    625  if (path.length >= 0 && path[0] == "/") {
    626    absolute = true;
    627  } else {
    628    absolute = false;
    629  }
    630  path.split("/").forEach(function (v) {
    631    switch (v) {
    632      case "":
    633      case ".": // fallthrough
    634        break;
    635      case "..":
    636        if (!stack.length) {
    637          if (absolute) {
    638            throw new Error("Path is ill-formed: attempting to go past root");
    639          } else {
    640            stack.push("..");
    641          }
    642        } else if (stack[stack.length - 1] == "..") {
    643          stack.push("..");
    644        } else {
    645          stack.pop();
    646        }
    647        break;
    648      default:
    649        stack.push(v);
    650    }
    651  });
    652  const string = stack.join("/");
    653  return absolute ? "/" + string : string;
    654 }