tor-browser

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

XPCShellContentUtils.sys.mjs (14978B)


      1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
      2 /* vim: set sts=2 sw=2 et tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
      8 
      9 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     10 
     11 // Windowless browsers can create documents that rely on XUL Custom Elements:
     12 ChromeUtils.importESModule(
     13  "resource://gre/modules/CustomElementsListener.sys.mjs"
     14 );
     15 
     16 // Need to import ActorManagerParent.sys.mjs so that the actors are initialized
     17 // before running extension XPCShell tests.
     18 ChromeUtils.importESModule("resource://gre/modules/ActorManagerParent.sys.mjs");
     19 
     20 const lazy = {};
     21 
     22 ChromeUtils.defineESModuleGetters(lazy, {
     23  ContentTask: "resource://testing-common/ContentTask.sys.mjs",
     24  HttpServer: "resource://testing-common/httpd.sys.mjs",
     25  SpecialPowersParent: "resource://testing-common/SpecialPowersParent.sys.mjs",
     26  SpecialPowersForProcess:
     27    "resource://testing-common/SpecialPowersProcessActor.sys.mjs",
     28  TestUtils: "resource://testing-common/TestUtils.sys.mjs",
     29 });
     30 
     31 XPCOMUtils.defineLazyServiceGetters(lazy, {
     32  proxyService: [
     33    "@mozilla.org/network/protocol-proxy-service;1",
     34    Ci.nsIProtocolProxyService,
     35  ],
     36 });
     37 
     38 const { promiseDocumentLoaded, promiseEvent, promiseObserved } = ExtensionUtils;
     39 
     40 var gRemoteContentScripts = Services.appinfo.browserTabsRemoteAutostart;
     41 const REMOTE_CONTENT_SUBFRAMES = Services.appinfo.fissionAutostart;
     42 
     43 function frameScript() {
     44  // We need to make sure that the ExtensionPolicy service has been initialized
     45  // as it sets up the observers that inject extension content scripts.
     46  Cc["@mozilla.org/addons/policy-service;1"].getService();
     47 
     48  Services.obs.notifyObservers(this, "tab-content-frameloader-created");
     49 
     50  // eslint-disable-next-line mozilla/balanced-listeners, no-undef
     51  addEventListener(
     52    "MozHeapMinimize",
     53    () => {
     54      Services.obs.notifyObservers(null, "memory-pressure", "heap-minimize");
     55    },
     56    true,
     57    true
     58  );
     59 }
     60 
     61 let kungFuDeathGrip = new Set();
     62 function promiseBrowserLoaded(browser, url, redirectUrl) {
     63  url = url && Services.io.newURI(url);
     64  redirectUrl = redirectUrl && Services.io.newURI(redirectUrl);
     65 
     66  return new Promise(resolve => {
     67    const listener = {
     68      QueryInterface: ChromeUtils.generateQI([
     69        "nsISupportsWeakReference",
     70        "nsIWebProgressListener",
     71      ]),
     72 
     73      onStateChange(webProgress, request, stateFlags) {
     74        request.QueryInterface(Ci.nsIChannel);
     75 
     76        let requestURI =
     77          request.originalURI ||
     78          webProgress.DOMWindow.document.documentURIObject;
     79        if (
     80          webProgress.isTopLevel &&
     81          (url?.equals(requestURI) || redirectUrl?.equals(requestURI)) &&
     82          stateFlags & Ci.nsIWebProgressListener.STATE_STOP
     83        ) {
     84          resolve();
     85          kungFuDeathGrip.delete(listener);
     86          browser.removeProgressListener(listener);
     87        }
     88      },
     89    };
     90 
     91    // addProgressListener only supports weak references, so we need to
     92    // use one. But we also need to make sure it stays alive until we're
     93    // done with it, so thunk away a strong reference to keep it alive.
     94    kungFuDeathGrip.add(listener);
     95    browser.addProgressListener(
     96      listener,
     97      Ci.nsIWebProgress.NOTIFY_STATE_WINDOW
     98    );
     99  });
    100 }
    101 
    102 export class ContentPage {
    103  constructor(
    104    remote = gRemoteContentScripts,
    105    remoteSubframes = REMOTE_CONTENT_SUBFRAMES,
    106    extension = null,
    107    privateBrowsing = false,
    108    userContextId = undefined
    109  ) {
    110    this.remote = remote;
    111 
    112    // If an extension has been passed, overwrite remote
    113    // with extension.remote to be sure that the ContentPage
    114    // will have the same remoteness of the extension.
    115    if (extension) {
    116      this.remote = extension.remote;
    117    }
    118 
    119    this.remoteSubframes = this.remote && remoteSubframes;
    120    this.extension = extension;
    121    this.privateBrowsing = privateBrowsing;
    122    this.userContextId = userContextId;
    123 
    124    this.browserReady = this._initBrowser();
    125  }
    126 
    127  async _initBrowser() {
    128    let chromeFlags = 0;
    129    if (this.remote) {
    130      chromeFlags |= Ci.nsIWebBrowserChrome.CHROME_REMOTE_WINDOW;
    131    }
    132    if (this.remoteSubframes) {
    133      chromeFlags |= Ci.nsIWebBrowserChrome.CHROME_FISSION_WINDOW;
    134    }
    135    if (this.privateBrowsing) {
    136      chromeFlags |= Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW;
    137    }
    138    this.windowlessBrowser = Services.appShell.createWindowlessBrowser(
    139      true,
    140      chromeFlags
    141    );
    142 
    143    let system = Services.scriptSecurityManager.getSystemPrincipal();
    144 
    145    let chromeShell = this.windowlessBrowser.docShell.QueryInterface(
    146      Ci.nsIWebNavigation
    147    );
    148 
    149    this.windowlessBrowser.browsingContext.useGlobalHistory = false;
    150    let loadURIOptions = {
    151      triggeringPrincipal: system,
    152    };
    153    chromeShell.loadURI(
    154      Services.io.newURI("chrome://extensions/content/dummy.xhtml"),
    155      loadURIOptions
    156    );
    157 
    158    await promiseObserved(
    159      "chrome-document-global-created",
    160      win => win.document == chromeShell.document
    161    );
    162 
    163    let chromeDoc = await promiseDocumentLoaded(chromeShell.document);
    164 
    165    let { SpecialPowers } = chromeDoc.ownerGlobal;
    166    SpecialPowers.xpcshellScope = XPCShellContentUtils.currentScope;
    167    SpecialPowers.setAsDefaultAssertHandler();
    168 
    169    let browser = chromeDoc.createXULElement("browser");
    170    browser.setAttribute("type", "content");
    171    browser.setAttribute("disableglobalhistory", "true");
    172    browser.setAttribute("messagemanagergroup", "webext-browsers");
    173    browser.setAttribute("nodefaultsrc", "true");
    174    if (this.userContextId) {
    175      browser.setAttribute("usercontextid", this.userContextId);
    176    }
    177 
    178    if (this.extension?.remote) {
    179      browser.setAttribute("remote", "true");
    180      browser.setAttribute("remoteType", "extension");
    181    }
    182 
    183    // Ensure that the extension is loaded into the correct
    184    // BrowsingContextGroupID by default.
    185    if (this.extension) {
    186      browser.setAttribute(
    187        "initialBrowsingContextGroupId",
    188        this.extension.browsingContextGroupId
    189      );
    190    }
    191 
    192    let awaitFrameLoader = Promise.resolve();
    193    if (this.remote) {
    194      awaitFrameLoader = promiseEvent(browser, "XULFrameLoaderCreated");
    195      browser.setAttribute("remote", "true");
    196 
    197      browser.setAttribute("maychangeremoteness", "true");
    198      browser.addEventListener(
    199        "DidChangeBrowserRemoteness",
    200        this.didChangeBrowserRemoteness.bind(this)
    201      );
    202    }
    203 
    204    chromeDoc.documentElement.appendChild(browser);
    205 
    206    // Forcibly flush layout so that we get a pres shell soon enough, see
    207    // bug 1274775.
    208    browser.getBoundingClientRect();
    209 
    210    await awaitFrameLoader;
    211 
    212    this.browser = browser;
    213 
    214    this.loadFrameScript(frameScript);
    215 
    216    return browser;
    217  }
    218 
    219  get browsingContext() {
    220    return this.browser.browsingContext;
    221  }
    222 
    223  get SpecialPowers() {
    224    return this.browser.ownerGlobal.SpecialPowers;
    225  }
    226 
    227  loadFrameScript(func) {
    228    let frameScript = `data:text/javascript,(${encodeURI(func)}).call(this)`;
    229    this.browser.messageManager.loadFrameScript(frameScript, true, true);
    230  }
    231 
    232  addFrameScriptHelper(func) {
    233    let frameScript = `data:text/javascript,${encodeURI(func)}`;
    234    this.browser.messageManager.loadFrameScript(frameScript, false, true);
    235  }
    236 
    237  didChangeBrowserRemoteness() {
    238    // XXX: Tests can load their own additional frame scripts, so we may need to
    239    // track all scripts that have been loaded, and reload them here?
    240    this.loadFrameScript(frameScript);
    241  }
    242 
    243  async loadURL(url, redirectUrl = undefined) {
    244    await this.browserReady;
    245 
    246    let browserLoadedPromise = promiseBrowserLoaded(
    247      this.browser,
    248      url,
    249      redirectUrl
    250    );
    251    this.browser.fixupAndLoadURIString(url, {
    252      triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
    253    });
    254    return browserLoadedPromise;
    255  }
    256 
    257  async fetch(...args) {
    258    return this.spawn(args, async (url, options) => {
    259      let resp = await this.content.fetch(url, options);
    260      return resp.text();
    261    });
    262  }
    263 
    264  spawn(params, task) {
    265    return this.SpecialPowers.spawn(this.browser, params, task);
    266  }
    267 
    268  // Get a SpecialPowersForProcess instance associated with the content process
    269  // of the currently loaded page. This allows callers to spawn() tasks that
    270  // outlive the page (for as long as the page's process is around).
    271  getCurrentContentProcessSpecialPowers() {
    272    const testScope = XPCShellContentUtils.currentScope;
    273    const domProcess = this.browsingContext.currentWindowGlobal.domProcess;
    274    return new lazy.SpecialPowersForProcess(testScope, domProcess);
    275  }
    276 
    277  // Like spawn(), but uses the legacy ContentTask infrastructure rather than
    278  // SpecialPowers. Exists only because the author of the SpecialPowers
    279  // migration did not have the time to fix all of the legacy users who relied
    280  // on the old semantics.
    281  //
    282  // DO NOT USE IN NEW CODE
    283  legacySpawn(params, task) {
    284    lazy.ContentTask.setTestScope(XPCShellContentUtils.currentScope);
    285 
    286    return lazy.ContentTask.spawn(this.browser, params, task);
    287  }
    288 
    289  async close() {
    290    await this.browserReady;
    291 
    292    let { messageManager } = this.browser;
    293 
    294    this.browser.removeEventListener(
    295      "DidChangeBrowserRemoteness",
    296      this.didChangeBrowserRemoteness.bind(this)
    297    );
    298    this.browser = null;
    299 
    300    this.windowlessBrowser.close();
    301    this.windowlessBrowser = null;
    302 
    303    await lazy.TestUtils.topicObserved(
    304      "message-manager-disconnect",
    305      subject => subject === messageManager
    306    );
    307  }
    308 }
    309 
    310 export var XPCShellContentUtils = {
    311  currentScope: null,
    312  fetchScopes: new Map(),
    313 
    314  initCommon(scope) {
    315    this.currentScope = scope;
    316 
    317    // We need to load at least one frame script into every message
    318    // manager to ensure that the scriptable wrapper for its global gets
    319    // created before we try to access it externally. If we don't, we
    320    // fail sanity checks on debug builds the first time we try to
    321    // create a wrapper, because we should never have a global without a
    322    // cached wrapper.
    323    Services.mm.loadFrameScript("data:text/javascript,//", true, true);
    324 
    325    scope.registerCleanupFunction(() => {
    326      this.currentScope = null;
    327 
    328      return Promise.all(
    329        Array.from(this.fetchScopes.values(), promise =>
    330          promise.then(scope => scope.close())
    331        )
    332      );
    333    });
    334  },
    335 
    336  init(scope) {
    337    // QuotaManager crashes if it doesn't have a profile.
    338    scope.do_get_profile();
    339 
    340    this.initCommon(scope);
    341 
    342    lazy.SpecialPowersParent.registerActor();
    343  },
    344 
    345  initMochitest(scope) {
    346    this.initCommon(scope);
    347  },
    348 
    349  ensureInitialized(scope) {
    350    if (!this.currentScope) {
    351      if (scope.do_get_profile) {
    352        this.init(scope);
    353      } else {
    354        this.initMochitest(scope);
    355      }
    356    }
    357  },
    358 
    359  /**
    360   * Creates a new HttpServer for testing, and begins listening on the
    361   * specified port. Automatically shuts down the server when the test
    362   * unit ends.
    363   *
    364   * @param {object} [options = {}]
    365   *        The options object.
    366   * @param {integer} [options.port = -1]
    367   *        The port to listen on. If omitted, listen on a random
    368   *        port. The latter is the preferred behavior.
    369   * @param {sequence<string>?} [options.hosts = null]
    370   *        A set of hosts to accept connections to. Support for this is
    371   *        implemented using a proxy filter.
    372   *
    373   * @returns {HttpServer}
    374   *        The HTTP server instance.
    375   */
    376  createHttpServer({ port = -1, hosts } = {}) {
    377    let server = new lazy.HttpServer();
    378    server.start(port);
    379 
    380    if (hosts) {
    381      const hostsSet = new Set();
    382      const serverHost = "localhost";
    383      const serverPort = server.identity.primaryPort;
    384 
    385      for (let host of hosts) {
    386        if (host.startsWith("[") && host.endsWith("]")) {
    387          // HttpServer expects IPv6 addresses in bracket notation, but the
    388          // proxy filter uses nsIURI.host, which does not have brackets.
    389          hostsSet.add(host.slice(1, -1));
    390        } else {
    391          hostsSet.add(host);
    392        }
    393        server.identity.add("http", host, 80);
    394      }
    395 
    396      const proxyFilter = {
    397        proxyInfo: lazy.proxyService.newProxyInfo(
    398          "http",
    399          serverHost,
    400          serverPort,
    401          "",
    402          "",
    403          0,
    404          4096,
    405          null
    406        ),
    407 
    408        applyFilter(channel, defaultProxyInfo, callback) {
    409          if (hostsSet.has(channel.URI.host)) {
    410            callback.onProxyFilterResult(this.proxyInfo);
    411          } else {
    412            callback.onProxyFilterResult(defaultProxyInfo);
    413          }
    414        },
    415      };
    416 
    417      lazy.proxyService.registerChannelFilter(proxyFilter, 0);
    418      this.currentScope.registerCleanupFunction(() => {
    419        lazy.proxyService.unregisterChannelFilter(proxyFilter);
    420      });
    421    }
    422 
    423    this.currentScope.registerCleanupFunction(() => {
    424      return new Promise(resolve => {
    425        server.stop(resolve);
    426      });
    427    });
    428 
    429    return server;
    430  },
    431 
    432  registerJSON(server, path, obj) {
    433    server.registerPathHandler(path, (request, response) => {
    434      response.setHeader("content-type", "application/json", true);
    435      response.write(JSON.stringify(obj));
    436    });
    437  },
    438 
    439  async fetch(origin, url, options) {
    440    let fetchScopePromise = this.fetchScopes.get(origin);
    441    if (!fetchScopePromise) {
    442      fetchScopePromise = this.loadContentPage(origin);
    443      this.fetchScopes.set(origin, fetchScopePromise);
    444    }
    445 
    446    let fetchScope = await fetchScopePromise;
    447    return fetchScope.fetch(url, options);
    448  },
    449 
    450  /**
    451   * Loads a content page into a hidden docShell.
    452   *
    453   * @param {string} url
    454   *        The URL to load.
    455   * @param {object} [options = {}]
    456   * @param {ExtensionWrapper} [options.extension]
    457   *        If passed, load the URL as an extension page for the given
    458   *        extension.
    459   * @param {boolean} [options.remote]
    460   *        If true, load the URL in a content process. If false, load
    461   *        it in the parent process.
    462   * @param {boolean} [options.remoteSubframes]
    463   *        If true, load cross-origin frames in separate content processes.
    464   *        This is ignored if |options.remote| is false.
    465   * @param {string} [options.redirectUrl]
    466   *        An optional URL that the initial page is expected to
    467   *        redirect to.
    468   *
    469   * @returns {ContentPage}
    470   */
    471  loadContentPage(
    472    url,
    473    {
    474      extension = undefined,
    475      remote = undefined,
    476      remoteSubframes = undefined,
    477      redirectUrl = undefined,
    478      privateBrowsing = false,
    479      userContextId = undefined,
    480    } = {}
    481  ) {
    482    let contentPage = new ContentPage(
    483      remote,
    484      remoteSubframes,
    485      extension && extension.extension,
    486      privateBrowsing,
    487      userContextId
    488    );
    489 
    490    return contentPage.loadURL(url, redirectUrl).then(() => {
    491      return contentPage;
    492    });
    493  },
    494 };