tor-browser

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

NetworkUtils.sys.mjs (30227B)


      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 
      7 ChromeUtils.defineESModuleGetters(
      8  lazy,
      9  {
     10    NetworkHelper:
     11      "resource://devtools/shared/network-observer/NetworkHelper.sys.mjs",
     12    NetworkTimings:
     13      "resource://devtools/shared/network-observer/NetworkTimings.sys.mjs",
     14  },
     15  { global: "contextual" }
     16 );
     17 
     18 ChromeUtils.defineLazyGetter(lazy, "tpFlagsMask", () => {
     19  const trackingProtectionLevel2Enabled = Services.prefs
     20    .getStringPref("urlclassifier.trackingTable")
     21    .includes("content-track-digest256");
     22 
     23  return trackingProtectionLevel2Enabled
     24    ? ~Ci.nsIClassifiedChannel.CLASSIFIED_ANY_BASIC_TRACKING &
     25        ~Ci.nsIClassifiedChannel.CLASSIFIED_ANY_STRICT_TRACKING
     26    : ~Ci.nsIClassifiedChannel.CLASSIFIED_ANY_BASIC_TRACKING &
     27        Ci.nsIClassifiedChannel.CLASSIFIED_ANY_STRICT_TRACKING;
     28 });
     29 
     30 // List of compression encodings that can be handled by the
     31 // NetworkResponseListener.
     32 const ACCEPTED_COMPRESSION_ENCODINGS = [
     33  "gzip",
     34  "deflate",
     35  "br",
     36  "x-gzip",
     37  "x-deflate",
     38  "zstd",
     39 ];
     40 
     41 // These include types indicating the availability of data e.g responseCookies
     42 // or the networkEventOwner action which triggered the specific update e.g responseStart.
     43 // These types are specific to devtools and used by BiDi.
     44 const NETWORK_EVENT_TYPES = {
     45  CACHE_DETAILS: "cacheDetails",
     46  EARLY_HINT_RESPONSE_HEADERS: "earlyHintsResponseHeaders",
     47  EVENT_TIMINGS: "eventTimings",
     48  REQUEST_COOKIES: "requestCookies",
     49  REQUEST_HEADERS: "requestHeaders",
     50  REQUEST_POSTDATA: "requestPostData",
     51  RESPONSE_CACHE: "responseCache",
     52  RESPONSE_CONTENT: "responseContent",
     53  RESPONSE_CONTENT_COMPLETE: "responseContentComplete",
     54  RESPONSE_COOKIES: "responseCookies",
     55  RESPONSE_HEADERS: "responseHeaders",
     56  RESPONSE_START: "responseStart",
     57  SECURITY_INFO: "securityInfo",
     58  RESPONSE_END: "responseEnd",
     59 };
     60 
     61 /**
     62 * Convert a nsIContentPolicy constant to a display string
     63 */
     64 const LOAD_CAUSE_STRINGS = {
     65  [Ci.nsIContentPolicy.TYPE_INVALID]: "invalid",
     66  [Ci.nsIContentPolicy.TYPE_OTHER]: "other",
     67  [Ci.nsIContentPolicy.TYPE_SCRIPT]: "script",
     68  [Ci.nsIContentPolicy.TYPE_IMAGE]: "img",
     69  [Ci.nsIContentPolicy.TYPE_STYLESHEET]: "stylesheet",
     70  [Ci.nsIContentPolicy.TYPE_OBJECT]: "object",
     71  [Ci.nsIContentPolicy.TYPE_DOCUMENT]: "document",
     72  [Ci.nsIContentPolicy.TYPE_SUBDOCUMENT]: "subdocument",
     73  [Ci.nsIContentPolicy.TYPE_PING]: "ping",
     74  [Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST]: "xhr",
     75  [Ci.nsIContentPolicy.TYPE_DTD]: "dtd",
     76  [Ci.nsIContentPolicy.TYPE_FONT]: "font",
     77  [Ci.nsIContentPolicy.TYPE_MEDIA]: "media",
     78  [Ci.nsIContentPolicy.TYPE_WEBSOCKET]: "websocket",
     79  [Ci.nsIContentPolicy.TYPE_WEB_TRANSPORT]: "webtransport",
     80  [Ci.nsIContentPolicy.TYPE_CSP_REPORT]: "csp",
     81  [Ci.nsIContentPolicy.TYPE_XSLT]: "xslt",
     82  [Ci.nsIContentPolicy.TYPE_BEACON]: "beacon",
     83  [Ci.nsIContentPolicy.TYPE_FETCH]: "fetch",
     84  [Ci.nsIContentPolicy.TYPE_IMAGESET]: "imageset",
     85  [Ci.nsIContentPolicy.TYPE_WEB_MANIFEST]: "webManifest",
     86  [Ci.nsIContentPolicy.TYPE_WEB_IDENTITY]: "webidentity",
     87 };
     88 
     89 function causeTypeToString(causeType, loadFlags, internalContentPolicyType) {
     90  let prefix = "";
     91  if (
     92    (causeType == Ci.nsIContentPolicy.TYPE_IMAGESET ||
     93      internalContentPolicyType == Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE) &&
     94    loadFlags & Ci.nsIRequest.LOAD_BACKGROUND
     95  ) {
     96    prefix = "lazy-";
     97  }
     98 
     99  return prefix + LOAD_CAUSE_STRINGS[causeType] || "unknown";
    100 }
    101 
    102 function stringToCauseType(value) {
    103  return Object.keys(LOAD_CAUSE_STRINGS).find(
    104    key => LOAD_CAUSE_STRINGS[key] === value
    105  );
    106 }
    107 
    108 function isChannelFromSystemPrincipal(channel) {
    109  let principal;
    110 
    111  if (channel.isDocument) {
    112    // The loadingPrincipal is the principal where the request will be used.
    113    principal = channel.loadInfo.loadingPrincipal;
    114  } else {
    115    // The triggeringPrincipal is the principal of the resource which triggered
    116    // the request. Except for document loads, this is normally the best way
    117    // to know if a request is done on behalf of a chrome resource.
    118    // For instance if a chrome stylesheet loads a resource which is used in a
    119    // content page, the loadingPrincipal will be a content principal, but the
    120    // triggeringPrincipal will be the system principal.
    121    principal = channel.loadInfo.triggeringPrincipal;
    122  }
    123 
    124  return !!principal?.isSystemPrincipal;
    125 }
    126 
    127 function isChromeFileChannel(channel) {
    128  if (!(channel instanceof Ci.nsIFileChannel)) {
    129    return false;
    130  }
    131 
    132  return (
    133    channel.originalURI.spec.startsWith("chrome://") ||
    134    channel.originalURI.spec.startsWith("resource://")
    135  );
    136 }
    137 
    138 function isPrivilegedChannel(channel) {
    139  return (
    140    isChannelFromSystemPrincipal(channel) ||
    141    isChromeFileChannel(channel) ||
    142    channel.loadInfo.isInDevToolsContext
    143  );
    144 }
    145 
    146 /**
    147 * Get the browsing context id for the channel.
    148 *
    149 * @param {*} channel
    150 * @returns {number}
    151 */
    152 function getChannelBrowsingContextID(channel) {
    153  // `frameBrowsingContextID` is non-0 if the channel is loading an iframe.
    154  // If available, use it instead of `browsingContextID` which is exceptionally
    155  // set to the parent's BrowsingContext id for such channels.
    156  if (channel.loadInfo.frameBrowsingContextID) {
    157    return channel.loadInfo.frameBrowsingContextID;
    158  }
    159 
    160  if (channel.loadInfo.browsingContextID) {
    161    return channel.loadInfo.browsingContextID;
    162  }
    163 
    164  if (channel.loadInfo.workerAssociatedBrowsingContextID) {
    165    return channel.loadInfo.workerAssociatedBrowsingContextID;
    166  }
    167 
    168  // At least WebSocket channel aren't having a browsingContextID set on their loadInfo
    169  // We fallback on top frame element, which works, but will be wrong for WebSocket
    170  // in same-process iframes...
    171  const topFrame = lazy.NetworkHelper.getTopFrameForRequest(channel);
    172  // topFrame is typically null for some chrome requests like favicons
    173  if (topFrame && topFrame.browsingContext) {
    174    return topFrame.browsingContext.id;
    175  }
    176  return null;
    177 }
    178 
    179 /**
    180 * Get the innerWindowId for the channel.
    181 *
    182 * @param {*} channel
    183 * @returns {number}
    184 */
    185 function getChannelInnerWindowId(channel) {
    186  if (channel.loadInfo.innerWindowID) {
    187    return channel.loadInfo.innerWindowID;
    188  }
    189  // At least WebSocket channel aren't having a browsingContextID set on their loadInfo
    190  // We fallback on top frame element, which works, but will be wrong for WebSocket
    191  // in same-process iframes...
    192  const topFrame = lazy.NetworkHelper.getTopFrameForRequest(channel);
    193  // topFrame is typically null for some chrome requests like favicons
    194  if (topFrame?.browsingContext?.currentWindowGlobal) {
    195    return topFrame.browsingContext.currentWindowGlobal.innerWindowId;
    196  }
    197  return null;
    198 }
    199 
    200 /**
    201 * Does this channel represent a Preload request.
    202 *
    203 * @param {*} channel
    204 * @returns {boolean}
    205 */
    206 function isPreloadRequest(channel) {
    207  const type = channel.loadInfo.internalContentPolicyType;
    208  return (
    209    type == Ci.nsIContentPolicy.TYPE_INTERNAL_SCRIPT_PRELOAD ||
    210    type == Ci.nsIContentPolicy.TYPE_INTERNAL_MODULE_PRELOAD ||
    211    type == Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_PRELOAD ||
    212    type == Ci.nsIContentPolicy.TYPE_INTERNAL_STYLESHEET_PRELOAD ||
    213    type == Ci.nsIContentPolicy.TYPE_INTERNAL_FONT_PRELOAD ||
    214    type == Ci.nsIContentPolicy.TYPE_INTERNAL_JSON_PRELOAD
    215  );
    216 }
    217 
    218 /**
    219 * Get the channel cause details.
    220 *
    221 * @param {nsIChannel} channel
    222 * @returns {object}
    223 *          - loadingDocumentUri {string} uri of the document which created the
    224 *            channel
    225 *          - type {string} cause type as string
    226 */
    227 function getCauseDetails(channel) {
    228  // Determine the cause and if this is an XHR request.
    229  let causeType = Ci.nsIContentPolicy.TYPE_OTHER;
    230  let causeUri = null;
    231 
    232  if (channel.loadInfo) {
    233    causeType = channel.loadInfo.externalContentPolicyType;
    234    const { loadingPrincipal } = channel.loadInfo;
    235    if (loadingPrincipal) {
    236      causeUri = loadingPrincipal.spec;
    237    }
    238  }
    239 
    240  return {
    241    loadingDocumentUri: causeUri,
    242    type: causeTypeToString(
    243      causeType,
    244      channel.loadFlags,
    245      channel.loadInfo.internalContentPolicyType
    246    ),
    247  };
    248 }
    249 
    250 /**
    251 * Get the channel priority. Priority is a number which typically ranges from
    252 * -20 (lowest priority) to 20 (highest priority). Can be null if the channel
    253 * does not implement nsISupportsPriority.
    254 *
    255 * @param {nsIChannel} channel
    256 * @returns {number|undefined}
    257 */
    258 function getChannelPriority(channel) {
    259  if (channel instanceof Ci.nsISupportsPriority) {
    260    return channel.priority;
    261  }
    262 
    263  return null;
    264 }
    265 
    266 /**
    267 * Get the channel HTTP version as an uppercase string starting with "HTTP/"
    268 * (eg "HTTP/2").
    269 *
    270 * @param {nsIChannel} channel
    271 * @returns {string}
    272 */
    273 function getHttpVersion(channel) {
    274  if (!(channel instanceof Ci.nsIHttpChannelInternal)) {
    275    return null;
    276  }
    277 
    278  // Determine the HTTP version.
    279  const httpVersionMaj = {};
    280  const httpVersionMin = {};
    281 
    282  channel.QueryInterface(Ci.nsIHttpChannelInternal);
    283  channel.getResponseVersion(httpVersionMaj, httpVersionMin);
    284 
    285  // The official name HTTP version 2.0 and 3.0 are HTTP/2 and HTTP/3, omit the
    286  // trailing `.0`.
    287  if (httpVersionMin.value == 0) {
    288    return "HTTP/" + httpVersionMaj.value;
    289  }
    290 
    291  return "HTTP/" + httpVersionMaj.value + "." + httpVersionMin.value;
    292 }
    293 
    294 const UNKNOWN_PROTOCOL_STRINGS = ["", "unknown"];
    295 const HTTP_PROTOCOL_STRINGS = ["http", "https"];
    296 /**
    297 * Get the protocol for the provided httpActivity. Either the ALPN negotiated
    298 * protocol or as a fallback a protocol computed from the scheme and the
    299 * response status.
    300 *
    301 * TODO: The `protocol` is similar to another response property called
    302 * `httpVersion`. `httpVersion` is uppercase and purely computed from the
    303 * response status, whereas `protocol` uses nsIHttpChannel.protocolVersion by
    304 * default and otherwise falls back on `httpVersion`. Ideally we should merge
    305 * the two properties.
    306 *
    307 * @param {object} httpActivity
    308 *     The httpActivity object for which we need to get the protocol.
    309 *
    310 * @returns {string}
    311 *     The protocol as a string.
    312 */
    313 function getProtocol(channel) {
    314  let protocol = "";
    315  try {
    316    const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
    317    // protocolVersion corresponds to ALPN negotiated protocol.
    318    protocol = httpChannel.protocolVersion;
    319  } catch (e) {
    320    // Ignore errors reading protocolVersion.
    321  }
    322 
    323  if (UNKNOWN_PROTOCOL_STRINGS.includes(protocol)) {
    324    protocol = channel.URI.scheme;
    325    const httpVersion = getHttpVersion(channel);
    326    if (
    327      typeof httpVersion == "string" &&
    328      HTTP_PROTOCOL_STRINGS.includes(protocol)
    329    ) {
    330      protocol = httpVersion.toLowerCase();
    331    }
    332  }
    333 
    334  return protocol;
    335 }
    336 
    337 /**
    338 * Get the channel referrer policy as a string
    339 * (eg "strict-origin-when-cross-origin").
    340 *
    341 * @param {nsIChannel} channel
    342 * @returns {string}
    343 */
    344 function getReferrerPolicy(channel) {
    345  return channel.referrerInfo
    346    ? channel.referrerInfo.getReferrerPolicyString()
    347    : "";
    348 }
    349 
    350 /**
    351 * Check if the channel is private.
    352 *
    353 * @param {nsIChannel} channel
    354 * @returns {boolean}
    355 */
    356 function isChannelPrivate(channel) {
    357  channel.QueryInterface(Ci.nsIPrivateBrowsingChannel);
    358  return channel.isChannelPrivate;
    359 }
    360 
    361 /**
    362 * Check if the channel data is loaded from the cache or not.
    363 *
    364 * @param {nsIChannel} channel
    365 *     The channel for which we need to check the cache status.
    366 *
    367 * @returns {boolean}
    368 *     True if the channel data is loaded from the cache, false otherwise.
    369 */
    370 function isFromCache(channel) {
    371  if (channel instanceof Ci.nsICacheInfoChannel) {
    372    return channel.isFromCache();
    373  }
    374 
    375  return false;
    376 }
    377 
    378 /**
    379 * isNavigationRequest is true for the one request used to load a new top level
    380 * document of a given tab, or top level window. It will typically be false for
    381 * navigation requests of iframes, i.e. the request loading another document in
    382 * an iframe.
    383 *
    384 * @param {nsIChannel} channel
    385 * @return {boolean}
    386 */
    387 function isNavigationRequest(channel) {
    388  return channel.isMainDocumentChannel && channel.loadInfo.isTopLevelLoad;
    389 }
    390 
    391 /**
    392 * Returns true  if the channel has been processed by URL-Classifier features
    393 * and is considered third-party with the top window URI, and if it has loaded
    394 * a resource that is classified as a tracker.
    395 *
    396 * @param {nsIChannel} channel
    397 * @return {boolean}
    398 */
    399 function isThirdPartyTrackingResource(channel) {
    400  // Only consider channels classified as level-1 to be trackers if our preferences
    401  // would not cause such channels to be blocked in strict content blocking mode.
    402  // Make sure the value produced here is a boolean.
    403  return !!(
    404    channel instanceof Ci.nsIClassifiedChannel &&
    405    channel.isThirdPartyTrackingResource() &&
    406    (channel.thirdPartyClassificationFlags & lazy.tpFlagsMask) == 0
    407  );
    408 }
    409 
    410 /**
    411 * Retrieve the websocket channel for the provided channel, if available.
    412 * Returns null otherwise.
    413 *
    414 * @param {nsIChannel} channel
    415 * @returns {nsIWebSocketChannel|null}
    416 */
    417 function getWebSocketChannel(channel) {
    418  let wsChannel = null;
    419  if (channel.notificationCallbacks) {
    420    try {
    421      wsChannel = channel.notificationCallbacks.QueryInterface(
    422        Ci.nsIWebSocketChannel
    423      );
    424    } catch (e) {
    425      // Not all channels implement nsIWebSocketChannel.
    426    }
    427  }
    428  return wsChannel;
    429 }
    430 
    431 /**
    432 * For a given channel, fetch the request's headers and cookies.
    433 *
    434 * @param {nsIChannel} channel
    435 * @return {object}
    436 *     An object with two properties:
    437 *     @property {Array<object>} cookies
    438 *         Array of { name, value } objects.
    439 *     @property {Array<object>} headers
    440 *         Array of { name, value } objects.
    441 */
    442 function fetchRequestHeadersAndCookies(channel) {
    443  const headers = [];
    444  let cookies = [];
    445  let cookieHeader = null;
    446 
    447  // Copy the request header data.
    448  channel.visitRequestHeaders({
    449    visitHeader(name, value) {
    450      // The `Proxy-Authorization` header even though it appears on the channel is not
    451      // actually sent to the server for non CONNECT requests after the HTTP/HTTPS tunnel
    452      // is setup by the proxy.
    453      if (name == "Proxy-Authorization") {
    454        return;
    455      }
    456      if (name == "Cookie") {
    457        cookieHeader = value;
    458      }
    459      headers.push({ name, value });
    460    },
    461  });
    462 
    463  if (cookieHeader) {
    464    cookies = lazy.NetworkHelper.parseCookieHeader(cookieHeader);
    465  }
    466 
    467  return { cookies, headers };
    468 }
    469 
    470 /**
    471 * Parse the early hint raw headers string to an
    472 * array of name/value object header pairs
    473 *
    474 * @param {string} rawHeaders
    475 * @returns {Array}
    476 */
    477 function parseEarlyHintsResponseHeaders(rawHeaders) {
    478  const headers = rawHeaders.split("\r\n");
    479  // Remove the line with the HTTP version and the status
    480  headers.shift();
    481  return headers
    482    .map(header => {
    483      const [name, value] = header.split(":");
    484      return { name, value };
    485    })
    486    .filter(header => header.name.length);
    487 }
    488 
    489 /**
    490 * For a given channel, fetch the response's headers and cookies.
    491 *
    492 * @param {nsIChannel} channel
    493 * @return {object}
    494 *     An object with two properties:
    495 *     @property {Array<object>} cookies
    496 *         Array of { name, value } objects.
    497 *     @property {Array<object>} headers
    498 *         Array of { name, value } objects.
    499 */
    500 function fetchResponseHeadersAndCookies(channel) {
    501  // Read response headers and cookies.
    502  const headers = [];
    503  const setCookieHeaders = [];
    504 
    505  const SET_COOKIE_REGEXP = /set-cookie/i;
    506  channel.visitOriginalResponseHeaders({
    507    visitHeader(name, value) {
    508      if (SET_COOKIE_REGEXP.test(name)) {
    509        setCookieHeaders.push(value);
    510      }
    511      headers.push({ name, value });
    512    },
    513  });
    514 
    515  return {
    516    cookies: lazy.NetworkHelper.parseSetCookieHeaders(setCookieHeaders),
    517    headers,
    518  };
    519 }
    520 
    521 /**
    522 * Check if a given network request should be logged by a network monitor
    523 * based on the specified filters.
    524 *
    525 * @param {(nsIHttpChannel|nsIFileChannel)} channel
    526 *        Request to check.
    527 * @param filters
    528 *        NetworkObserver filters to match against. An object with one of the following attributes:
    529 *        - sessionContext: When inspecting requests from the parent process, pass the WatcherActor's session context.
    530 *          This helps know what is the overall debugged scope.
    531 *          See watcher actor constructor for more info.
    532 *        - targetActor: When inspecting requests from the content process, pass the WindowGlobalTargetActor.
    533 *          This helps know what exact subset of request we should accept.
    534 *          This is especially useful to behave correctly regarding EFT, where we should include or not
    535 *          iframes requests.
    536 *        - browserId, addonId, window: All these attributes are legacy.
    537 *          Only browserId attribute is still used by the legacy WebConsoleActor startListener API.
    538 * @return boolean
    539 *         True if the network request should be logged, false otherwise.
    540 */
    541 function matchRequest(channel, filters) {
    542  // NetworkEventWatcher should now pass a session context for the parent process codepath
    543  if (filters.sessionContext) {
    544    const { type } = filters.sessionContext;
    545    if (type == "all") {
    546      return true;
    547    }
    548 
    549    // Ignore requests from chrome or add-on code when we don't monitor the whole browser
    550    if (
    551      channel.loadInfo?.loadingDocument === null &&
    552      isPrivilegedChannel(channel)
    553    ) {
    554      return false;
    555    }
    556 
    557    // When a page fails loading in top level or in iframe, an error page is shown
    558    // which will trigger a request to about:neterror (which is translated into a file:// URI request).
    559    // Ignore this request in regular toolbox (but not in the browser toolbox).
    560    if (channel.loadInfo?.loadErrorPage) {
    561      return false;
    562    }
    563 
    564    if (type == "browser-element") {
    565      if (!channel.loadInfo.browsingContext) {
    566        const topFrame = lazy.NetworkHelper.getTopFrameForRequest(channel);
    567        // `topFrame` is typically null for some chrome requests like favicons
    568        // And its `browsingContext` attribute might be null if the request happened
    569        // while the tab is being closed.
    570        return (
    571          topFrame?.browsingContext?.browserId ==
    572          filters.sessionContext.browserId
    573        );
    574      }
    575      return (
    576        channel.loadInfo.browsingContext.browserId ==
    577        filters.sessionContext.browserId
    578      );
    579    }
    580    if (type == "webextension") {
    581      return (
    582        channel.loadInfo?.loadingPrincipal?.addonId ===
    583        filters.sessionContext.addonId
    584      );
    585    }
    586    throw new Error("Unsupported session context type: " + type);
    587  }
    588 
    589  // NetworkEventContentWatcher and NetworkEventStackTraces pass a target actor instead, from the content processes
    590  // Because of EFT, we can't use session context as we have to know what exact windows the target actor covers.
    591  if (filters.targetActor) {
    592    // Ignore requests from chrome or add-on code when we don't monitor the whole browser
    593    if (
    594      filters.targetActor.sessionContext?.type !== "all" &&
    595      isPrivilegedChannel(channel)
    596    ) {
    597      return false;
    598    }
    599 
    600    // Bug 1769982 the target actor might be destroying and accessing windows will throw.
    601    // Ignore all further request when this happens.
    602    let windows;
    603    try {
    604      windows = filters.targetActor.windows;
    605    } catch (e) {
    606      return false;
    607    }
    608    const win = lazy.NetworkHelper.getWindowForRequest(channel);
    609    return windows.includes(win);
    610  }
    611 
    612  throw new Error(
    613    "matchRequest expects either a 'targetActor' or a 'sessionContext' attribute"
    614  );
    615 }
    616 
    617 function getBlockedReason(channel, fromCache = false) {
    618  let blockedReason;
    619  const extension = {};
    620  const { status } = channel;
    621 
    622  try {
    623    const request = channel.QueryInterface(Ci.nsIHttpChannel);
    624    const properties = request.QueryInterface(Ci.nsIPropertyBag);
    625 
    626    blockedReason = request.loadInfo.requestBlockingReason;
    627    extension.blocking = properties.getProperty("cancelledByExtension");
    628 
    629    // WebExtensionPolicy is not available for workers
    630    if (typeof WebExtensionPolicy !== "undefined") {
    631      extension.blocking = WebExtensionPolicy.getByID(extension.blocking).name;
    632    }
    633  } catch (err) {
    634    // "cancelledByExtension" doesn't have to be available.
    635  }
    636 
    637  if (
    638    blockedReason === Ci.nsILoadInfo.BLOCKING_REASON_CLASSIFY_HARMFULADDON_URI
    639  ) {
    640    try {
    641      const properties = channel.QueryInterface(Ci.nsIPropertyBag);
    642      extension.blocked = properties.getProperty("blockedExtension");
    643    } catch (err) {
    644      // "blockedExtension" doesn't have to be available.
    645    }
    646  }
    647 
    648  // These are platform errors which are not exposed to the users,
    649  // usually the requests (with these errors) might be displayed with various
    650  // other status codes.
    651  const ignoreList = [
    652    // These are emited when the request is already in the cache.
    653    "NS_ERROR_PARSED_DATA_CACHED",
    654    // This is emited when there is some issues around images e.g When the img.src
    655    // links to a non existent url. This is typically shown as a 404 request.
    656    "NS_IMAGELIB_ERROR_FAILURE",
    657    // This is emited when there is a redirect. They are shown as 301 requests.
    658    "NS_BINDING_REDIRECTED",
    659    // E.g Emited by send beacon requests.
    660    "NS_ERROR_ABORT",
    661    // This is emmited when browser.http.blank_page_with_error_response.enabled
    662    // is set to false, and a 404 or 500 request has no content.
    663    // They are shown as 404 or 500 requests.
    664    "NS_ERROR_NET_EMPTY_RESPONSE",
    665  ];
    666 
    667  // NS_BINDING_ABORTED are emmited when request are abruptly halted, these are valid and should not be ignored.
    668  // They can also be emmited for requests already cache which have the `cached` status, these should be ignored.
    669  if (fromCache) {
    670    ignoreList.push("NS_BINDING_ABORTED");
    671  }
    672 
    673  // If the request has not failed or is not blocked by a web extension, check for
    674  // any errors not on the ignore list. e.g When a host is not found (NS_ERROR_UNKNOWN_HOST).
    675  if (
    676    blockedReason == 0 &&
    677    !Components.isSuccessCode(status) &&
    678    !ignoreList.includes(ChromeUtils.getXPCOMErrorName(status))
    679  ) {
    680    blockedReason = ChromeUtils.getXPCOMErrorName(status);
    681  }
    682 
    683  return { extension, blockedReason };
    684 }
    685 
    686 function getCharset(channel) {
    687  const win = lazy.NetworkHelper.getWindowForRequest(channel);
    688  return win ? win.document.characterSet : null;
    689 }
    690 
    691 /**
    692 * Data channels are either handled in the parent process NetworkObserver for
    693 * navigation requests, or in content processes for any other request.
    694 *
    695 * This function allows to apply the same logic to build the network event actor
    696 * in both cases.
    697 *
    698 * @param {nsIDataChannel} channel
    699 *     The data channel for which we are creating a network event actor.
    700 * @param {object} networkEventActor
    701 *     The network event actor owning this resource.
    702 */
    703 function handleDataChannel(channel, networkEventActor) {
    704  networkEventActor.addResponseStart({
    705    channel,
    706    fromCache: false,
    707    // According to the fetch spec for data URLs we can just hardcode
    708    // "Content-Type" header.
    709    rawHeaders: "content-type: " + channel.contentType,
    710  });
    711 
    712  // For data URLs we can not set up a stream listener as for http,
    713  // so we have to create a response manually and complete it.
    714  const response = {
    715    // TODO: Bug 1903807. Re-evaluate if it's correct to just return
    716    // zero for `bodySize` and `decodedBodySize`.
    717    bodySize: 0,
    718    decodedBodySize: 0,
    719    contentCharset: channel.contentCharset,
    720    contentLength: channel.contentLength,
    721    contentType: channel.contentType,
    722    mimeType: lazy.NetworkHelper.addCharsetToMimeType(
    723      channel.contentType,
    724      channel.contentCharset
    725    ),
    726    transferredSize: 0,
    727  };
    728 
    729  // For data URIs all timings can be set to zero.
    730  const result = lazy.NetworkTimings.getEmptyHARTimings();
    731  networkEventActor.addEventTimings(
    732    result.total,
    733    result.timings,
    734    result.offsets
    735  );
    736 
    737  const url = channel.URI.spec;
    738  response.text = url.substring(url.indexOf(",") + 1);
    739  if (
    740    !response.mimeType ||
    741    !lazy.NetworkHelper.isTextMimeType(response.mimeType)
    742  ) {
    743    response.encoding = "base64";
    744  }
    745 
    746  // Note: `size`` is only used by DevTools, WebDriverBiDi relies on
    747  // `bodySize` and `decodedBodySize`. Waiting on Bug 1903807 to decide
    748  // if those fields should have non-0 values as well.
    749  response.size = response.text.length;
    750 
    751  // Security information is not relevant for data channel, but it should
    752  // not be considered as insecure either. Set empty string as security
    753  // state.
    754  networkEventActor.addSecurityInfo({ state: "" });
    755  networkEventActor.addResponseContent(response);
    756  networkEventActor.addResponseContentComplete({});
    757 }
    758 
    759 /**
    760 * Sets a flag on the resource to specify that the data for a network event
    761 * is available. The flag is used by the consumer of the resource (frontend)
    762 * to determine when to lazily fetch the data.
    763 *
    764 * @param {object} resource - This could be a network resource object or a network resource
    765 *                            updates object.
    766 * @param {Array} networkEvents
    767 */
    768 function setEventAsAvailable(resource, networkEvents) {
    769  for (const event of networkEvents) {
    770    if (!Object.values(NETWORK_EVENT_TYPES).includes(event)) {
    771      console.warn(`${event} is not a valid network event type.`);
    772      return;
    773    }
    774    resource[`${event}Available`] = true;
    775  }
    776 }
    777 
    778 /**
    779 * Helper to decode the content of a response object built by a
    780 * NetworkResponseListener.
    781 *
    782 * @param {Array<TypedArray>} chunks
    783 *     Array of response chunks read via NetUtil.readInputStream.
    784 * @param {object} options
    785 * @param {string} options.charset
    786 *     Charset used for the response.
    787 * @param {Array<string>} options.compressionEncodings
    788 *     Array of compression encodings applied to the response.
    789 * @param {number} encodedBodySize
    790 *     The total size of the encoded response.
    791 * @param {string} encoding
    792 *     The "encoding" of the response as computed by NetworkResponseListener.
    793 *     (can be either undefined or "base64" if the mime type is not text)
    794 * @returns {string}
    795 *     The decoded content, as a string.
    796 */
    797 async function decodeResponseChunks(chunks, options) {
    798  const charset = options.charset || null;
    799  const { compressionEncodings = [], encodedBodySize, encoding } = options;
    800 
    801  const bytes = new Uint8Array(encodedBodySize);
    802  let offset = 0;
    803  for (const chunk of chunks) {
    804    bytes.set(new Uint8Array(chunk), offset);
    805    offset += chunk.byteLength;
    806  }
    807 
    808  const ArrayBufferInputStream = Components.Constructor(
    809    "@mozilla.org/io/arraybuffer-input-stream;1",
    810    "nsIArrayBufferInputStream",
    811    "setData"
    812  );
    813  const bodyStream = new ArrayBufferInputStream(
    814    bytes.buffer,
    815    0,
    816    bytes.byteLength
    817  );
    818 
    819  let decodedContent;
    820  if (compressionEncodings.length) {
    821    decodedContent = await decodeCompressedStream(
    822      bodyStream,
    823      bytes.byteLength,
    824      compressionEncodings,
    825      charset
    826    );
    827  } else {
    828    decodedContent = decodeUncompressedStream(
    829      bodyStream,
    830      bytes.byteLength,
    831      charset
    832    );
    833  }
    834 
    835  decodedContent = lazy.NetworkHelper.convertToUnicode(decodedContent, charset);
    836  if (encoding === "base64") {
    837    try {
    838      decodedContent = btoa(decodedContent);
    839    } catch {
    840      // Ignore `btoa`` errors because encoding="base64" does not guarantee the
    841      // content is actually base64 (loosely based on the mime type not being
    842      // a text mime type).
    843    }
    844  }
    845 
    846  return decodedContent;
    847 }
    848 
    849 function decodeUncompressedStream(stream, length) {
    850  const sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
    851    Ci.nsIScriptableInputStream
    852  );
    853  sis.init(stream);
    854  return sis.readBytes(length);
    855 }
    856 
    857 async function decodeCompressedStream(stream, length, encodings) {
    858  const listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance(
    859    Ci.nsIStreamLoader
    860  );
    861  const onDecodingComplete = new Promise(resolve => {
    862    listener.init({
    863      onStreamComplete: function onStreamComplete(
    864        _loader,
    865        _context,
    866        _status,
    867        _length,
    868        data
    869      ) {
    870        // `data`` might be a very large array, chunk calls to fromCharCode to
    871        // avoid "RangeError: too many arguments provided for a function call".
    872        const CHUNK_SIZE = 65536;
    873        let result = "";
    874        for (let i = 0; i < data.length; i += CHUNK_SIZE) {
    875          const chunk = data.slice(i, i + CHUNK_SIZE);
    876          result += String.fromCharCode.apply(null, chunk);
    877        }
    878        resolve(result);
    879      },
    880    });
    881  });
    882 
    883  const scs = Cc["@mozilla.org/streamConverters;1"].getService(
    884    Ci.nsIStreamConverterService
    885  );
    886 
    887  let converter;
    888  let nextListener = listener;
    889  for (const encoding of encodings) {
    890    // There can be multiple compressions applied
    891    converter = scs.asyncConvertData(
    892      encoding,
    893      "uncompressed",
    894      nextListener,
    895      null
    896    );
    897    nextListener = converter;
    898  }
    899 
    900  converter.onStartRequest(null, null);
    901  converter.onDataAvailable(null, stream, 0, length);
    902  converter.onStopRequest(null, null, null);
    903 
    904  return onDecodingComplete;
    905 }
    906 
    907 /**
    908 * Remove any frames in a stack that are related to chrome resource files.
    909 *
    910 * @param array stack
    911 *        An array of frames, each of which has a
    912 *        'filename' property.
    913 * @return array
    914 *         An array of stack frames with any chrome frames removed.
    915 *         The original array is not modified.
    916 */
    917 function removeChromeFrames(stacktrace) {
    918  return stacktrace.filter(({ filename }) => {
    919    return (
    920      filename &&
    921      !filename.startsWith("resource://") &&
    922      !filename.startsWith("chrome://")
    923    );
    924  });
    925 }
    926 
    927 export const NetworkUtils = {
    928  ACCEPTED_COMPRESSION_ENCODINGS,
    929  causeTypeToString,
    930  decodeResponseChunks,
    931  fetchRequestHeadersAndCookies,
    932  fetchResponseHeadersAndCookies,
    933  getBlockedReason,
    934  getCauseDetails,
    935  getChannelBrowsingContextID,
    936  getChannelInnerWindowId,
    937  getChannelPriority,
    938  getCharset,
    939  getHttpVersion,
    940  getProtocol,
    941  getReferrerPolicy,
    942  getWebSocketChannel,
    943  handleDataChannel,
    944  isChannelFromSystemPrincipal,
    945  isChannelPrivate,
    946  isFromCache,
    947  isNavigationRequest,
    948  isPreloadRequest,
    949  isThirdPartyTrackingResource,
    950  matchRequest,
    951  NETWORK_EVENT_TYPES,
    952  parseEarlyHintsResponseHeaders,
    953  removeChromeFrames,
    954  setEventAsAvailable,
    955  stringToCauseType,
    956 };