tor-browser

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

NetworkRequest.sys.mjs (16848B)


      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  NetworkHelper:
      8    "resource://devtools/shared/network-observer/NetworkHelper.sys.mjs",
      9  NetworkUtils:
     10    "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs",
     11 
     12  generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
     13  NavigableManager: "chrome://remote/content/shared/NavigableManager.sys.mjs",
     14  NavigationState: "chrome://remote/content/shared/NavigationManager.sys.mjs",
     15  NetworkDataBytes: "chrome://remote/content/shared/NetworkDataBytes.sys.mjs",
     16  notifyNavigationStarted:
     17    "chrome://remote/content/shared/NavigationManager.sys.mjs",
     18 });
     19 
     20 /**
     21 * The NetworkRequest class is a wrapper around the internal channel which
     22 * provides getters and methods closer to fetch's response concept
     23 * (https://fetch.spec.whatwg.org/#concept-response).
     24 */
     25 export class NetworkRequest {
     26  #alreadyCompleted;
     27  #channel;
     28  #contextId;
     29  #eventRecord;
     30  #isDataURL;
     31  #navigationId;
     32  #navigationManager;
     33  #postData;
     34  #postDataSize;
     35  #rawHeaders;
     36  #redirectCount;
     37  #requestId;
     38  #timedChannel;
     39  #wrappedChannel;
     40 
     41  /**
     42   * NetworkRequest relies on WrappedChannel's id to identify requests. However
     43   * this id is generated based on a counter in each process. Therefore we can
     44   * have overlaps for network requests handled in different processes
     45   */
     46  static UNIQUE_ID_SUFFIX = lazy.generateUUID();
     47 
     48  /**
     49   *
     50   * @param {nsIChannel} channel
     51   *     The channel for the request.
     52   * @param {object} params
     53   * @param {NetworkEventRecord} params.networkEventRecord
     54   *     The NetworkEventRecord owning this NetworkRequest.
     55   * @param {NavigationManager} params.navigationManager
     56   *     The NavigationManager where navigations for the current session are
     57   *     monitored.
     58   * @param {string=} params.rawHeaders
     59   *     The request's raw (ie potentially compressed) headers
     60   */
     61  constructor(channel, params) {
     62    const { eventRecord, navigationManager, rawHeaders = "" } = params;
     63 
     64    this.#channel = channel;
     65    this.#eventRecord = eventRecord;
     66    this.#isDataURL = this.#channel instanceof Ci.nsIDataChannel;
     67    this.#navigationManager = navigationManager;
     68    this.#rawHeaders = rawHeaders;
     69 
     70    // Platform timestamp is in microseconds.
     71    const currentTimeStamp = Date.now() * 1000;
     72    this.#timedChannel =
     73      this.#channel instanceof Ci.nsITimedChannel
     74        ? this.#channel.QueryInterface(Ci.nsITimedChannel)
     75        : {
     76            redirectCount: 0,
     77            initiatorType: "",
     78            asyncOpenTime: currentTimeStamp,
     79            redirectStartTime: 0,
     80            redirectEndTime: 0,
     81            domainLookupStartTime: currentTimeStamp,
     82            domainLookupEndTime: currentTimeStamp,
     83            connectStartTime: currentTimeStamp,
     84            connectEndTime: currentTimeStamp,
     85            secureConnectionStartTime: currentTimeStamp,
     86            requestStartTime: currentTimeStamp,
     87            responseStartTime: currentTimeStamp,
     88            responseEndTime: currentTimeStamp,
     89          };
     90    this.#wrappedChannel = ChannelWrapper.get(channel);
     91 
     92    this.#redirectCount = this.#timedChannel.redirectCount;
     93 
     94    // The wrappedChannel id remains identical across redirects, whereas
     95    // nsIChannel.channelId is different for each and every request.
     96    // Add a suffix unique to the process where the event is handled.
     97    this.#requestId = `${this.#wrappedChannel.id.toString()}-${NetworkRequest.UNIQUE_ID_SUFFIX}`;
     98 
     99    this.#contextId = this.#getContextId();
    100    this.#navigationId = this.#getNavigationId();
    101 
    102    // The postData will no longer be available after the channel is closed.
    103    // Compute the postData and postDataSize properties, to be updated later if
    104    // `setRequestBody` is used.
    105    this.#updatePostData();
    106  }
    107 
    108  get alreadyCompleted() {
    109    return this.#alreadyCompleted;
    110  }
    111 
    112  get channel() {
    113    return this.#channel;
    114  }
    115 
    116  get contextId() {
    117    return this.#contextId;
    118  }
    119 
    120  get destination() {
    121    return this.#channel.loadInfo?.fetchDestination;
    122  }
    123 
    124  get errorText() {
    125    // TODO: Update with a proper error text. Bug 1873037.
    126    return ChromeUtils.getXPCOMErrorName(this.#channel.status);
    127  }
    128 
    129  get headers() {
    130    return this.#getHeadersList();
    131  }
    132 
    133  get headersSize() {
    134    // TODO: rawHeaders will not be updated after modifying the headers via
    135    // request interception. Need to find another way to retrieve the
    136    // information dynamically.
    137    return this.#rawHeaders.length;
    138  }
    139 
    140  get initiatorType() {
    141    const initiatorType = this.#timedChannel.initiatorType;
    142    if (initiatorType === "") {
    143      return null;
    144    }
    145 
    146    if (this.#isTopLevelDocumentLoad()) {
    147      return null;
    148    }
    149 
    150    return initiatorType;
    151  }
    152 
    153  get isHttpChannel() {
    154    return this.#channel instanceof Ci.nsIHttpChannel;
    155  }
    156 
    157  get method() {
    158    return this.#isDataURL ? "GET" : this.#channel.requestMethod;
    159  }
    160 
    161  get navigationId() {
    162    return this.#navigationId;
    163  }
    164 
    165  get postData() {
    166    return this.#postData;
    167  }
    168 
    169  get postDataSize() {
    170    return this.#postDataSize;
    171  }
    172 
    173  get redirectCount() {
    174    return this.#redirectCount;
    175  }
    176 
    177  get requestId() {
    178    return this.#requestId;
    179  }
    180 
    181  get serializedURL() {
    182    return this.#channel.URI.spec;
    183  }
    184 
    185  get supportsInterception() {
    186    // The request which doesn't have `wrappedChannel` can not be intercepted.
    187    return !!this.#wrappedChannel;
    188  }
    189 
    190  get timings() {
    191    return this.#getFetchTimings();
    192  }
    193 
    194  get wrappedChannel() {
    195    return this.#wrappedChannel;
    196  }
    197 
    198  set alreadyCompleted(value) {
    199    this.#alreadyCompleted = value;
    200  }
    201 
    202  /**
    203   * Add information about raw headers, collected from NetworkObserver events.
    204   *
    205   * @param {string} rawHeaders
    206   *     The raw headers.
    207   */
    208  addRawHeaders(rawHeaders) {
    209    this.#rawHeaders = rawHeaders || "";
    210  }
    211 
    212  /**
    213   * Clear a request header from the request's headers list.
    214   *
    215   * @param {string} name
    216   *     The header's name.
    217   */
    218  clearRequestHeader(name) {
    219    this.#channel.setRequestHeader(
    220      name, // aName
    221      "", // aValue="" as an empty value
    222      false // aMerge=false to force clearing the header
    223    );
    224  }
    225 
    226  /**
    227   * Returns the NetworkDataBytes instance representing the request body for
    228   * this request.
    229   *
    230   * @returns {NetworkDataBytes}
    231   */
    232  readAndProcessRequestBody = () => {
    233    return new lazy.NetworkDataBytes({
    234      getBytesValue: () => this.#postData.text,
    235      isBase64: this.#postData.isBase64,
    236    });
    237  };
    238 
    239  /**
    240   * Redirect the request to another provided URL.
    241   *
    242   * @param {string} url
    243   *     The URL to redirect to.
    244   */
    245  redirectTo(url) {
    246    this.#channel.transparentRedirectTo(Services.io.newURI(url));
    247  }
    248 
    249  /**
    250   * Set the request post body
    251   *
    252   * @param {string} body
    253   *     The body to set.
    254   */
    255  setRequestBody(body) {
    256    // Update the requestObserversCalled flag to allow modifying the request,
    257    // and reset once done.
    258    this.#channel.requestObserversCalled = false;
    259 
    260    try {
    261      this.#channel.QueryInterface(Ci.nsIUploadChannel2);
    262      const bodyStream = Cc[
    263        "@mozilla.org/io/string-input-stream;1"
    264      ].createInstance(Ci.nsIStringInputStream);
    265      bodyStream.setByteStringData(body);
    266      this.#channel.explicitSetUploadStream(
    267        bodyStream,
    268        null,
    269        -1,
    270        this.#channel.requestMethod,
    271        false
    272      );
    273    } finally {
    274      // Make sure to reset the flag once the modification was attempted.
    275      this.#channel.requestObserversCalled = true;
    276      this.#updatePostData();
    277    }
    278  }
    279 
    280  /**
    281   * Set a request header
    282   *
    283   * @param {string} name
    284   *     The header's name.
    285   * @param {string} value
    286   *     The header's value.
    287   * @param {object} options
    288   * @param {boolean} options.merge
    289   *     True if the value should be merged with the existing value, false if it
    290   *     should override it. Defaults to false.
    291   */
    292  setRequestHeader(name, value, options) {
    293    const { merge = false } = options;
    294    this.#channel.setRequestHeader(name, value, merge);
    295  }
    296 
    297  /**
    298   * Update the request's method.
    299   *
    300   * @param {string} method
    301   *     The method to set.
    302   */
    303  setRequestMethod(method) {
    304    // Update the requestObserversCalled flag to allow modifying the request,
    305    // and reset once done.
    306    this.#channel.requestObserversCalled = false;
    307 
    308    try {
    309      this.#channel.requestMethod = method;
    310    } finally {
    311      // Make sure to reset the flag once the modification was attempted.
    312      this.#channel.requestObserversCalled = true;
    313    }
    314  }
    315 
    316  /**
    317   * Allows to bypass the actual network request and immediately respond with
    318   * the provided nsIReplacedHttpResponse.
    319   *
    320   * @param {nsIReplacedHttpResponse} replacedHttpResponse
    321   *     The replaced response to use.
    322   */
    323  setResponseOverride(replacedHttpResponse) {
    324    this.wrappedChannel.channel
    325      .QueryInterface(Ci.nsIHttpChannelInternal)
    326      .setResponseOverride(replacedHttpResponse);
    327 
    328    const rawHeaders = [];
    329    replacedHttpResponse.visitResponseHeaders({
    330      visitHeader(name, value) {
    331        rawHeaders.push(`${name}: ${value}`);
    332      },
    333    });
    334 
    335    // Setting an override bypasses the usual codepath for network responses.
    336    // There will be no notification about receiving a response.
    337    // However, there will be a notification about the end of the response.
    338    // Therefore, simulate a addResponseStart here to make sure we handle
    339    // addResponseContent properly.
    340    this.#eventRecord.prepareResponseStart({
    341      channel: this.#channel,
    342      fromCache: false,
    343      rawHeaders: rawHeaders.join("\n"),
    344    });
    345  }
    346 
    347  /**
    348   * Return a static version of the class instance.
    349   * This method is used to prepare the data to be sent with the events for cached resources
    350   * generated from the content process but need to be sent to the parent.
    351   */
    352  toJSON() {
    353    return {
    354      destination: this.destination,
    355      headers: this.headers,
    356      headersSize: this.headersSize,
    357      initiatorType: this.initiatorType,
    358      method: this.method,
    359      navigationId: this.navigationId,
    360      postData: this.postData,
    361      postDataSize: this.postDataSize,
    362      redirectCount: this.redirectCount,
    363      requestId: this.requestId,
    364      serializedURL: this.serializedURL,
    365      // Since this data is meant to be sent to the parent process
    366      // it will not be possible to intercept such request.
    367      supportsInterception: false,
    368      timings: this.timings,
    369    };
    370  }
    371 
    372  /**
    373   * Convert the provided request timing to a timing relative to the beginning
    374   * of the request. Note that https://w3c.github.io/resource-timing/#dfn-convert-fetch-timestamp
    375   * only expects high resolution timestamps (double in milliseconds) as inputs
    376   * of this method, but since platform timestamps are integers in microseconds,
    377   * they will be converted on the fly in this helper.
    378   *
    379   * @param {number} timing
    380   *     Platform TimeStamp for a request timing relative from the time origin
    381   *     in microseconds.
    382   * @param {number} requestTime
    383   *     Platform TimeStamp for the request start time relative from the time
    384   *     origin, in microseconds.
    385   *
    386   * @returns {number}
    387   *     High resolution timestamp (https://www.w3.org/TR/hr-time-3/#dom-domhighrestimestamp)
    388   *     for the request timing relative to the start time of the request, or 0
    389   *     if the provided timing was 0.
    390   */
    391  #convertTimestamp(timing, requestTime) {
    392    if (timing == 0) {
    393      return 0;
    394    }
    395 
    396    // Convert from platform timestamp to high resolution timestamp.
    397    return (timing - requestTime) / 1000;
    398  }
    399 
    400  #getContextId() {
    401    const id = lazy.NetworkUtils.getChannelBrowsingContextID(this.#channel);
    402    const browsingContext = BrowsingContext.get(id);
    403    return lazy.NavigableManager.getIdForBrowsingContext(browsingContext);
    404  }
    405 
    406  /**
    407   * Retrieve the Fetch timings for the NetworkRequest.
    408   *
    409   * @returns {object}
    410   *     Object with keys corresponding to fetch timing names, and their
    411   *     corresponding values.
    412   */
    413  #getFetchTimings() {
    414    const {
    415      asyncOpenTime,
    416      redirectStartTime,
    417      redirectEndTime,
    418      dispatchFetchEventStartTime,
    419      cacheReadStartTime,
    420      domainLookupStartTime,
    421      domainLookupEndTime,
    422      connectStartTime,
    423      connectEndTime,
    424      secureConnectionStartTime,
    425      requestStartTime,
    426      responseStartTime,
    427      responseEndTime,
    428    } = this.#timedChannel;
    429 
    430    // fetchStart should be the post-redirect start time, which should be the
    431    // first non-zero timing from: dispatchFetchEventStart, cacheReadStart and
    432    // domainLookupStart. See https://www.w3.org/TR/navigation-timing-2/#processing-model
    433    const fetchStartTime =
    434      dispatchFetchEventStartTime ||
    435      cacheReadStartTime ||
    436      domainLookupStartTime;
    437 
    438    // Bug 1805478: Per spec, the origin time should match Performance API's
    439    // timeOrigin for the global which initiated the request. This is not
    440    // available in the parent process, so for now we will use 0.
    441    const timeOrigin = 0;
    442 
    443    return {
    444      timeOrigin,
    445      requestTime: this.#convertTimestamp(asyncOpenTime, timeOrigin),
    446      redirectStart: this.#convertTimestamp(redirectStartTime, timeOrigin),
    447      redirectEnd: this.#convertTimestamp(redirectEndTime, timeOrigin),
    448      fetchStart: this.#convertTimestamp(fetchStartTime, timeOrigin),
    449      dnsStart: this.#convertTimestamp(domainLookupStartTime, timeOrigin),
    450      dnsEnd: this.#convertTimestamp(domainLookupEndTime, timeOrigin),
    451      connectStart: this.#convertTimestamp(connectStartTime, timeOrigin),
    452      connectEnd: this.#convertTimestamp(connectEndTime, timeOrigin),
    453      tlsStart: this.#convertTimestamp(secureConnectionStartTime, timeOrigin),
    454      tlsEnd: this.#convertTimestamp(connectEndTime, timeOrigin),
    455      requestStart: this.#convertTimestamp(requestStartTime, timeOrigin),
    456      responseStart: this.#convertTimestamp(responseStartTime, timeOrigin),
    457      responseEnd: this.#convertTimestamp(responseEndTime, timeOrigin),
    458    };
    459  }
    460 
    461  /**
    462   * Retrieve the list of headers for the NetworkRequest.
    463   *
    464   * @returns {Array.Array}
    465   *     Array of (name, value) tuples.
    466   */
    467  #getHeadersList() {
    468    const headers = [];
    469 
    470    if (this.#channel instanceof Ci.nsIHttpChannel) {
    471      this.#channel.visitRequestHeaders({
    472        visitHeader(name, value) {
    473          // The `Proxy-Authorization` header even though it appears on the channel is not
    474          // actually sent to the server for non CONNECT requests after the HTTP/HTTPS tunnel
    475          // is setup by the proxy.
    476          if (name == "Proxy-Authorization") {
    477            return;
    478          }
    479          headers.push([name, value]);
    480        },
    481      });
    482    }
    483 
    484    if (this.#channel instanceof Ci.nsIDataChannel) {
    485      // Data channels have no request headers.
    486      return [];
    487    }
    488 
    489    if (this.#channel instanceof Ci.nsIFileChannel) {
    490      // File channels have no request headers.
    491      return [];
    492    }
    493 
    494    return headers;
    495  }
    496 
    497  #getNavigationId() {
    498    if (!this.#channel.isDocument) {
    499      return null;
    500    }
    501 
    502    const browsingContext = lazy.NavigableManager.getBrowsingContextById(
    503      this.#contextId
    504    );
    505 
    506    let navigation =
    507      this.#navigationManager.getNavigationForBrowsingContext(browsingContext);
    508 
    509    // `onBeforeRequestSent` might be too early for the NavigationManager.
    510    // If there is no ongoing navigation, create one ourselves.
    511    // TODO: Bug 1835704 to detect navigations earlier and avoid this.
    512    if (!navigation || navigation.state !== lazy.NavigationState.Started) {
    513      navigation = lazy.notifyNavigationStarted({
    514        contextDetails: { context: browsingContext },
    515        url: this.serializedURL,
    516      });
    517    }
    518 
    519    return navigation ? navigation.navigationId : null;
    520  }
    521 
    522  #isTopLevelDocumentLoad() {
    523    if (!this.#channel.isDocument) {
    524      return false;
    525    }
    526 
    527    const browsingContext = lazy.NavigableManager.getBrowsingContextById(
    528      this.#contextId
    529    );
    530    return !browsingContext.parent;
    531  }
    532 
    533  #readPostDataFromRequestAsUTF8() {
    534    const postData = lazy.NetworkHelper.readPostDataFromRequest(
    535      this.#channel,
    536      "UTF-8"
    537    );
    538 
    539    if (postData === null || postData.data === null) {
    540      return null;
    541    }
    542 
    543    return {
    544      text: postData.isDecodedAsText ? postData.data : btoa(postData.data),
    545      isBase64: !postData.isDecodedAsText,
    546    };
    547  }
    548 
    549  #updatePostData() {
    550    const sentBody = this.#readPostDataFromRequestAsUTF8();
    551    if (sentBody) {
    552      this.#postData = sentBody;
    553      this.#postDataSize = sentBody.text.length;
    554    } else {
    555      this.#postData = null;
    556      this.#postDataSize = 0;
    557    }
    558  }
    559 }