tor-browser

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

NetworkResponse.sys.mjs (9399B)


      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 const lazy = {};
      6 ChromeUtils.defineESModuleGetters(lazy, {
      7  NetworkUtils:
      8    "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs",
      9 
     10  NetworkDataBytes: "chrome://remote/content/shared/NetworkDataBytes.sys.mjs",
     11 });
     12 
     13 /**
     14 * The NetworkResponse class is a wrapper around the internal channel which
     15 * provides getters and methods closer to fetch's response concept
     16 * (https://fetch.spec.whatwg.org/#concept-response).
     17 */
     18 export class NetworkResponse {
     19  #channel;
     20  #decodedBodySize;
     21  #encodedBodySize;
     22  #fromCache;
     23  #fromServiceWorker;
     24  #isCachedResource;
     25  #isDataURL;
     26  #headersTransmittedSize;
     27  #responseBodyReady;
     28  #status;
     29  #statusMessage;
     30  #totalTransmittedSize;
     31  #wrappedChannel;
     32 
     33  /**
     34   *
     35   * @param {nsIChannel} channel
     36   *     The channel for the response.
     37   * @param {object} params
     38   * @param {boolean} params.fromCache
     39   *     Whether the response was read from the cache or not.
     40   * @param {boolean} params.fromServiceWorker
     41   *     Whether the response is coming from a service worker or not.
     42   * @param {boolean} params.isCachedResource
     43   *     Whether the response is served by the stencil (image/CSS/JS) cache.
     44   * @param {string=} params.rawHeaders
     45   *     The response's raw (ie potentially compressed) headers
     46   */
     47  constructor(channel, params) {
     48    this.#channel = channel;
     49    const {
     50      fromCache,
     51      fromServiceWorker,
     52      isCachedResource,
     53      rawHeaders = "",
     54    } = params;
     55    this.#fromCache = fromCache;
     56    this.#fromServiceWorker = fromServiceWorker;
     57    this.#isCachedResource = isCachedResource;
     58    this.#isDataURL = this.#channel instanceof Ci.nsIDataChannel;
     59    this.#responseBodyReady = Promise.withResolvers();
     60    this.#wrappedChannel = ChannelWrapper.get(channel);
     61 
     62    this.#decodedBodySize = 0;
     63    this.#encodedBodySize = 0;
     64    this.#headersTransmittedSize = rawHeaders.length;
     65    this.#totalTransmittedSize = rawHeaders.length;
     66 
     67    // See https://github.com/w3c/webdriver-bidi/issues/761
     68    // For 304 responses, the response will be replaced by the cached response
     69    // between responseStarted and responseCompleted, which will effectively
     70    // change the status and statusMessage.
     71    // Until the issue linked above has been discussed and closed, we will
     72    // cache the status/statusMessage in order to ensure consistent values
     73    // between responseStarted and responseCompleted.
     74    this.#status = this.#isDataURL ? 200 : this.#channel.responseStatus;
     75    this.#statusMessage =
     76      this.#isDataURL || this.#isCachedResource
     77        ? "OK"
     78        : this.#channel.responseStatusText;
     79  }
     80 
     81  get decodedBodySize() {
     82    return this.#decodedBodySize;
     83  }
     84 
     85  get encodedBodySize() {
     86    return this.#encodedBodySize;
     87  }
     88 
     89  get headers() {
     90    return this.#getHeadersList();
     91  }
     92 
     93  get headersTransmittedSize() {
     94    return this.#headersTransmittedSize;
     95  }
     96 
     97  get fromCache() {
     98    return this.#fromCache;
     99  }
    100 
    101  get fromServiceWorker() {
    102    return this.#fromServiceWorker;
    103  }
    104 
    105  get isDataURL() {
    106    return this.#isDataURL;
    107  }
    108 
    109  get mimeType() {
    110    return this.#getComputedMimeType();
    111  }
    112 
    113  get protocol() {
    114    return lazy.NetworkUtils.getProtocol(this.#channel);
    115  }
    116 
    117  get serializedURL() {
    118    return this.#channel.URI.spec;
    119  }
    120 
    121  get status() {
    122    return this.#status;
    123  }
    124 
    125  get statusMessage() {
    126    return this.#statusMessage;
    127  }
    128 
    129  get totalTransmittedSize() {
    130    return this.#totalTransmittedSize;
    131  }
    132 
    133  /**
    134   * Check if this response will lead to a redirect.
    135   */
    136  get willRedirect() {
    137    // See static helper on nsHttpChannel:WillRedirect
    138    // https://searchfox.org/mozilla-central/rev/6b4cb595d05ac38e2cfc493e3b81fe4c97a71f12/netwerk/protocol/http/nsHttpChannel.cpp#283-288
    139    const isRedirectStatus =
    140      this.#status == 301 ||
    141      this.#status == 302 ||
    142      this.#status == 303 ||
    143      this.#status == 307 ||
    144      this.#status == 308;
    145    return isRedirectStatus && this.#channel.getResponseHeader("Location");
    146  }
    147 
    148  /**
    149   * Clear a response header from the responses's headers list.
    150   *
    151   * @param {string} name
    152   *     The header's name.
    153   */
    154  clearResponseHeader(name) {
    155    this.#channel.setResponseHeader(
    156      name, // aName
    157      "", // aValue="" as an empty value
    158      false // aMerge=false to force clearing the header
    159    );
    160  }
    161 
    162  /**
    163   * Returns the NetworkDataBytes instance representing the response body for
    164   * this response.
    165   *
    166   * @returns {NetworkDataBytes}
    167   */
    168  readAndProcessResponseBody = async () => {
    169    const responseContent = await this.#responseBodyReady.promise;
    170 
    171    return new lazy.NetworkDataBytes({
    172      getBytesValue: async () => {
    173        if (responseContent.isContentEncoded) {
    174          return lazy.NetworkUtils.decodeResponseChunks(
    175            responseContent.encodedData,
    176            {
    177              // Should always attempt to decode as UTF-8.
    178              charset: "UTF-8",
    179              compressionEncodings: responseContent.compressionEncodings,
    180              encodedBodySize: responseContent.encodedBodySize,
    181              encoding: responseContent.encoding,
    182            }
    183          );
    184        }
    185        return responseContent.text;
    186      },
    187      isBase64: responseContent.encoding === "base64",
    188    });
    189  };
    190 
    191  setResponseContent(responseContent) {
    192    this.#responseBodyReady.resolve(responseContent);
    193  }
    194 
    195  /**
    196   * Set a response header
    197   *
    198   * @param {string} name
    199   *     The header's name.
    200   * @param {string} value
    201   *     The header's value.
    202   * @param {object} options
    203   * @param {boolean} options.merge
    204   *     True if the value should be merged with the existing value, false if it
    205   *     should override it. Defaults to false.
    206   */
    207  setResponseHeader(name, value, options) {
    208    const { merge = false } = options;
    209    this.#channel.setResponseHeader(name, value, merge);
    210  }
    211 
    212  setResponseStatus(options) {
    213    let { status, statusText } = options;
    214    if (status === null) {
    215      status = this.#channel.responseStatus;
    216    }
    217 
    218    if (statusText === null) {
    219      statusText = this.#channel.responseStatusText;
    220    }
    221 
    222    this.#channel.setResponseStatus(status, statusText);
    223 
    224    // Update the cached status and statusMessage.
    225    this.#status = this.#channel.responseStatus;
    226    this.#statusMessage = this.#channel.responseStatusText;
    227  }
    228 
    229  /**
    230   * Set the various response sizes for this response. Depending on how the
    231   * completion was monitored (DevTools NetworkResponseListener or ChannelWrapper
    232   * event), sizes need to be retrieved differently.
    233   * There this is a simple setter and the actual logic to retrieve sizes is in
    234   * NetworkEventRecord.
    235   *
    236   * @param {object} sizes
    237   * @param {number} sizes.decodedBodySize
    238   *     The decoded body size.
    239   * @param {number} sizes.encodedBodySize
    240   *     The encoded body size.
    241   * @param {number} sizes.totalTransmittedSize
    242   *     The total transmitted size.
    243   */
    244  setResponseSizes(sizes) {
    245    const { decodedBodySize, encodedBodySize, totalTransmittedSize } = sizes;
    246    this.#decodedBodySize = decodedBodySize;
    247    this.#encodedBodySize = encodedBodySize;
    248    this.#totalTransmittedSize = totalTransmittedSize;
    249  }
    250 
    251  /**
    252   * Return a static version of the class instance.
    253   * This method is used to prepare the data to be sent with the events for cached resources
    254   * generated from the content process but need to be sent to the parent.
    255   */
    256  toJSON() {
    257    return {
    258      decodedBodySize: this.decodedBodySize,
    259      encodedBodySize: this.encodedBodySize,
    260      fromCache: this.fromCache,
    261      headers: this.headers,
    262      headersTransmittedSize: this.headersTransmittedSize,
    263      isDataURL: this.isDataURL,
    264      mimeType: this.mimeType,
    265      protocol: this.protocol,
    266      serializedURL: this.serializedURL,
    267      status: this.status,
    268      statusMessage: this.statusMessage,
    269      totalTransmittedSize: this.totalTransmittedSize,
    270      willRedirect: this.willRedirect,
    271    };
    272  }
    273 
    274  #getComputedMimeType() {
    275    // TODO: DevTools NetworkObserver is computing a similar value in
    276    // addResponseContent, but uses an inconsistent implementation in
    277    // addResponseStart. This approach can only be used as early as in
    278    // addResponseHeaders. We should move this logic to the NetworkObserver and
    279    // expose mimeType in addResponseStart. Bug 1809670.
    280    let mimeType = "";
    281 
    282    try {
    283      if (this.#isDataURL || this.#isCachedResource) {
    284        mimeType = this.#channel.contentType;
    285      } else {
    286        mimeType = this.#wrappedChannel.contentType;
    287      }
    288      const contentCharset = this.#channel.contentCharset;
    289      if (contentCharset) {
    290        mimeType += `;charset=${contentCharset}`;
    291      }
    292    } catch (e) {
    293      // Ignore exceptions when reading contentType/contentCharset
    294    }
    295 
    296    return mimeType;
    297  }
    298 
    299  #getHeadersList() {
    300    const headers = [];
    301 
    302    // According to the fetch spec for data URLs we can just hardcode
    303    // "Content-Type" header.
    304    if (this.#isDataURL) {
    305      headers.push(["Content-Type", this.#channel.contentType]);
    306    } else {
    307      this.#channel.visitResponseHeaders({
    308        visitHeader(name, value) {
    309          headers.push([name, value]);
    310        },
    311      });
    312    }
    313 
    314    return headers;
    315  }
    316 }