/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const lazy = {};

ChromeUtils.defineESModuleGetters(
  lazy,
  {
    NetworkHelper:
      "resource://devtools/shared/network-observer/NetworkHelper.sys.mjs",
    NetworkTimings:
      "resource://devtools/shared/network-observer/NetworkTimings.sys.mjs",
  },
  { global: "contextual" }
);

ChromeUtils.defineLazyGetter(lazy, "tpFlagsMask", () => {
  const trackingProtectionLevel2Enabled = Services.prefs
    .getStringPref("urlclassifier.trackingTable")
    .includes("content-track-digest256");

  return trackingProtectionLevel2Enabled
    ? ~Ci.nsIClassifiedChannel.CLASSIFIED_ANY_BASIC_TRACKING &
        ~Ci.nsIClassifiedChannel.CLASSIFIED_ANY_STRICT_TRACKING
    : ~Ci.nsIClassifiedChannel.CLASSIFIED_ANY_BASIC_TRACKING &
        Ci.nsIClassifiedChannel.CLASSIFIED_ANY_STRICT_TRACKING;
});

// List of compression encodings that can be handled by the
// NetworkResponseListener.
const ACCEPTED_COMPRESSION_ENCODINGS = [
  "gzip",
  "deflate",
  "br",
  "x-gzip",
  "x-deflate",
  "zstd",
];

// These include types indicating the availability of data e.g responseCookies
// or the networkEventOwner action which triggered the specific update e.g responseStart.
// These types are specific to devtools and used by BiDi.
const NETWORK_EVENT_TYPES = {
  CACHE_DETAILS: "cacheDetails",
  EARLY_HINT_RESPONSE_HEADERS: "earlyHintsResponseHeaders",
  EVENT_TIMINGS: "eventTimings",
  REQUEST_COOKIES: "requestCookies",
  REQUEST_HEADERS: "requestHeaders",
  REQUEST_POSTDATA: "requestPostData",
  RESPONSE_CACHE: "responseCache",
  RESPONSE_CONTENT: "responseContent",
  RESPONSE_CONTENT_COMPLETE: "responseContentComplete",
  RESPONSE_COOKIES: "responseCookies",
  RESPONSE_HEADERS: "responseHeaders",
  RESPONSE_START: "responseStart",
  SECURITY_INFO: "securityInfo",
  RESPONSE_END: "responseEnd",
};

/**
 * Convert a nsIContentPolicy constant to a display string
 */
const LOAD_CAUSE_STRINGS = {
  [Ci.nsIContentPolicy.TYPE_INVALID]: "invalid",
  [Ci.nsIContentPolicy.TYPE_OTHER]: "other",
  [Ci.nsIContentPolicy.TYPE_SCRIPT]: "script",
  [Ci.nsIContentPolicy.TYPE_IMAGE]: "img",
  [Ci.nsIContentPolicy.TYPE_STYLESHEET]: "stylesheet",
  [Ci.nsIContentPolicy.TYPE_OBJECT]: "object",
  [Ci.nsIContentPolicy.TYPE_DOCUMENT]: "document",
  [Ci.nsIContentPolicy.TYPE_SUBDOCUMENT]: "subdocument",
  [Ci.nsIContentPolicy.TYPE_PING]: "ping",
  [Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST]: "xhr",
  [Ci.nsIContentPolicy.TYPE_DTD]: "dtd",
  [Ci.nsIContentPolicy.TYPE_FONT]: "font",
  [Ci.nsIContentPolicy.TYPE_MEDIA]: "media",
  [Ci.nsIContentPolicy.TYPE_WEBSOCKET]: "websocket",
  [Ci.nsIContentPolicy.TYPE_WEB_TRANSPORT]: "webtransport",
  [Ci.nsIContentPolicy.TYPE_CSP_REPORT]: "csp",
  [Ci.nsIContentPolicy.TYPE_XSLT]: "xslt",
  [Ci.nsIContentPolicy.TYPE_BEACON]: "beacon",
  [Ci.nsIContentPolicy.TYPE_FETCH]: "fetch",
  [Ci.nsIContentPolicy.TYPE_IMAGESET]: "imageset",
  [Ci.nsIContentPolicy.TYPE_WEB_MANIFEST]: "webManifest",
  [Ci.nsIContentPolicy.TYPE_WEB_IDENTITY]: "webidentity",
};

const REDIRECT_CODES = [
  301, // HTTP Moved Permanently
  302, // HTTP Found
  303, // HTTP See Other
  307, // HTTP Temporary Redirect
  308, // HTTP Moved Permanently
];

function getChannelCauseType(channel) {
  if (channel.loadInfo.isInDevToolsContext) {
    return "devtools";
  }

  const { externalContentPolicyType, internalContentPolicyType } =
    channel.loadInfo;

  let prefix = "";
  if (
    (externalContentPolicyType == Ci.nsIContentPolicy.TYPE_IMAGESET ||
      internalContentPolicyType == Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE) &&
    channel.loadFlags & Ci.nsIRequest.LOAD_BACKGROUND
  ) {
    prefix = "lazy-";
  }

  return prefix + LOAD_CAUSE_STRINGS[externalContentPolicyType] || "unknown";
}

function stringToCauseType(value) {
  return Object.keys(LOAD_CAUSE_STRINGS).find(
    key => LOAD_CAUSE_STRINGS[key] === value
  );
}

function isChannelFromSystemPrincipal(channel) {
  let principal;
  const channelURI = channel.originalURI || channel.URI;
  if (channelURI?.spec.startsWith("view-source:") || channel.isDocument) {
    // The loadingPrincipal is the principal where the request will be used.
    principal = channel.loadInfo.loadingPrincipal;
  } else {
    // The triggeringPrincipal is the principal of the resource which triggered
    // the request. Except for document loads, this is normally the best way
    // to know if a request is done on behalf of a chrome resource.
    // For instance if a chrome stylesheet loads a resource which is used in a
    // content page, the loadingPrincipal will be a content principal, but the
    // triggeringPrincipal will be the system principal.
    principal = channel.loadInfo.triggeringPrincipal;
  }

  return !!principal?.isSystemPrincipal;
}

function isChromeFileChannel(channel) {
  if (!(channel instanceof Ci.nsIFileChannel)) {
    return false;
  }

  return (
    channel.originalURI.spec.startsWith("chrome://") ||
    channel.originalURI.spec.startsWith("resource://")
  );
}

function isPrivilegedChannel(channel) {
  return isChannelFromSystemPrincipal(channel) || isChromeFileChannel(channel);
}

/**
 * Get the browsing context id for the channel.
 *
 * @param {*} channel
 * @returns {number}
 */
function getChannelBrowsingContextID(channel) {
  // `frameBrowsingContextID` is non-0 if the channel is loading an iframe.
  // If available, use it instead of `browsingContextID` which is exceptionally
  // set to the parent's BrowsingContext id for such channels.
  if (channel.loadInfo.frameBrowsingContextID) {
    return channel.loadInfo.frameBrowsingContextID;
  }

  if (channel.loadInfo.browsingContextID) {
    return channel.loadInfo.browsingContextID;
  }

  if (channel.loadInfo.associatedBrowsingContextID) {
    return channel.loadInfo.associatedBrowsingContextID;
  }

  // At least WebSocket channel aren't having a browsingContextID set on their loadInfo
  // We fallback on top frame element, which works, but will be wrong for WebSocket
  // in same-process iframes...
  const topFrame = lazy.NetworkHelper.getTopFrameForRequest(channel);
  // topFrame is typically null for some chrome requests like favicons
  if (topFrame && topFrame.browsingContext) {
    return topFrame.browsingContext.id;
  }
  return null;
}

/**
 * Get the innerWindowId for the channel.
 *
 * @param {*} channel
 * @returns {number}
 */
function getChannelInnerWindowId(channel) {
  if (channel.loadInfo.innerWindowID) {
    return channel.loadInfo.innerWindowID;
  }
  // At least WebSocket channel aren't having a browsingContextID set on their loadInfo
  // We fallback on top frame element, which works, but will be wrong for WebSocket
  // in same-process iframes...
  const topFrame = lazy.NetworkHelper.getTopFrameForRequest(channel);
  // topFrame is typically null for some chrome requests like favicons
  if (topFrame?.browsingContext?.currentWindowGlobal) {
    return topFrame.browsingContext.currentWindowGlobal.innerWindowId;
  }
  return null;
}

/**
 * Does this channel represent a Preload request.
 *
 * @param {*} channel
 * @returns {boolean}
 */
function isPreloadRequest(channel) {
  const type = channel.loadInfo.internalContentPolicyType;
  return (
    type == Ci.nsIContentPolicy.TYPE_INTERNAL_SCRIPT_PRELOAD ||
    type == Ci.nsIContentPolicy.TYPE_INTERNAL_MODULE_PRELOAD ||
    type == Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_PRELOAD ||
    type == Ci.nsIContentPolicy.TYPE_INTERNAL_STYLESHEET_PRELOAD ||
    type == Ci.nsIContentPolicy.TYPE_INTERNAL_FONT_PRELOAD ||
    type == Ci.nsIContentPolicy.TYPE_INTERNAL_JSON_PRELOAD ||
    type == Ci.nsIContentPolicy.TYPE_INTERNAL_TEXT_PRELOAD
  );
}

/**
 * Get the channel cause details.
 *
 * @param {nsIChannel} channel
 * @returns {object}
 *          - loadingDocumentUri {string} uri of the document which created the
 *            channel
 *          - type {string} cause type as string
 */
function getCauseDetails(channel) {
  return {
    loadingDocumentUri: channel.loadInfo.loadingPrincipal
      ? channel.loadInfo.loadingPrincipal.spec
      : null,
    type: getChannelCauseType(channel),
  };
}

/**
 * Get the channel priority. Priority is a number which typically ranges from
 * -20 (lowest priority) to 20 (highest priority). Can be null if the channel
 * does not implement nsISupportsPriority.
 *
 * @param {nsIChannel} channel
 * @returns {number|undefined}
 */
function getChannelPriority(channel) {
  if (channel instanceof Ci.nsISupportsPriority) {
    return channel.priority;
  }

  return null;
}

/**
 * Get the channel HTTP version as an uppercase string starting with "HTTP/"
 * (eg "HTTP/2").
 *
 * @param {nsIChannel} channel
 * @returns {string}
 */
function getHttpVersion(channel) {
  if (!(channel instanceof Ci.nsIHttpChannelInternal)) {
    return null;
  }

  // Determine the HTTP version.
  const httpVersionMaj = {};
  const httpVersionMin = {};

  channel.QueryInterface(Ci.nsIHttpChannelInternal);
  channel.getResponseVersion(httpVersionMaj, httpVersionMin);

  // The official name HTTP version 2.0 and 3.0 are HTTP/2 and HTTP/3, omit the
  // trailing `.0`.
  if (httpVersionMin.value == 0) {
    return "HTTP/" + httpVersionMaj.value;
  }

  return "HTTP/" + httpVersionMaj.value + "." + httpVersionMin.value;
}

const UNKNOWN_PROTOCOL_STRINGS = ["", "unknown"];
const HTTP_PROTOCOL_STRINGS = ["http", "https"];
/**
 * Get the protocol for the provided httpActivity. Either the ALPN negotiated
 * protocol or as a fallback a protocol computed from the scheme and the
 * response status.
 *
 * TODO: The `protocol` is similar to another response property called
 * `httpVersion`. `httpVersion` is uppercase and purely computed from the
 * response status, whereas `protocol` uses nsIHttpChannel.protocolVersion by
 * default and otherwise falls back on `httpVersion`. Ideally we should merge
 * the two properties.
 *
 * @param {object} httpActivity
 *     The httpActivity object for which we need to get the protocol.
 *
 * @returns {string}
 *     The protocol as a string.
 */
function getProtocol(channel) {
  let protocol = "";
  try {
    const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
    // protocolVersion corresponds to ALPN negotiated protocol.
    protocol = httpChannel.protocolVersion;
  } catch (e) {
    // Ignore errors reading protocolVersion.
  }

  if (UNKNOWN_PROTOCOL_STRINGS.includes(protocol)) {
    protocol = channel.URI.scheme;
    const httpVersion = getHttpVersion(channel);
    if (
      typeof httpVersion == "string" &&
      HTTP_PROTOCOL_STRINGS.includes(protocol)
    ) {
      protocol = httpVersion.toLowerCase();
    }
  }

  return protocol;
}

/**
 * Get the channel referrer policy as a string
 * (eg "strict-origin-when-cross-origin").
 *
 * @param {nsIChannel} channel
 * @returns {string}
 */
function getReferrerPolicy(channel) {
  return channel.referrerInfo
    ? channel.referrerInfo.getReferrerPolicyString()
    : "";
}

/**
 * Check if the channel is private.
 *
 * @param {nsIChannel} channel
 * @returns {boolean}
 */
function isChannelPrivate(channel) {
  channel.QueryInterface(Ci.nsIPrivateBrowsingChannel);
  return channel.isChannelPrivate;
}

/**
 * Check if the channel data is loaded from the cache or not.
 *
 * @param {nsIChannel} channel
 *     The channel for which we need to check the cache status.
 *
 * @returns {boolean}
 *     True if the channel data is loaded from the cache, false otherwise.
 */
function isFromCache(channel) {
  if (channel instanceof Ci.nsICacheInfoChannel) {
    return channel.isFromCache();
  }

  return false;
}

/**
 * isNavigationRequest is true for the one request used to load a new top level
 * document of a given tab, or top level window. It will typically be false for
 * navigation requests of iframes, i.e. the request loading another document in
 * an iframe.
 *
 * @param {nsIChannel} channel
 * @return {boolean}
 */
function isNavigationRequest(channel) {
  return channel.isMainDocumentChannel && channel.loadInfo.isTopLevelLoad;
}

/**
 * Returns true  if the channel has been processed by URL-Classifier features
 * and is considered third-party with the top window URI, and if it has loaded
 * a resource that is classified as a tracker.
 *
 * @param {nsIChannel} channel
 * @return {boolean}
 */
function isThirdPartyTrackingResource(channel) {
  // Only consider channels classified as level-1 to be trackers if our preferences
  // would not cause such channels to be blocked in strict content blocking mode.
  // Make sure the value produced here is a boolean.
  return !!(
    channel instanceof Ci.nsIClassifiedChannel &&
    channel.isThirdPartyTrackingResource() &&
    (channel.thirdPartyClassificationFlags & lazy.tpFlagsMask) == 0
  );
}

/**
 * Retrieve the websocket channel for the provided channel, if available.
 * Returns null otherwise.
 *
 * @param {nsIChannel} channel
 * @returns {nsIWebSocketChannel|null}
 */
function getWebSocketChannel(channel) {
  let wsChannel = null;
  if (channel.notificationCallbacks) {
    try {
      wsChannel = channel.notificationCallbacks.QueryInterface(
        Ci.nsIWebSocketChannel
      );
    } catch (e) {
      // Not all channels implement nsIWebSocketChannel.
    }
  }
  return wsChannel;
}

/**
 * For a given channel, fetch the request's headers and cookies.
 *
 * @param {nsIChannel} channel
 * @return {object}
 *     An object with two properties:
 *     @property {Array<object>} cookies
 *         Array of { name, value } objects.
 *     @property {Array<object>} headers
 *         Array of { name, value } objects.
 */
function fetchRequestHeadersAndCookies(channel) {
  const headers = [];
  let cookies = [];
  let cookieHeader = null;

  // Copy the request header data.
  channel.visitRequestHeaders({
    visitHeader(name, value) {
      // The `Proxy-Authorization` header even though it appears on the channel is not
      // actually sent to the server for non CONNECT requests after the HTTP/HTTPS tunnel
      // is setup by the proxy.
      if (name == "Proxy-Authorization") {
        return;
      }
      if (name == "Cookie") {
        cookieHeader = value;
      }
      headers.push({ name, value });
    },
  });

  if (cookieHeader) {
    cookies = lazy.NetworkHelper.parseCookieHeader(cookieHeader);
  }

  return { cookies, headers };
}

/**
 * Parse the early hint raw headers string to an
 * array of name/value object header pairs
 *
 * @param {string} rawHeaders
 * @returns {Array}
 */
function parseEarlyHintsResponseHeaders(rawHeaders) {
  const headers = rawHeaders.split("\r\n");
  // Remove the line with the HTTP version and the status
  headers.shift();
  return headers
    .map(header => {
      const [name, value] = header.split(":");
      return { name, value };
    })
    .filter(header => header.name.length);
}

/**
 * For a given channel, fetch the response's headers and cookies.
 *
 * @param {nsIChannel} channel
 * @return {object}
 *     An object with two properties:
 *     @property {Array<object>} cookies
 *         Array of { name, value } objects.
 *     @property {Array<object>} headers
 *         Array of { name, value } objects.
 */
function fetchResponseHeadersAndCookies(channel) {
  // Read response headers and cookies.
  const headers = [];
  const setCookieHeaders = [];

  const SET_COOKIE_REGEXP = /set-cookie/i;
  channel.visitOriginalResponseHeaders({
    visitHeader(name, value) {
      if (SET_COOKIE_REGEXP.test(name)) {
        setCookieHeaders.push(value);
      }
      headers.push({ name, value });
    },
  });

  return {
    cookies: lazy.NetworkHelper.parseSetCookieHeaders(setCookieHeaders),
    headers,
  };
}

/**
 * Check if a given network request should be logged by a network monitor
 * based on the specified filters.
 *
 * @param {(nsIHttpChannel|nsIFileChannel)} channel
 *        Request to check.
 * @param filters
 *        NetworkObserver filters to match against. An object with one of the following attributes:
 *        - sessionContext: When inspecting requests from the parent process, pass the WatcherActor's session context.
 *          This helps know what is the overall debugged scope.
 *          See watcher actor constructor for more info.
 *        - targetActor: When inspecting requests from the content process, pass the WindowGlobalTargetActor.
 *          This helps know what exact subset of request we should accept.
 *          This is especially useful to behave correctly regarding EFT, where we should include or not
 *          iframes requests.
 *        - browserId, addonId, window: All these attributes are legacy.
 *          Only browserId attribute is still used by the legacy WebConsoleActor startListener API.
 * @return boolean
 *         True if the network request should be logged, false otherwise.
 */
function matchRequest(channel, filters) {
  // NetworkEventWatcher should now pass a session context for the parent process codepath
  if (filters.sessionContext) {
    const { type } = filters.sessionContext;
    if (type == "all") {
      return true;
    }

    // Ignore requests from chrome or add-on code when we don't monitor the whole browser
    if (
      channel.loadInfo?.loadingDocument === null &&
      isPrivilegedChannel(channel)
    ) {
      return false;
    }

    // When a page fails loading in top level or in iframe, an error page is shown
    // which will trigger a request to about:neterror (which is translated into a file:// URI request).
    // Ignore this request in regular toolbox (but not in the browser toolbox).
    if (channel.loadInfo?.loadErrorPage) {
      return false;
    }

    if (type == "browser-element") {
      let browsingContext =
        channel.loadInfo.browsingContext ||
        channel.loadInfo.associatedBrowsingContext;

      if (!browsingContext) {
        const topFrame = lazy.NetworkHelper.getTopFrameForRequest(channel);
        // `topFrame` is typically null for some chrome requests like favicons,
        // and its `browsingContext` attribute might be null if the request happened
        // while the tab is being closed.
        browsingContext = topFrame?.browsingContext;
      }

      return browsingContext?.browserId == filters.sessionContext.browserId;
    }
    if (type == "webextension") {
      return (
        channel.loadInfo?.loadingPrincipal?.addonId ===
        filters.sessionContext.addonId
      );
    }
    throw new Error("Unsupported session context type: " + type);
  }

  // NetworkEventContentWatcher and NetworkEventStackTraces pass a target actor instead, from the content processes
  // Because of EFT, we can't use session context as we have to know what exact windows the target actor covers.
  if (filters.targetActor) {
    // Ignore requests from chrome or add-on code when we don't monitor the whole browser
    if (
      filters.targetActor.sessionContext?.type !== "all" &&
      isPrivilegedChannel(channel)
    ) {
      return false;
    }

    // Bug 1769982 the target actor might be destroying and accessing windows will throw.
    // Ignore all further request when this happens.
    let windows;
    try {
      windows = filters.targetActor.windows;
    } catch (e) {
      return false;
    }
    const win = lazy.NetworkHelper.getWindowForRequest(channel);
    return windows.includes(win);
  }

  throw new Error(
    "matchRequest expects either a 'targetActor' or a 'sessionContext' attribute"
  );
}

function getBlockedReason(channel, fromCache = false) {
  let blockedReason;
  const extension = {};
  const { status } = channel;

  try {
    const request = channel.QueryInterface(Ci.nsIHttpChannel);
    const properties = request.QueryInterface(Ci.nsIPropertyBag);

    blockedReason = request.loadInfo.requestBlockingReason;
    extension.blocking = properties.getProperty("cancelledByExtension");

    // WebExtensionPolicy is not available for workers
    if (typeof WebExtensionPolicy !== "undefined") {
      extension.blocking = WebExtensionPolicy.getByID(extension.blocking).name;
    }
  } catch (err) {
    // "cancelledByExtension" doesn't have to be available.
  }

  if (
    blockedReason === Ci.nsILoadInfo.BLOCKING_REASON_CLASSIFY_HARMFULADDON_URI
  ) {
    try {
      const properties = channel.QueryInterface(Ci.nsIPropertyBag);
      extension.blocked = properties.getProperty("blockedExtension");
    } catch (err) {
      // "blockedExtension" doesn't have to be available.
    }
  }

  // These are platform errors which are not exposed to the users,
  // usually the requests (with these errors) might be displayed with various
  // other status codes.
  const ignoreList = [
    // These are emited when the request is already in the cache.
    "NS_ERROR_PARSED_DATA_CACHED",
    // This is emited when there is some issues around images e.g When the img.src
    // links to a non existent url. This is typically shown as a 404 request.
    "NS_IMAGELIB_ERROR_FAILURE",
    // This is emited when there is a redirect. They are shown as 301 requests.
    "NS_BINDING_REDIRECTED",
    // E.g Emited by send beacon requests.
    "NS_ERROR_ABORT",
    // This is emmited when browser.http.blank_page_with_error_response.enabled
    // is set to false, and a 404 or 500 request has no content.
    // They are shown as 404 or 500 requests.
    "NS_ERROR_NET_EMPTY_RESPONSE",
  ];

  // NS_BINDING_ABORTED are emmited when request are abruptly halted, these are valid and should not be ignored.
  // They can also be emmited for requests already cache which have the `cached` status, these should be ignored.
  if (fromCache) {
    ignoreList.push("NS_BINDING_ABORTED");
  }

  // If the request has not failed or is not blocked by a web extension, check for
  // any errors not on the ignore list. e.g When a host is not found (NS_ERROR_UNKNOWN_HOST).
  if (
    blockedReason == 0 &&
    !Components.isSuccessCode(status) &&
    !ignoreList.includes(ChromeUtils.getXPCOMErrorName(status))
  ) {
    blockedReason = ChromeUtils.getXPCOMErrorName(status);
  }

  return { extension, blockedReason };
}

function getCharset(channel) {
  const win = lazy.NetworkHelper.getWindowForRequest(channel);
  return win ? win.document.characterSet : null;
}

/**
 * Data channels are either handled in the parent process NetworkObserver for
 * navigation requests, or in content processes for any other request.
 *
 * This function allows to apply the same logic to build the network event actor
 * in both cases.
 *
 * @param {nsIDataChannel} channel
 *     The data channel for which we are creating a network event actor.
 * @param {object} networkEventActor
 *     The network event actor owning this resource.
 */
function handleDataChannel(channel, networkEventActor) {
  networkEventActor.addResponseStart({
    channel,
    fromCache: false,
    // According to the fetch spec for data URLs we can just hardcode
    // "Content-Type" header.
    rawHeaders: "content-type: " + channel.contentType,
  });

  // For data URLs we can not set up a stream listener as for http,
  // so we have to create a response manually and complete it.
  const response = {
    // TODO: Bug 1903807. Re-evaluate if it's correct to just return
    // zero for `bodySize` and `decodedBodySize`.
    bodySize: 0,
    decodedBodySize: 0,
    contentCharset: channel.contentCharset,
    contentLength: channel.contentLength,
    contentType: channel.contentType,
    mimeType: lazy.NetworkHelper.addCharsetToMimeType(
      channel.contentType,
      channel.contentCharset
    ),
    transferredSize: 0,
  };

  // For data URIs all timings can be set to zero.
  const result = lazy.NetworkTimings.getEmptyHARTimings();
  networkEventActor.addEventTimings(
    result.total,
    result.timings,
    result.offsets
  );

  const url = channel.URI.spec;
  response.text = url.substring(url.indexOf(",") + 1);
  if (
    !response.mimeType ||
    !lazy.NetworkHelper.isTextMimeType(response.mimeType)
  ) {
    response.encoding = "base64";
  }

  // Note: `size`` is only used by DevTools, WebDriverBiDi relies on
  // `bodySize` and `decodedBodySize`. Waiting on Bug 1903807 to decide
  // if those fields should have non-0 values as well.
  response.size = response.text.length;

  // Security information is not relevant for data channel, but it should
  // not be considered as insecure either. Set empty string as security
  // state.
  networkEventActor.addSecurityInfo({ state: "" });
  networkEventActor.addResponseContent(response);
  networkEventActor.addResponseContentComplete({});
}

/**
 * Sets a flag on the resource to specify that the data for a network event
 * is available. The flag is used by the consumer of the resource (frontend)
 * to determine when to lazily fetch the data.
 *
 * @param {object} resource - This could be a network resource object or a network resource
 *                            updates object.
 * @param {Array} networkEvents
 */
function setEventAsAvailable(resource, networkEvents) {
  for (const event of networkEvents) {
    if (!Object.values(NETWORK_EVENT_TYPES).includes(event)) {
      console.warn(`${event} is not a valid network event type.`);
      return;
    }
    resource[`${event}Available`] = true;
  }
}

/**
 * Helper to decode the content of a response object built by a
 * NetworkResponseListener.
 *
 * @param {Array<TypedArray>} chunks
 *     Array of response chunks read via NetUtil.readInputStream.
 * @param {object} options
 * @param {string} options.charset
 *     Charset used for the response.
 * @param {Array<string>} options.compressionEncodings
 *     Array of compression encodings applied to the response.
 * @param {number} encodedBodySize
 *     The total size of the encoded response.
 * @param {string} encoding
 *     The "encoding" of the response as computed by NetworkResponseListener.
 *     (can be either undefined or "base64" if the mime type is not text)
 * @returns {string}
 *     The decoded content, as a string.
 */
async function decodeResponseChunks(chunks, options) {
  const charset = options.charset || null;
  const { compressionEncodings = [], encodedBodySize, encoding } = options;

  const bytes = new Uint8Array(encodedBodySize);
  let offset = 0;
  for (const chunk of chunks) {
    bytes.set(new Uint8Array(chunk), offset);
    offset += chunk.byteLength;
  }

  const ArrayBufferInputStream = Components.Constructor(
    "@mozilla.org/io/arraybuffer-input-stream;1",
    "nsIArrayBufferInputStream",
    "setData"
  );
  const bodyStream = new ArrayBufferInputStream(
    bytes.buffer,
    0,
    bytes.byteLength
  );

  let decodedContent;
  if (compressionEncodings.length) {
    decodedContent = await decodeCompressedStream(
      bodyStream,
      bytes.byteLength,
      compressionEncodings,
      charset
    );
  } else {
    decodedContent = decodeUncompressedStream(
      bodyStream,
      bytes.byteLength,
      charset
    );
  }

  decodedContent = lazy.NetworkHelper.convertToUnicode(decodedContent, charset);
  if (encoding === "base64") {
    try {
      decodedContent = btoa(decodedContent);
    } catch {
      // Ignore `btoa`` errors because encoding="base64" does not guarantee the
      // content is actually base64 (loosely based on the mime type not being
      // a text mime type).
    }
  }

  return decodedContent;
}

function decodeUncompressedStream(stream, length) {
  const sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
    Ci.nsIScriptableInputStream
  );
  sis.init(stream);
  return sis.readBytes(length);
}

async function decodeCompressedStream(stream, length, encodings) {
  const listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance(
    Ci.nsIStreamLoader
  );
  const onDecodingComplete = new Promise(resolve => {
    listener.init({
      onStreamComplete: function onStreamComplete(
        _loader,
        _context,
        _status,
        _length,
        data
      ) {
        // `data`` might be a very large array, chunk calls to fromCharCode to
        // avoid "RangeError: too many arguments provided for a function call".
        const CHUNK_SIZE = 65536;
        let result = "";
        for (let i = 0; i < data.length; i += CHUNK_SIZE) {
          const chunk = data.slice(i, i + CHUNK_SIZE);
          result += String.fromCharCode.apply(null, chunk);
        }
        resolve(result);
      },
    });
  });

  const scs = Cc["@mozilla.org/streamConverters;1"].getService(
    Ci.nsIStreamConverterService
  );

  let converter;
  let nextListener = listener;
  for (const encoding of encodings) {
    // There can be multiple compressions applied
    converter = scs.asyncConvertData(
      encoding,
      "uncompressed",
      nextListener,
      null
    );
    nextListener = converter;
  }

  converter.onStartRequest(null, null);
  converter.onDataAvailable(null, stream, 0, length);
  converter.onStopRequest(null, null, null);

  return onDecodingComplete;
}

function isRedirect(responseStatus) {
  return REDIRECT_CODES.includes(responseStatus);
}

/**
 * Remove any frames in a stack that are related to chrome resource files.
 *
 * @param array stack
 *        An array of frames, each of which has a
 *        'filename' property.
 * @return array
 *         An array of stack frames with any chrome frames removed.
 *         The original array is not modified.
 */
function removeChromeFrames(stacktrace) {
  return stacktrace.filter(({ filename }) => {
    return (
      filename &&
      !filename.startsWith("resource://") &&
      !filename.startsWith("chrome://")
    );
  });
}

export const NetworkUtils = {
  ACCEPTED_COMPRESSION_ENCODINGS,
  decodeResponseChunks,
  fetchRequestHeadersAndCookies,
  fetchResponseHeadersAndCookies,
  getBlockedReason,
  getCauseDetails,
  getChannelBrowsingContextID,
  getChannelInnerWindowId,
  getChannelPriority,
  getCharset,
  getHttpVersion,
  getProtocol,
  getReferrerPolicy,
  getWebSocketChannel,
  handleDataChannel,
  isChannelFromSystemPrincipal,
  isChannelPrivate,
  isFromCache,
  isNavigationRequest,
  isPreloadRequest,
  isRedirect,
  isThirdPartyTrackingResource,
  matchRequest,
  NETWORK_EVENT_TYPES,
  parseEarlyHintsResponseHeaders,
  removeChromeFrames,
  setEventAsAvailable,
  stringToCauseType,
};
