tor-browser

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

AboutNewTabRedirector.sys.mjs (20239B)


      1 /**
      2 * This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
      5 */
      6 
      7 /**
      8 * This nsIAboutModule is for about:home and about:newtab. The primary
      9 * job of the AboutNewTabRedirector is to resolve requests to load about:home
     10 * and about:newtab to the appropriate resources for those requests.
     11 *
     12 * The AboutNewTabRedirector is not involved when the user has overridden
     13 * the default about:home or about:newtab pages.
     14 *
     15 * There are two implementations of this nsIAboutModule - one for the parent
     16 * process, and one for content processes. Each one has some secondary
     17 * responsibilties that are process-specific.
     18 *
     19 * The need for two implementations is an unfortunate consequence of how
     20 * document loading and process redirection for about: pages currently
     21 * works in Gecko. The commonalities between the two implementations has
     22 * been put into an abstract base class.
     23 */
     24 
     25 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
     26 import { E10SUtils } from "resource://gre/modules/E10SUtils.sys.mjs";
     27 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     28 
     29 const lazy = {};
     30 
     31 ChromeUtils.defineESModuleGetters(lazy, {
     32  BasePromiseWorker: "resource://gre/modules/PromiseWorker.sys.mjs",
     33 });
     34 
     35 XPCOMUtils.defineLazyPreferenceGetter(
     36  lazy,
     37  "BUILTIN_NEWTAB_ENABLED",
     38  "browser.newtabpage.enabled",
     39  true
     40 );
     41 
     42 /**
     43 * BEWARE: Do not add variables for holding state in the global scope.
     44 * Any state variables should be properties of the appropriate class
     45 * below. This is to avoid confusion where the state is set in one process,
     46 * but not in another.
     47 *
     48 * Constants are fine in the global scope.
     49 */
     50 
     51 const PREF_ABOUT_HOME_CACHE_TESTING =
     52  "browser.startup.homepage.abouthome_cache.testing";
     53 
     54 const CACHE_WORKER_URL = "resource://newtab/lib/cache.worker.js";
     55 
     56 const IS_PRIVILEGED_PROCESS =
     57  Services.appinfo.remoteType === E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE;
     58 
     59 const PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS =
     60  "browser.tabs.remote.separatePrivilegedContentProcess";
     61 const PREF_ACTIVITY_STREAM_DEBUG = "browser.newtabpage.activity-stream.debug";
     62 
     63 /**
     64 * The AboutHomeStartupCacheChild is responsible for connecting the
     65 * AboutNewTabRedirectorChild with a cached document and script for about:home
     66 * if one happens to exist. The AboutHomeStartupCacheChild is only ever
     67 * handed the streams for those caches when the "privileged about content
     68 * process" first launches, so subsequent loads of about:home do not read
     69 * from this cache.
     70 *
     71 * See https://firefox-source-docs.mozilla.org/browser/extensions/newtab/docs/v2-system-addon/about_home_startup_cache.html
     72 * for further details.
     73 */
     74 export const AboutHomeStartupCacheChild = {
     75  _initted: false,
     76  CACHE_REQUEST_MESSAGE: "AboutHomeStartupCache:CacheRequest",
     77  CACHE_RESPONSE_MESSAGE: "AboutHomeStartupCache:CacheResponse",
     78  CACHE_USAGE_RESULT_MESSAGE: "AboutHomeStartupCache:UsageResult",
     79  STATES: {
     80    UNAVAILABLE: 0,
     81    UNCONSUMED: 1,
     82    PAGE_CONSUMED: 2,
     83    PAGE_AND_SCRIPT_CONSUMED: 3,
     84    FAILED: 4,
     85    DISQUALIFIED: 5,
     86  },
     87  REQUEST_TYPE: {
     88    PAGE: 0,
     89    SCRIPT: 1,
     90  },
     91  _state: 0,
     92  _consumerBCID: null,
     93 
     94  /**
     95   * Called via a process script very early on in the process lifetime. This
     96   * prepares the AboutHomeStartupCacheChild to pass an nsIChannel back to
     97   * the AboutNewTabRedirectorChild when the initial about:home document is
     98   * eventually requested.
     99   *
    100   * @param {nsIInputStream} pageInputStream
    101   *   The stream for the cached page markup.
    102   * @param {nsIInputStream} scriptInputStream
    103   *   The stream for the cached script to run on the page.
    104   */
    105  init(pageInputStream, scriptInputStream) {
    106    if (
    107      !IS_PRIVILEGED_PROCESS &&
    108      !Services.prefs.getBoolPref(PREF_ABOUT_HOME_CACHE_TESTING, false)
    109    ) {
    110      throw new Error(
    111        "Can only instantiate in the privileged about content processes."
    112      );
    113    }
    114 
    115    if (
    116      !Services.prefs.getBoolPref(
    117        "browser.startup.homepage.abouthome_cache.enabled"
    118      )
    119    ) {
    120      return;
    121    }
    122 
    123    if (this._initted) {
    124      throw new Error("AboutHomeStartupCacheChild already initted.");
    125    }
    126 
    127    Services.obs.addObserver(this, "memory-pressure");
    128    Services.cpmm.addMessageListener(this.CACHE_REQUEST_MESSAGE, this);
    129 
    130    this._pageInputStream = pageInputStream;
    131    this._scriptInputStream = scriptInputStream;
    132    this._initted = true;
    133    this.setState(this.STATES.UNCONSUMED);
    134  },
    135 
    136  /**
    137   * A function that lets us put the AboutHomeStartupCacheChild back into
    138   * its initial state. This is used by tests to let us simulate the startup
    139   * behaviour of the module without having to manually launch a new privileged
    140   * about content process every time.
    141   */
    142  uninit() {
    143    if (!Services.prefs.getBoolPref(PREF_ABOUT_HOME_CACHE_TESTING, false)) {
    144      throw new Error(
    145        "Cannot uninit AboutHomeStartupCacheChild unless testing."
    146      );
    147    }
    148 
    149    if (!this._initted) {
    150      return;
    151    }
    152 
    153    Services.obs.removeObserver(this, "memory-pressure");
    154    Services.cpmm.removeMessageListener(this.CACHE_REQUEST_MESSAGE, this);
    155 
    156    if (this._cacheWorker) {
    157      this._cacheWorker.terminate();
    158      this._cacheWorker = null;
    159    }
    160 
    161    this._pageInputStream = null;
    162    this._scriptInputStream = null;
    163    this._initted = false;
    164    this._state = this.STATES.UNAVAILABLE;
    165    this._consumerBCID = null;
    166  },
    167 
    168  /**
    169   * Attempts to return an nsIChannel for a cached about:home document that
    170   * we were initialized with. If we failed to be initted with the cache, or the
    171   * input streams that we were sent have no data yet available, this function
    172   * returns null. The caller should fall back to generating the page
    173   * dynamically.
    174   *
    175   * This function will be called when loading about:home, or
    176   * about:home?jscache - the latter returns the cached script.
    177   *
    178   * It is expected that the same BrowsingContext that loads the cached
    179   * page will also load the cached script.
    180   *
    181   * @param {nsIURI} uri
    182   *   The URI for the requested page, as passed by AboutNewTabRedirectorChild.
    183   * @param {nsILoadInfo} loadInfo
    184   *   The nsILoadInfo for the requested load, as passed by
    185   *   AboutNewTabRedirectorChild.
    186   * @returns {?nsIChannel}
    187   */
    188  maybeGetCachedPageChannel(uri, loadInfo) {
    189    if (!this._initted) {
    190      return null;
    191    }
    192 
    193    if (this._state >= this.STATES.PAGE_AND_SCRIPT_CONSUMED) {
    194      return null;
    195    }
    196 
    197    let requestType =
    198      uri.query === "jscache"
    199        ? this.REQUEST_TYPE.SCRIPT
    200        : this.REQUEST_TYPE.PAGE;
    201 
    202    // If this is a page request, then we need to be in the UNCONSUMED state,
    203    // since we expect the page request to come first. If this is a script
    204    // request, we expect to be in PAGE_CONSUMED state, since the page cache
    205    // stream should he been consumed already.
    206    if (
    207      (requestType === this.REQUEST_TYPE.PAGE &&
    208        this._state !== this.STATES.UNCONSUMED) ||
    209      (requestType === this.REQUEST_TYPE_SCRIPT &&
    210        this._state !== this.STATES.PAGE_CONSUMED)
    211    ) {
    212      return null;
    213    }
    214 
    215    // If by this point, we don't have anything in the streams,
    216    // then either the cache was too slow to give us data, or the cache
    217    // doesn't exist. The caller should fall back to generating the
    218    // page dynamically.
    219    //
    220    // We only do this on the page request, because by the time
    221    // we get to the script request, we should have already drained
    222    // the page input stream.
    223    if (requestType === this.REQUEST_TYPE.PAGE) {
    224      try {
    225        if (
    226          !this._scriptInputStream.available() ||
    227          !this._pageInputStream.available()
    228        ) {
    229          this.setState(this.STATES.FAILED);
    230          this.reportUsageResult(false /* success */);
    231          return null;
    232        }
    233      } catch (e) {
    234        this.setState(this.STATES.FAILED);
    235        if (e.result === Cr.NS_BASE_STREAM_CLOSED) {
    236          this.reportUsageResult(false /* success */);
    237          return null;
    238        }
    239        throw e;
    240      }
    241    }
    242 
    243    if (
    244      requestType === this.REQUEST_TYPE.SCRIPT &&
    245      this._consumerBCID !== loadInfo.browsingContextID
    246    ) {
    247      // Some other document is somehow requesting the script - one
    248      // that didn't originally request the page. This is not allowed.
    249      this.setState(this.STATES.FAILED);
    250      return null;
    251    }
    252 
    253    let channel = Cc[
    254      "@mozilla.org/network/input-stream-channel;1"
    255    ].createInstance(Ci.nsIInputStreamChannel);
    256    channel.QueryInterface(Ci.nsIChannel);
    257    channel.setURI(uri);
    258    channel.loadInfo = loadInfo;
    259    channel.contentStream =
    260      requestType === this.REQUEST_TYPE.PAGE
    261        ? this._pageInputStream
    262        : this._scriptInputStream;
    263 
    264    if (requestType === this.REQUEST_TYPE.SCRIPT) {
    265      this.setState(this.STATES.PAGE_AND_SCRIPT_CONSUMED);
    266      this.reportUsageResult(true /* success */);
    267    } else {
    268      this.setState(this.STATES.PAGE_CONSUMED);
    269      // Stash the BrowsingContext ID so that when the script stream
    270      // attempts to be consumed, we ensure that it's from the same
    271      // BrowsingContext that loaded the page.
    272      this._consumerBCID = loadInfo.browsingContextID;
    273    }
    274 
    275    return channel;
    276  },
    277 
    278  /**
    279   * This function takes the state information required to generate
    280   * the about:home cache markup and script, and then generates that
    281   * markup in script asynchronously. Once that's done, a message
    282   * is sent to the parent process with the nsIInputStream's for the
    283   * markup and script contents.
    284   *
    285   * @param {object} state
    286   *   The Redux state of the about:home document to render.
    287   * @returns {Promise<undefined>}
    288   *   Fulfills after the message with the nsIInputStream's have been sent to
    289   *   the parent.
    290   */
    291  async constructAndSendCache(state) {
    292    if (!IS_PRIVILEGED_PROCESS) {
    293      throw new Error("Wrong process type.");
    294    }
    295 
    296    let worker = this.getOrCreateWorker();
    297 
    298    let timerId = Glean.newtab.abouthomeCacheConstruction.start();
    299 
    300    let { page, script } = await worker
    301      .post("construct", [state])
    302      .finally(() => {
    303        Glean.newtab.abouthomeCacheConstruction.stopAndAccumulate(timerId);
    304      });
    305 
    306    let pageInputStream = Cc[
    307      "@mozilla.org/io/string-input-stream;1"
    308    ].createInstance(Ci.nsIStringInputStream);
    309 
    310    pageInputStream.setUTF8Data(page);
    311 
    312    let scriptInputStream = Cc[
    313      "@mozilla.org/io/string-input-stream;1"
    314    ].createInstance(Ci.nsIStringInputStream);
    315 
    316    scriptInputStream.setUTF8Data(script);
    317 
    318    Services.cpmm.sendAsyncMessage(this.CACHE_RESPONSE_MESSAGE, {
    319      pageInputStream,
    320      scriptInputStream,
    321    });
    322  },
    323 
    324  _cacheWorker: null,
    325  getOrCreateWorker() {
    326    if (this._cacheWorker) {
    327      return this._cacheWorker;
    328    }
    329 
    330    this._cacheWorker = new lazy.BasePromiseWorker(CACHE_WORKER_URL);
    331    return this._cacheWorker;
    332  },
    333 
    334  receiveMessage(message) {
    335    if (message.name === this.CACHE_REQUEST_MESSAGE) {
    336      let { state } = message.data;
    337      this.constructAndSendCache(state);
    338    }
    339  },
    340 
    341  reportUsageResult(success) {
    342    Services.cpmm.sendAsyncMessage(this.CACHE_USAGE_RESULT_MESSAGE, {
    343      success,
    344    });
    345  },
    346 
    347  observe(subject, topic) {
    348    if (topic === "memory-pressure" && this._cacheWorker) {
    349      this._cacheWorker.terminate();
    350      this._cacheWorker = null;
    351    }
    352  },
    353 
    354  /**
    355   * Transitions the AboutHomeStartupCacheChild from one state
    356   * to the next, where each state is defined in this.STATES.
    357   *
    358   * States can only be transitioned in increasing order, otherwise
    359   * an error is logged.
    360   */
    361  setState(state) {
    362    if (state > this._state) {
    363      this._state = state;
    364    } else {
    365      console.error(
    366        "AboutHomeStartupCacheChild could not transition from state " +
    367          `${this._state} to ${state}`,
    368        new Error().stack
    369      );
    370    }
    371  },
    372 
    373  /**
    374   * If the cache hasn't been used, transitions it into the DISQUALIFIED
    375   * state so that it cannot be used. This should be called if it's been
    376   * determined that about:newtab is going to be loaded, which doesn't
    377   * use the cache.
    378   */
    379  disqualifyCache() {
    380    if (this._state === this.STATES.UNCONSUMED) {
    381      this.setState(this.STATES.DISQUALIFIED);
    382      this.reportUsageResult(false /* success */);
    383    }
    384  },
    385 };
    386 
    387 /**
    388 * This is an abstract base class for the nsIAboutModule implementations for
    389 * about:home and about:newtab that has some common methods and properties.
    390 */
    391 class BaseAboutNewTabRedirector {
    392  constructor() {
    393    if (!AppConstants.RELEASE_OR_BETA) {
    394      XPCOMUtils.defineLazyPreferenceGetter(
    395        this,
    396        "activityStreamDebug",
    397        PREF_ACTIVITY_STREAM_DEBUG,
    398        false
    399      );
    400    } else {
    401      this.activityStreamDebug = false;
    402    }
    403 
    404    XPCOMUtils.defineLazyPreferenceGetter(
    405      this,
    406      "privilegedAboutProcessEnabled",
    407      PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS,
    408      false
    409    );
    410  }
    411 
    412  /**
    413   * @returns {string} the default URL
    414   *
    415   * This URL depends on various activity stream prefs. Overriding
    416   * the newtab page has no effect on the result of this function.
    417   */
    418  get defaultURL() {
    419    // Generate the desired activity stream resource depending on state, e.g.,
    420    // "resource://newtab/prerendered/activity-stream.html"
    421    // "resource://newtab/prerendered/activity-stream-debug.html"
    422    // "resource://newtab/prerendered/activity-stream-noscripts.html"
    423    return [
    424      "resource://newtab/prerendered/",
    425      "activity-stream",
    426      // Debug version loads dev scripts but noscripts separately loads scripts
    427      this.activityStreamDebug && !this.privilegedAboutProcessEnabled
    428        ? "-debug"
    429        : "",
    430      this.privilegedAboutProcessEnabled ? "-noscripts" : "",
    431      ".html",
    432    ].join("");
    433  }
    434 
    435  newChannel() {
    436    throw Components.Exception(
    437      "getChannel not implemented for this process.",
    438      Cr.NS_ERROR_NOT_IMPLEMENTED
    439    );
    440  }
    441 
    442  getURIFlags() {
    443    return (
    444      Ci.nsIAboutModule.ALLOW_SCRIPT |
    445      Ci.nsIAboutModule.ENABLE_INDEXED_DB |
    446      Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD |
    447      Ci.nsIAboutModule.URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS |
    448      Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT
    449    );
    450  }
    451 
    452  getChromeURI() {
    453    return Services.io.newURI("chrome://browser/content/blanktab.html");
    454  }
    455 
    456  QueryInterface = ChromeUtils.generateQI(["nsIAboutModule"]);
    457 }
    458 
    459 /**
    460 * The parent-process implementation of the nsIAboutModule, which is the first
    461 * stop for when requests are made to visit about:home or about:newtab (so
    462 * before the AboutNewTabRedirectorChild has a chance to handle the request).
    463 */
    464 export class AboutNewTabRedirectorParent extends BaseAboutNewTabRedirector {
    465  #addonInitialized = false;
    466  #suspendedChannels = [];
    467  #addonInitializedPromise = null;
    468  #addonInitializedResolver = null;
    469 
    470  constructor() {
    471    super();
    472 
    473    let { promise, resolve } = Promise.withResolvers();
    474    this.#addonInitializedPromise = promise;
    475    this.#addonInitializedResolver = resolve;
    476 
    477    ChromeUtils.registerWindowActor("AboutNewTab", {
    478      parent: {
    479        esModuleURI: "resource:///actors/AboutNewTabParent.sys.mjs",
    480      },
    481      child: {
    482        esModuleURI: "resource:///actors/AboutNewTabChild.sys.mjs",
    483        events: {
    484          DOMDocElementInserted: {},
    485          DOMContentLoaded: { capture: true },
    486          load: { capture: true },
    487          unload: { capture: true },
    488          pageshow: {},
    489          visibilitychange: {},
    490        },
    491      },
    492      // The wildcard on about:newtab is for the # parameter
    493      // that is used for the newtab devtools. The wildcard for about:home
    494      // is similar, and also allows for falling back to loading the
    495      // about:home document dynamically if an attempt is made to load
    496      // about:home?jscache from the AboutHomeStartupCache as a top-level
    497      // load.
    498      matches: ["about:home*", "about:newtab*"],
    499      remoteTypes: ["privilegedabout"],
    500    });
    501    this.wrappedJSObject = this;
    502  }
    503 
    504  /**
    505   * Waits for the AddonManager to be fully initialized, and for the built-in
    506   * addon to be ready. Once that's done, it tterates any suspended channels and
    507   * resumes them, now that the built-in addon has been set up.
    508   */
    509  notifyBuiltInAddonInitialized() {
    510    this.#addonInitialized = true;
    511 
    512    for (let suspendedChannel of this.#suspendedChannels) {
    513      suspendedChannel.resume();
    514    }
    515    this.#suspendedChannels = [];
    516    this.#addonInitializedResolver();
    517  }
    518 
    519  /**
    520   * Returns a Promise that reoslves when the newtab built-in addon has notified
    521   * that it has finished initializing. If this is somehow checked when
    522   * BROWSER_NEWTAB_AS_ADDON is not true, then this always resolves.
    523   *
    524   * @type {Promise<undefined>}
    525   */
    526  get promiseBuiltInAddonInitialized() {
    527    if (!AppConstants.BROWSER_NEWTAB_AS_ADDON) {
    528      return Promise.resolve();
    529    }
    530 
    531    return this.#addonInitializedPromise;
    532  }
    533 
    534  newChannel(uri, loadInfo) {
    535    let chromeURI = this.getChromeURI(uri);
    536 
    537    if (
    538      uri.spec.startsWith("about:home") ||
    539      (uri.spec.startsWith("about:newtab") && lazy.BUILTIN_NEWTAB_ENABLED)
    540    ) {
    541      chromeURI = Services.io.newURI(this.defaultURL);
    542    }
    543 
    544    let resultChannel = Services.io.newChannelFromURIWithLoadInfo(
    545      chromeURI,
    546      loadInfo
    547    );
    548    resultChannel.originalURI = uri;
    549 
    550    if (AppConstants.BROWSER_NEWTAB_AS_ADDON && !this.#addonInitialized) {
    551      return this.#getSuspendedChannel(resultChannel);
    552    }
    553 
    554    return resultChannel;
    555  }
    556 
    557  /**
    558   * Wraps an nsIChannel with an nsISuspendableChannelWrapper, suspends that
    559   * wrapper, and then stores the wrapper in #suspendedChannels so that it can
    560   * be resumed with a call to #notifyBuildInAddonInitialized.
    561   *
    562   * @param {nsIChannel} innerChannel
    563   *   The channel to wrap and suspend.
    564   * @returns {nsISuspendableChannelWrapper}
    565   */
    566  #getSuspendedChannel(innerChannel) {
    567    let suspendedChannel =
    568      Services.io.newSuspendableChannelWrapper(innerChannel);
    569    suspendedChannel.suspend();
    570 
    571    this.#suspendedChannels.push(suspendedChannel);
    572    return suspendedChannel;
    573  }
    574 }
    575 
    576 /**
    577 * The child-process implementation of nsIAboutModule, which also does the work
    578 * of redirecting about:home loads to the about:home startup cache if its
    579 * available.
    580 */
    581 export class AboutNewTabRedirectorChild extends BaseAboutNewTabRedirector {
    582  newChannel(uri, loadInfo) {
    583    if (!IS_PRIVILEGED_PROCESS) {
    584      throw Components.Exception(
    585        "newChannel can only be called from the privilegedabout content process.",
    586        Cr.NS_ERROR_UNEXPECTED
    587      );
    588    }
    589 
    590    let pageURI;
    591 
    592    if (uri.spec.startsWith("about:home")) {
    593      let cacheChannel = AboutHomeStartupCacheChild.maybeGetCachedPageChannel(
    594        uri,
    595        loadInfo
    596      );
    597      if (cacheChannel) {
    598        return cacheChannel;
    599      }
    600      pageURI = Services.io.newURI(this.defaultURL);
    601    } else {
    602      // The only other possibility is about:newtab.
    603      //
    604      // If about:newtab is being requested, then any subsequent request for
    605      // about:home should _never_ request the cache (which might be woefully
    606      // out of date compared to about:newtab), so we disqualify the cache if
    607      // it still happens to be around.
    608      AboutHomeStartupCacheChild.disqualifyCache();
    609 
    610      if (lazy.BUILTIN_NEWTAB_ENABLED) {
    611        pageURI = Services.io.newURI(this.defaultURL);
    612      } else {
    613        pageURI = this.getChromeURI(uri);
    614      }
    615    }
    616 
    617    let resultChannel = Services.io.newChannelFromURIWithLoadInfo(
    618      pageURI,
    619      loadInfo
    620    );
    621    resultChannel.originalURI = uri;
    622    return resultChannel;
    623  }
    624 }
    625 
    626 /**
    627 * The AboutNewTabRedirectorStub is a function called in both the main and
    628 * content processes when trying to get at the nsIAboutModule for about:newtab
    629 * and about:home. This function does the job of choosing the appropriate
    630 * implementation of nsIAboutModule for the process type.
    631 */
    632 export function AboutNewTabRedirectorStub() {
    633  if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT) {
    634    return new AboutNewTabRedirectorParent();
    635  }
    636  return new AboutNewTabRedirectorChild();
    637 }