tor-browser

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

network-events.js (15130B)


      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 "use strict";
      6 
      7 const { Pool } = require("resource://devtools/shared/protocol/Pool.js");
      8 const { ParentProcessWatcherRegistry } = ChromeUtils.importESModule(
      9  "resource://devtools/server/actors/watcher/ParentProcessWatcherRegistry.sys.mjs",
     10  // ParentProcessWatcherRegistry needs to be a true singleton and loads ActorManagerParent
     11  // which also has to be a true singleton.
     12  { global: "shared" }
     13 );
     14 const Targets = require("resource://devtools/server/actors/targets/index.js");
     15 
     16 const lazy = {};
     17 
     18 const { XPCOMUtils } = ChromeUtils.importESModule(
     19  "resource://gre/modules/XPCOMUtils.sys.mjs",
     20  { global: "contextual" }
     21 );
     22 XPCOMUtils.defineLazyPreferenceGetter(
     23  lazy,
     24  "responseBodyLimit",
     25  "devtools.netmonitor.responseBodyLimit",
     26  0
     27 );
     28 
     29 ChromeUtils.defineESModuleGetters(
     30  lazy,
     31  {
     32    NetworkObserver:
     33      "resource://devtools/shared/network-observer/NetworkObserver.sys.mjs",
     34    NetworkUtils:
     35      "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs",
     36  },
     37  { global: "contextual" }
     38 );
     39 
     40 loader.lazyRequireGetter(
     41  this,
     42  "NetworkEventActor",
     43  "resource://devtools/server/actors/network-monitor/network-event-actor.js",
     44  true
     45 );
     46 
     47 /**
     48 * Handles network events from the parent process
     49 */
     50 class NetworkEventWatcher {
     51  /**
     52   * Start watching for all network events related to a given Watcher Actor.
     53   *
     54   * @param WatcherActor watcherActor
     55   *        The watcher actor in the parent process from which we should
     56   *        observe network events.
     57   * @param Object options
     58   *        Dictionary object with following attributes:
     59   *        - onAvailable: mandatory function
     60   *          This will be called for each resource.
     61   *        - onUpdated: optional function
     62   *          This would be called multiple times for each resource.
     63   */
     64  async watch(watcherActor, { onAvailable, onUpdated }) {
     65    this.networkEvents = new Map();
     66 
     67    this.watcherActor = watcherActor;
     68    this.onNetworkEventAvailable = onAvailable;
     69    this.onNetworkEventUpdated = onUpdated;
     70    // Boolean to know if we keep previous document network events or not.
     71    this.persist = false;
     72    this.listener = new lazy.NetworkObserver({
     73      decodeResponseBodies: true,
     74      responseBodyLimit: lazy.responseBodyLimit,
     75      ignoreChannelFunction: this.shouldIgnoreChannel.bind(this),
     76      onNetworkEvent: this.onNetworkEvent.bind(this),
     77    });
     78 
     79    this.watcherActor.on(
     80      "top-browsing-context-will-navigate",
     81      this.#onTopBrowsingContextWillNavigate
     82    );
     83  }
     84 
     85  /**
     86   * Clear all the network events and the related actors.
     87   *
     88   * This is called on actor destroy, but also from WatcherActor.clearResources(NETWORK_EVENT)
     89   */
     90  clear() {
     91    this.networkEvents.clear();
     92    this.listener.clear();
     93    if (this._pool) {
     94      this._pool.destroy();
     95      this._pool = null;
     96    }
     97  }
     98 
     99  /**
    100   * A protocol.js Pool to store all NetworkEventActor's which may be destroyed on navigations.
    101   */
    102  get pool() {
    103    if (this._pool) {
    104      return this._pool;
    105    }
    106    this._pool = new Pool(this.watcherActor.conn, "network-events");
    107    this.watcherActor.manage(this._pool);
    108    return this._pool;
    109  }
    110 
    111  /**
    112   * Instruct to keep reference to previous document requests or not.
    113   * If persist is disabled, we will clear all informations about previous document
    114   * on each navigation.
    115   * If persist is enabled, we will keep all informations for all documents, leading
    116   * to lots of allocations!
    117   *
    118   * @param {boolean} enabled
    119   */
    120  setPersist(enabled) {
    121    this.persist = enabled;
    122  }
    123 
    124  /**
    125   * Gets the throttle settings
    126   *
    127   * @return {*} data
    128   */
    129  getThrottleData() {
    130    return this.listener.getThrottleData();
    131  }
    132 
    133  /**
    134   * Sets the throttle data
    135   *
    136   * @param {*} data
    137   */
    138  setThrottleData(data) {
    139    this.listener.setThrottleData(data);
    140  }
    141 
    142  /**
    143   * Instruct to save or ignore request and response bodies
    144   *
    145   * @param {boolean} save
    146   */
    147  setSaveRequestAndResponseBodies(save) {
    148    this.listener.setSaveRequestAndResponseBodies(save);
    149  }
    150 
    151  /**
    152   * Block requests based on the filters
    153   *
    154   * @param {object} filters
    155   */
    156  blockRequest(filters) {
    157    this.listener.blockRequest(filters);
    158  }
    159 
    160  /**
    161   * Unblock requests based on the fitlers
    162   *
    163   * @param {object} filters
    164   */
    165  unblockRequest(filters) {
    166    this.listener.unblockRequest(filters);
    167  }
    168 
    169  /**
    170   * Calls the listener to set blocked urls
    171   *
    172   * @param {Array} urls
    173   *        The urls to block
    174   */
    175 
    176  setBlockedUrls(urls) {
    177    this.listener.setBlockedUrls(urls);
    178  }
    179 
    180  /**
    181   * Calls the listener to get the blocked urls
    182   *
    183   * @return {Array} urls
    184   *          The blocked urls
    185   */
    186 
    187  getBlockedUrls() {
    188    return this.listener.getBlockedUrls();
    189  }
    190 
    191  override(url, path) {
    192    this.listener.override(url, path);
    193  }
    194 
    195  removeOverride(url) {
    196    this.listener.removeOverride(url);
    197  }
    198 
    199  /**
    200   * Watch for previous document being unloaded in order to clear
    201   * all related network events, in case persist is disabled.
    202   * (which is the default behavior)
    203   *
    204   * This "will-navigate" event should only be fired when debugging tabs
    205   * (not for web extensions or browser toolbox).
    206   */
    207  #onTopBrowsingContextWillNavigate = () => {
    208    // If we persist, we will keep all requests allocated.
    209    if (this.persist) {
    210      return;
    211    }
    212 
    213    const { innerWindowId } =
    214      this.watcherActor.browserElement.browsingContext.currentWindowGlobal;
    215 
    216    // When a navigation starts, destroy all network request actors as the UI should not longer show them.
    217    // We can easily destroy all requests which aren't navigation request.
    218    // But navigation requests should be preserved as they started just before the navigation
    219    // (and the will-navigate" event fired).
    220    // The current WindowGloball is still for the document we navigate **from**,
    221    // so destroy navigation requests from iframes or the WindowGlobal from the previous navigation
    222    // with the `innerWindowId` comparison.
    223    for (const child of this.pool.poolChildren()) {
    224      if (
    225        !child.isNavigationRequest() ||
    226        (child.getInnerWindowId() && child.getInnerWindowId() != innerWindowId)
    227      ) {
    228        child.destroy();
    229      }
    230    }
    231  };
    232 
    233  /**
    234   * Called by NetworkObserver in order to know if the channel should be ignored
    235   */
    236  shouldIgnoreChannel(channel) {
    237    // First of all, check if the channel matches the watcherActor's session.
    238    const filters = { sessionContext: this.watcherActor.sessionContext };
    239    if (!lazy.NetworkUtils.matchRequest(channel, filters)) {
    240      return true;
    241    }
    242 
    243    // When we are in the browser toolbox in parent process scope,
    244    // the session context is still "all", but we are no longer watching frame and process targets.
    245    // In this case, we should ignore all requests belonging to a BrowsingContext that isn't in the parent process
    246    // (i.e. the process where this Watcher runs)
    247    const isParentProcessOnlyBrowserToolbox =
    248      this.watcherActor.sessionContext.type == "all" &&
    249      !ParentProcessWatcherRegistry.isWatchingTargets(
    250        this.watcherActor,
    251        Targets.TYPES.FRAME
    252      );
    253    if (isParentProcessOnlyBrowserToolbox) {
    254      // We should ignore all requests coming from BrowsingContext running in another process
    255      const browsingContextID =
    256        lazy.NetworkUtils.getChannelBrowsingContextID(channel);
    257      const browsingContext = BrowsingContext.get(browsingContextID);
    258      // We accept any request that isn't bound to any BrowsingContext.
    259      // This is most likely a privileged request done from a JSM/C++.
    260      // `isInProcess` will be true, when the document executes in the parent process.
    261      //
    262      // Note that we will still accept all requests that aren't bound to any BrowsingContext
    263      // See browser_resources_network_events_parent_process.js test with privileged request
    264      // made from the content processes.
    265      // We miss some attribute on channel/loadInfo to know that it comes from the content process.
    266      if (browsingContext?.currentWindowGlobal.isInProcess === false) {
    267        return true;
    268      }
    269    }
    270    return false;
    271  }
    272 
    273  onNetworkEvent(networkEventOptions, channel) {
    274    if (channel.channelId && this.networkEvents.has(channel.channelId)) {
    275      throw new Error(
    276        `Got notified about channel ${channel.channelId} more than once.`
    277      );
    278    }
    279 
    280    const actor = new NetworkEventActor(
    281      this.watcherActor.conn,
    282      this.watcherActor.sessionContext,
    283      {
    284        onNetworkEventUpdate: this.onNetworkEventUpdate.bind(this),
    285        onNetworkEventDestroy: this.onNetworkEventDestroy.bind(this),
    286      },
    287      networkEventOptions,
    288      channel
    289    );
    290    this.pool.manage(actor);
    291 
    292    const resource = actor.asResource();
    293    const isBlocked = !!resource.blockedReason;
    294    const networkEvent = {
    295      browsingContextID: resource.browsingContextID,
    296      innerWindowId: resource.innerWindowId,
    297      resourceId: resource.resourceId,
    298      isBlocked,
    299      receivedUpdates: [],
    300      resourceUpdates: {},
    301    };
    302 
    303    // Requests already come with request cookies and headers, so those
    304    // should always be considered as available. But the client still
    305    // heavily relies on those `Available` flags to fetch additional data,
    306    // so it is better to keep them for consistency.
    307 
    308    // Set the flags on the resource so that the front-end can fetch
    309    // and display request headers and cookies details asap.
    310    lazy.NetworkUtils.setEventAsAvailable(resource, [
    311      lazy.NetworkUtils.NETWORK_EVENT_TYPES.REQUEST_COOKIES,
    312      lazy.NetworkUtils.NETWORK_EVENT_TYPES.REQUEST_HEADERS,
    313    ]);
    314 
    315    this.networkEvents.set(resource.resourceId, networkEvent);
    316 
    317    this.onNetworkEventAvailable([resource]);
    318 
    319    // Blocked requests will not receive further updates and should emit an
    320    // update packet immediately.
    321    // The frontend expects to receive a dedicated update to consider the
    322    // request as completed. TODO: lift this restriction so that we can only
    323    // emit a resource available notification if no update is needed.
    324    if (isBlocked) {
    325      lazy.NetworkUtils.setEventAsAvailable(networkEvent.resourceUpdates, [
    326        lazy.NetworkUtils.NETWORK_EVENT_TYPES.RESPONSE_END,
    327      ]);
    328      this._emitUpdate(networkEvent);
    329    }
    330 
    331    return actor;
    332  }
    333 
    334  onNetworkEventUpdate(updateResource) {
    335    const networkEvent = this.networkEvents.get(updateResource.resourceId);
    336 
    337    if (!networkEvent) {
    338      return;
    339    }
    340    const { NETWORK_EVENT_TYPES } = lazy.NetworkUtils;
    341    const { resourceUpdates, receivedUpdates } = networkEvent;
    342 
    343    const networkEventTypes = [
    344      NETWORK_EVENT_TYPES.RESPONSE_COOKIES,
    345      NETWORK_EVENT_TYPES.RESPONSE_HEADERS,
    346    ];
    347 
    348    switch (updateResource.updateType) {
    349      case NETWORK_EVENT_TYPES.CACHE_DETAILS:
    350        resourceUpdates.fromCache = updateResource.fromCache;
    351        resourceUpdates.fromServiceWorker = updateResource.fromServiceWorker;
    352        break;
    353      case NETWORK_EVENT_TYPES.RESPONSE_START:
    354        resourceUpdates.httpVersion = updateResource.httpVersion;
    355        resourceUpdates.status = updateResource.status;
    356        resourceUpdates.statusText = updateResource.statusText;
    357        resourceUpdates.earlyHintsStatus = updateResource.earlyHintsStatus;
    358        resourceUpdates.remoteAddress = updateResource.remoteAddress;
    359        resourceUpdates.remotePort = updateResource.remotePort;
    360        // The mimetype is only set when then the contentType is available
    361        // in the _onResponseHeader and not for cached/service worker requests
    362        // in _httpResponseExaminer.
    363        resourceUpdates.mimeType = updateResource.mimeType;
    364        resourceUpdates.waitingTime = updateResource.waitingTime;
    365        resourceUpdates.isResolvedByTRR = updateResource.isResolvedByTRR;
    366        resourceUpdates.proxyHttpVersion = updateResource.proxyHttpVersion;
    367        resourceUpdates.proxyStatus = updateResource.proxyStatus;
    368        resourceUpdates.proxyStatusText = updateResource.proxyStatusText;
    369 
    370        if (resourceUpdates.earlyHintsStatus.length) {
    371          networkEventTypes.push(
    372            NETWORK_EVENT_TYPES.EARLY_HINT_RESPONSE_HEADERS
    373          );
    374        }
    375 
    376        lazy.NetworkUtils.setEventAsAvailable(
    377          resourceUpdates,
    378          networkEventTypes
    379        );
    380 
    381        break;
    382      case NETWORK_EVENT_TYPES.RESPONSE_CONTENT:
    383        resourceUpdates.contentSize = updateResource.contentSize;
    384        resourceUpdates.transferredSize = updateResource.transferredSize;
    385        resourceUpdates.mimeType = updateResource.mimeType;
    386        break;
    387      case NETWORK_EVENT_TYPES.RESPONSE_CONTENT_COMPLETE:
    388        resourceUpdates.extension = updateResource.extension;
    389        resourceUpdates.blockedReason = updateResource.blockedReason;
    390        break;
    391      case NETWORK_EVENT_TYPES.EVENT_TIMINGS:
    392        resourceUpdates.totalTime = updateResource.totalTime;
    393        break;
    394      case NETWORK_EVENT_TYPES.SECURITY_INFO:
    395        resourceUpdates.securityState = updateResource.state;
    396        resourceUpdates.isRacing = updateResource.isRacing;
    397        break;
    398    }
    399 
    400    lazy.NetworkUtils.setEventAsAvailable(resourceUpdates, [
    401      updateResource.updateType,
    402    ]);
    403 
    404    receivedUpdates.push(updateResource.updateType);
    405 
    406    const isResponseComplete =
    407      receivedUpdates.includes(NETWORK_EVENT_TYPES.EVENT_TIMINGS) &&
    408      receivedUpdates.includes(NETWORK_EVENT_TYPES.RESPONSE_CONTENT_COMPLETE) &&
    409      receivedUpdates.includes(NETWORK_EVENT_TYPES.SECURITY_INFO);
    410 
    411    if (isResponseComplete) {
    412      // Lets add an event to clearly define the last update expected to be
    413      // emitted. There will be no more updates after this.
    414      lazy.NetworkUtils.setEventAsAvailable(resourceUpdates, [
    415        lazy.NetworkUtils.NETWORK_EVENT_TYPES.RESPONSE_END,
    416      ]);
    417    }
    418 
    419    if (
    420      updateResource.updateType == NETWORK_EVENT_TYPES.RESPONSE_START ||
    421      updateResource.updateType == NETWORK_EVENT_TYPES.RESPONSE_CONTENT ||
    422      isResponseComplete
    423    ) {
    424      this._emitUpdate(networkEvent);
    425      // clean up already sent updates
    426      networkEvent.resourceUpdates = {};
    427    }
    428  }
    429 
    430  _emitUpdate(networkEvent) {
    431    this.onNetworkEventUpdated([
    432      {
    433        resourceId: networkEvent.resourceId,
    434        resourceUpdates: networkEvent.resourceUpdates,
    435        browsingContextID: networkEvent.browsingContextID,
    436        innerWindowId: networkEvent.innerWindowId,
    437      },
    438    ]);
    439  }
    440 
    441  onNetworkEventDestroy(channelId) {
    442    if (this.networkEvents.has(channelId)) {
    443      this.networkEvents.delete(channelId);
    444    }
    445  }
    446 
    447  /**
    448   * Stop watching for network event related to a given Watcher Actor.
    449   */
    450  destroy() {
    451    if (this.listener) {
    452      this.clear();
    453      this.listener.destroy();
    454      this.watcherActor.off(
    455        "top-browsing-context-will-navigate",
    456        this.#onTopBrowsingContextWillNavigate
    457      );
    458    }
    459  }
    460 }
    461 
    462 module.exports = NetworkEventWatcher;