tor-browser

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

network-events-content.js (11370B)


      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 loader.lazyRequireGetter(
      8  this,
      9  "NetworkEventActor",
     10  "resource://devtools/server/actors/network-monitor/network-event-actor.js",
     11  true
     12 );
     13 
     14 const lazy = {};
     15 
     16 ChromeUtils.defineESModuleGetters(
     17  lazy,
     18  {
     19    NetworkUtils:
     20      "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs",
     21  },
     22  { global: "contextual" }
     23 );
     24 
     25 // Internal resource types used to create the appropriate network event based
     26 // on where the channel / resource is coming from.
     27 const RESOURCE_TYPES = {
     28  BLOCKED: "blocked-resource",
     29  CACHED: "cached-resource",
     30  DATA_CHANNEL: "data-channel-resource",
     31 };
     32 
     33 /**
     34 * Handles network events from the content process
     35 * This currently only handles events for requests (js/css) blocked by CSP.
     36 */
     37 class NetworkEventContentWatcher {
     38  /**
     39   * Start watching for all network events related to a given Target Actor.
     40   *
     41   * @param TargetActor targetActor
     42   *        The target actor in the content process from which we should
     43   *        observe network events.
     44   * @param Object options
     45   *        Dictionary object with following attributes:
     46   *        - onAvailable: mandatory function
     47   *          This will be called for each resource.
     48   *        - onUpdated: optional function
     49   *          This would be called multiple times for each resource.
     50   */
     51  async watch(targetActor, { onAvailable, onUpdated }) {
     52    // Map from channelId to network event objects.
     53    this.networkEvents = new Map();
     54 
     55    this.targetActor = targetActor;
     56    this.onAvailable = onAvailable;
     57    this.onUpdated = onUpdated;
     58 
     59    Services.obs.addObserver(
     60      this.httpFailedOpeningRequest,
     61      "http-on-failed-opening-request"
     62    );
     63 
     64    Services.obs.addObserver(
     65      this.httpOnResourceCacheResponse,
     66      "http-on-resource-cache-response"
     67    );
     68 
     69    Services.obs.addObserver(this.onDataChannelOpened, "data-channel-opened");
     70  }
     71  /**
     72   * Allows clearing of network events
     73   */
     74  clear() {
     75    this.networkEvents.clear();
     76  }
     77 
     78  httpFailedOpeningRequest = (subject, topic) => {
     79    if (
     80      topic != "http-on-failed-opening-request" ||
     81      !(subject instanceof Ci.nsIHttpChannel)
     82    ) {
     83      const channel = subject.QueryInterface(Ci.nsIChannel);
     84      console.warn(
     85        `httpFailedOpeningRequest triggered on non-nsIHttpChannel for uri: ${channel.URI.spec}`
     86      );
     87      return;
     88    }
     89 
     90    const channel = subject.QueryInterface(Ci.nsIHttpChannel);
     91 
     92    // Ignore preload requests to avoid duplicity request entries in
     93    // the Network panel. If a preload fails (for whatever reason)
     94    // then the platform kicks off another 'real' request.
     95    if (lazy.NetworkUtils.isPreloadRequest(channel)) {
     96      return;
     97    }
     98 
     99    if (
    100      !lazy.NetworkUtils.matchRequest(channel, {
    101        targetActor: this.targetActor,
    102      })
    103    ) {
    104      return;
    105    }
    106 
    107    this.onNetworkEventAvailable(channel, {
    108      networkEventOptions: {
    109        blockedReason: channel.loadInfo.requestBlockingReason,
    110      },
    111      type: RESOURCE_TYPES.BLOCKED,
    112    });
    113  };
    114 
    115  httpOnResourceCacheResponse = (subject, topic) => {
    116    if (
    117      topic != "http-on-resource-cache-response" ||
    118      !(subject instanceof Ci.nsIHttpChannel)
    119    ) {
    120      return;
    121    }
    122 
    123    const channel = subject.QueryInterface(Ci.nsIHttpChannel);
    124 
    125    if (
    126      !lazy.NetworkUtils.matchRequest(channel, {
    127        targetActor: this.targetActor,
    128      })
    129    ) {
    130      return;
    131    }
    132 
    133    if (
    134      channel.loadInfo?.externalContentPolicyType !==
    135      Ci.nsIContentPolicy.TYPE_SCRIPT
    136    ) {
    137      // For images and stylesheets from the cache, only one network request
    138      // should be created per URI.
    139      //
    140      // For scripts from the cache, multiple network requests should be
    141      // created.
    142      const hasURI = Array.from(this.networkEvents.values()).some(
    143        networkEvent => networkEvent.uri === channel.URI.spec
    144      );
    145 
    146      if (hasURI) {
    147        return;
    148      }
    149    }
    150 
    151    this.onNetworkEventAvailable(channel, {
    152      fromCache: true,
    153      networkEventOptions: {},
    154      type: RESOURCE_TYPES.CACHED,
    155    });
    156  };
    157 
    158  onDataChannelOpened = (subject, topic) => {
    159    if (
    160      topic != "data-channel-opened" ||
    161      !(subject instanceof Ci.nsIDataChannel)
    162    ) {
    163      return;
    164    }
    165 
    166    const channel = subject.QueryInterface(Ci.nsIDataChannel);
    167    channel.QueryInterface(Ci.nsIIdentChannel);
    168    channel.QueryInterface(Ci.nsIChannel);
    169 
    170    if (channel.isDocument) {
    171      // Navigation data channels are available in the parent process and will
    172      // be monitored there.
    173      return;
    174    }
    175 
    176    if (
    177      !lazy.NetworkUtils.matchRequest(channel, {
    178        targetActor: this.targetActor,
    179      })
    180    ) {
    181      return;
    182    }
    183 
    184    this.onNetworkEventAvailable(channel, {
    185      fromCache: false,
    186      networkEventOptions: {},
    187      type: RESOURCE_TYPES.DATA_CHANNEL,
    188    });
    189  };
    190 
    191  onNetworkEventAvailable(channel, { fromCache, networkEventOptions, type }) {
    192    const networkEventActor = new NetworkEventActor(
    193      this.targetActor.conn,
    194      this.targetActor.sessionContext,
    195      {
    196        onNetworkEventUpdate: this.onNetworkEventUpdate.bind(this),
    197        onNetworkEventDestroy: this.onNetworkEventDestroyed.bind(this),
    198      },
    199      networkEventOptions,
    200      channel
    201    );
    202    this.targetActor.manage(networkEventActor);
    203 
    204    const resource = networkEventActor.asResource();
    205 
    206    const networkEvent = {
    207      browsingContextID: resource.browsingContextID,
    208      innerWindowId: resource.innerWindowId,
    209      resourceId: resource.resourceId,
    210      receivedUpdates: [],
    211      resourceUpdates: {},
    212      uri: channel.URI.spec,
    213    };
    214 
    215    // Requests already come with request cookies and headers, so those
    216    // should always be considered as available. But the client still
    217    // heavily relies on those `Available` flags to fetch additional data,
    218    // so it is better to keep them for consistency.
    219 
    220    // Set the flags on the resource so that the front-end can fetch
    221    // and display request headers and cookies details asap.
    222    lazy.NetworkUtils.setEventAsAvailable(resource, [
    223      lazy.NetworkUtils.NETWORK_EVENT_TYPES.REQUEST_HEADERS,
    224      lazy.NetworkUtils.NETWORK_EVENT_TYPES.REQUEST_COOKIES,
    225    ]);
    226 
    227    this.networkEvents.set(resource.resourceId, networkEvent);
    228 
    229    this.onAvailable([resource]);
    230 
    231    networkEventActor.addCacheDetails({ fromCache });
    232    if (type == RESOURCE_TYPES.BLOCKED) {
    233      lazy.NetworkUtils.setEventAsAvailable(networkEvent.resourceUpdates, [
    234        lazy.NetworkUtils.NETWORK_EVENT_TYPES.RESPONSE_END,
    235      ]);
    236      this._emitUpdate(networkEvent);
    237    } else if (type == RESOURCE_TYPES.CACHED) {
    238      networkEventActor.addResponseStart({ channel, fromCache: true });
    239      networkEventActor.addEventTimings(
    240        0 /* totalTime */,
    241        {} /* timings */,
    242        {} /* offsets */
    243      );
    244      networkEventActor.addServerTimings({});
    245      networkEventActor.addResponseContent({
    246        mimeType: channel.contentType,
    247        size: channel.contentLength,
    248        text: "",
    249        transferredSize: 0,
    250      });
    251      networkEventActor.addResponseContentComplete({});
    252    } else if (type == RESOURCE_TYPES.DATA_CHANNEL) {
    253      lazy.NetworkUtils.handleDataChannel(channel, networkEventActor);
    254    }
    255  }
    256 
    257  onNetworkEventUpdate(updateResource) {
    258    const networkEvent = this.networkEvents.get(updateResource.resourceId);
    259 
    260    if (!networkEvent) {
    261      return;
    262    }
    263 
    264    const { NETWORK_EVENT_TYPES } = lazy.NetworkUtils;
    265    const { resourceUpdates, receivedUpdates } = networkEvent;
    266 
    267    switch (updateResource.updateType) {
    268      case NETWORK_EVENT_TYPES.CACHE_DETAILS:
    269        resourceUpdates.fromCache = updateResource.fromCache;
    270        resourceUpdates.fromServiceWorker = updateResource.fromServiceWorker;
    271        break;
    272      case NETWORK_EVENT_TYPES.RESPONSE_START: {
    273        // For cached image requests channel.responseStatus is set to 200 as
    274        // expected. However responseStatusText is empty. In this case fallback
    275        // to the expected statusText "OK".
    276        let statusText = updateResource.statusText;
    277        if (!statusText && updateResource.status === "200") {
    278          statusText = "OK";
    279        }
    280        resourceUpdates.httpVersion = updateResource.httpVersion;
    281        resourceUpdates.status = updateResource.status;
    282        resourceUpdates.statusText = statusText;
    283        resourceUpdates.remoteAddress = updateResource.remoteAddress;
    284        resourceUpdates.remotePort = updateResource.remotePort;
    285        resourceUpdates.waitingTime = updateResource.waitingTime;
    286 
    287        lazy.NetworkUtils.setEventAsAvailable(resourceUpdates, [
    288          NETWORK_EVENT_TYPES.RESPONSE_COOKIES,
    289          NETWORK_EVENT_TYPES.RESPONSE_HEADERS,
    290        ]);
    291        break;
    292      }
    293      case NETWORK_EVENT_TYPES.RESPONSE_CONTENT:
    294        resourceUpdates.contentSize = updateResource.contentSize;
    295        resourceUpdates.mimeType = updateResource.mimeType;
    296        resourceUpdates.transferredSize = updateResource.transferredSize;
    297        break;
    298      case NETWORK_EVENT_TYPES.EVENT_TIMINGS:
    299        resourceUpdates.totalTime = updateResource.totalTime;
    300        break;
    301    }
    302 
    303    lazy.NetworkUtils.setEventAsAvailable(resourceUpdates, [
    304      updateResource.updateType,
    305    ]);
    306 
    307    receivedUpdates.push(updateResource.updateType);
    308 
    309    // Here we explicitly call all three `add` helpers on each network event
    310    // actor so in theory we could check only the last one to be called, ie
    311    // responseContent.
    312    const isResponseComplete =
    313      receivedUpdates.includes(NETWORK_EVENT_TYPES.RESPONSE_START) &&
    314      receivedUpdates.includes(NETWORK_EVENT_TYPES.RESPONSE_CONTENT_COMPLETE) &&
    315      receivedUpdates.includes(NETWORK_EVENT_TYPES.EVENT_TIMINGS);
    316 
    317    if (isResponseComplete) {
    318      // Lets add an event to clearly define the last update expected to be
    319      // emitted. There will be no more updates after this.
    320      lazy.NetworkUtils.setEventAsAvailable(resourceUpdates, [
    321        NETWORK_EVENT_TYPES.RESPONSE_END,
    322      ]);
    323    }
    324 
    325    if (
    326      updateResource.updateType == NETWORK_EVENT_TYPES.RESPONSE_START ||
    327      updateResource.updateType == NETWORK_EVENT_TYPES.RESPONSE_CONTENT ||
    328      isResponseComplete
    329    ) {
    330      this._emitUpdate(networkEvent);
    331      // clean up already sent updates
    332      networkEvent.resourceUpdates = {};
    333    }
    334  }
    335 
    336  _emitUpdate(networkEvent) {
    337    this.onUpdated([
    338      {
    339        resourceId: networkEvent.resourceId,
    340        resourceUpdates: networkEvent.resourceUpdates,
    341        browsingContextID: networkEvent.browsingContextID,
    342        innerWindowId: networkEvent.innerWindowId,
    343      },
    344    ]);
    345  }
    346 
    347  onNetworkEventDestroyed(channelId) {
    348    if (this.networkEvents.has(channelId)) {
    349      this.networkEvents.delete(channelId);
    350    }
    351  }
    352 
    353  destroy() {
    354    this.clear();
    355    Services.obs.removeObserver(
    356      this.httpFailedOpeningRequest,
    357      "http-on-failed-opening-request"
    358    );
    359 
    360    Services.obs.removeObserver(
    361      this.httpOnResourceCacheResponse,
    362      "http-on-resource-cache-response"
    363    );
    364 
    365    Services.obs.removeObserver(
    366      this.onDataChannelOpened,
    367      "data-channel-opened"
    368    );
    369  }
    370 }
    371 
    372 module.exports = NetworkEventContentWatcher;