tor-browser

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

network.sys.mjs (89858B)


      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 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      6 
      7 import { RootBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/RootBiDiModule.sys.mjs";
      8 
      9 const lazy = {};
     10 
     11 ChromeUtils.defineESModuleGetters(lazy, {
     12  NetworkHelper:
     13    "resource://devtools/shared/network-observer/NetworkHelper.sys.mjs",
     14 
     15  assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
     16  CacheBehavior: "chrome://remote/content/shared/NetworkCacheManager.sys.mjs",
     17  error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
     18  generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
     19  Log: "chrome://remote/content/shared/Log.sys.mjs",
     20  matchURLPattern:
     21    "chrome://remote/content/shared/webdriver/URLPattern.sys.mjs",
     22  NavigableManager: "chrome://remote/content/shared/NavigableManager.sys.mjs",
     23  NetworkDataBytes: "chrome://remote/content/shared/NetworkDataBytes.sys.mjs",
     24  NetworkDecodedBodySizeMap:
     25    "chrome://remote/content/shared/NetworkDecodedBodySizeMap.sys.mjs",
     26  NetworkListener:
     27    "chrome://remote/content/shared/listeners/NetworkListener.sys.mjs",
     28  NetworkResponse: "chrome://remote/content/shared/NetworkResponse.sys.mjs",
     29  parseChallengeHeader:
     30    "chrome://remote/content/shared/ChallengeHeaderParser.sys.mjs",
     31  parseURLPattern:
     32    "chrome://remote/content/shared/webdriver/URLPattern.sys.mjs",
     33  pprint: "chrome://remote/content/shared/Format.sys.mjs",
     34  truncate: "chrome://remote/content/shared/Format.sys.mjs",
     35  updateCacheBehavior:
     36    "chrome://remote/content/shared/NetworkCacheManager.sys.mjs",
     37  UserContextManager:
     38    "chrome://remote/content/shared/UserContextManager.sys.mjs",
     39 });
     40 
     41 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
     42  lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI)
     43 );
     44 
     45 // WIP: See Bug 1994983, the invalid tests for network.setExtraHeaders expect
     46 // the optional arguments not to accept null.
     47 const NULL = Symbol("NULL");
     48 
     49 /**
     50 * Defines the maximum total size as expected by the specification at
     51 * https://w3c.github.io/webdriver-bidi/#max-total-data-size
     52 */
     53 const DEFAULT_MAX_TOTAL_SIZE = 200 * 1000 * 1000;
     54 XPCOMUtils.defineLazyPreferenceGetter(
     55  lazy,
     56  "maxTotalDataSize",
     57  "remote.network.maxTotalDataSize",
     58  DEFAULT_MAX_TOTAL_SIZE
     59 );
     60 
     61 /**
     62 * @typedef {object} AuthChallenge
     63 * @property {string} scheme
     64 * @property {string} realm
     65 */
     66 
     67 /**
     68 * @typedef {object} AuthCredentials
     69 * @property {'password'} type
     70 * @property {string} username
     71 * @property {string} password
     72 */
     73 
     74 /**
     75 * @typedef {object} BaseParameters
     76 * @property {string=} context
     77 * @property {Array<string>?} intercepts
     78 * @property {boolean} isBlocked
     79 * @property {Navigation=} navigation
     80 * @property {number} redirectCount
     81 * @property {RequestData} request
     82 * @property {number} timestamp
     83 */
     84 
     85 /**
     86 * @typedef {object} BlockedRequest
     87 * @property {NetworkEventRecord} networkEventRecord
     88 * @property {InterceptPhase} phase
     89 */
     90 
     91 /**
     92 * Enum of possible BytesValue types.
     93 *
     94 * @readonly
     95 * @enum {BytesValueType}
     96 */
     97 export const BytesValueType = {
     98  Base64: "base64",
     99  String: "string",
    100 };
    101 
    102 /**
    103 * @typedef {object} BytesValue
    104 * @property {BytesValueType} type
    105 * @property {string} value
    106 */
    107 
    108 /**
    109 * Enum of possible network data collector types.
    110 *
    111 * @readonly
    112 * @enum {CollectorType}
    113 */
    114 const CollectorType = {
    115  Blob: "blob",
    116 };
    117 
    118 /**
    119 * @typedef {object} Collector
    120 * @property {number} maxEncodedDataSize
    121 * @property {Set<DataType>} dataTypes
    122 * @property {string} collector
    123 * @property {CollectorType} collectorType
    124 * @property {Set<string>} userContexts
    125 */
    126 
    127 /**
    128 * Enum of possible continueWithAuth actions.
    129 *
    130 * @readonly
    131 * @enum {ContinueWithAuthAction}
    132 */
    133 const ContinueWithAuthAction = {
    134  Cancel: "cancel",
    135  Default: "default",
    136  ProvideCredentials: "provideCredentials",
    137 };
    138 
    139 /**
    140 * @typedef {object} Cookie
    141 * @property {string} domain
    142 * @property {number=} expires
    143 * @property {boolean} httpOnly
    144 * @property {string} name
    145 * @property {string} path
    146 * @property {SameSite} sameSite
    147 * @property {boolean} secure
    148 * @property {number} size
    149 * @property {BytesValue} value
    150 */
    151 
    152 /**
    153 * @typedef {object} CookieHeader
    154 * @property {string} name
    155 * @property {BytesValue} value
    156 */
    157 
    158 /**
    159 * Enum of possible network data types.
    160 *
    161 * @readonly
    162 * @enum {DataType}
    163 */
    164 const DataType = {
    165  Request: "request",
    166  Response: "response",
    167 };
    168 
    169 /**
    170 * @typedef {object} Data
    171 * @property {BytesValue|null} bytes
    172 * @property {Array<Collector>} collectors
    173 * @property {boolean} pending
    174 * @property {string} request
    175 * @property {number} size
    176 * @property {DataType} type
    177 */
    178 
    179 /**
    180 * @typedef {object} FetchTimingInfo
    181 * @property {number} timeOrigin
    182 * @property {number} requestTime
    183 * @property {number} redirectStart
    184 * @property {number} redirectEnd
    185 * @property {number} fetchStart
    186 * @property {number} dnsStart
    187 * @property {number} dnsEnd
    188 * @property {number} connectStart
    189 * @property {number} connectEnd
    190 * @property {number} tlsStart
    191 * @property {number} requestStart
    192 * @property {number} responseStart
    193 * @property {number} responseEnd
    194 */
    195 
    196 /**
    197 * @typedef {object} Header
    198 * @property {string} name
    199 * @property {BytesValue} value
    200 */
    201 
    202 /**
    203 * @typedef {string} InitiatorType
    204 */
    205 
    206 /**
    207 * Enum of possible initiator types.
    208 *
    209 * @readonly
    210 * @enum {InitiatorType}
    211 */
    212 const InitiatorType = {
    213  Other: "other",
    214  Parser: "parser",
    215  Preflight: "preflight",
    216  Script: "script",
    217 };
    218 
    219 /**
    220 * @typedef {object} Initiator
    221 * @property {InitiatorType} type
    222 * @property {number=} columnNumber
    223 * @property {number=} lineNumber
    224 * @property {string=} request
    225 * @property {StackTrace=} stackTrace
    226 */
    227 
    228 /**
    229 * Enum of intercept phases.
    230 *
    231 * @readonly
    232 * @enum {InterceptPhase}
    233 */
    234 const InterceptPhase = {
    235  AuthRequired: "authRequired",
    236  BeforeRequestSent: "beforeRequestSent",
    237  ResponseStarted: "responseStarted",
    238 };
    239 
    240 /**
    241 * @typedef {object} InterceptProperties
    242 * @property {Array<InterceptPhase>} phases
    243 * @property {Array<URLPattern>} urlPatterns
    244 */
    245 
    246 /**
    247 * @typedef {object} RequestData
    248 * @property {number|null} bodySize
    249 *     Defaults to null.
    250 * @property {Array<Cookie>} cookies
    251 * @property {Array<Header>} headers
    252 * @property {number} headersSize
    253 * @property {string} method
    254 * @property {string} request
    255 * @property {FetchTimingInfo} timings
    256 * @property {string} url
    257 */
    258 
    259 /**
    260 * @typedef {object} BeforeRequestSentParametersProperties
    261 * @property {Initiator} initiator
    262 */
    263 
    264 /* eslint-disable jsdoc/valid-types */
    265 /**
    266 * Parameters for the BeforeRequestSent event
    267 *
    268 * @typedef {BaseParameters & BeforeRequestSentParametersProperties} BeforeRequestSentParameters
    269 */
    270 /* eslint-enable jsdoc/valid-types */
    271 
    272 /**
    273 * @typedef {object} ResponseContent
    274 * @property {number|null} size
    275 *     Defaults to null.
    276 */
    277 
    278 /**
    279 * @typedef {object} ResponseData
    280 * @property {string} url
    281 * @property {string} protocol
    282 * @property {number} status
    283 * @property {string} statusText
    284 * @property {boolean} fromCache
    285 * @property {Array<Header>} headers
    286 * @property {string} mimeType
    287 * @property {number} bytesReceived
    288 * @property {number|null} headersSize
    289 *     Defaults to null.
    290 * @property {number|null} bodySize
    291 *     Defaults to null.
    292 * @property {ResponseContent} content
    293 * @property {Array<AuthChallenge>=} authChallenges
    294 */
    295 
    296 /**
    297 * @typedef {object} ResponseStartedParametersProperties
    298 * @property {ResponseData} response
    299 */
    300 
    301 /* eslint-disable jsdoc/valid-types */
    302 /**
    303 * Parameters for the ResponseStarted event
    304 *
    305 * @typedef {BaseParameters & ResponseStartedParametersProperties} ResponseStartedParameters
    306 */
    307 /* eslint-enable jsdoc/valid-types */
    308 
    309 /**
    310 * @typedef {object} ResponseCompletedParametersProperties
    311 * @property {ResponseData} response
    312 */
    313 
    314 /**
    315 * Enum of possible sameSite values.
    316 *
    317 * @readonly
    318 * @enum {SameSite}
    319 */
    320 const SameSite = {
    321  Lax: "lax",
    322  None: "none",
    323  Strict: "strict",
    324 };
    325 
    326 /**
    327 * @typedef {object} SetCookieHeader
    328 * @property {string} name
    329 * @property {BytesValue} value
    330 * @property {string=} domain
    331 * @property {boolean=} httpOnly
    332 * @property {string=} expiry
    333 * @property {number=} maxAge
    334 * @property {string=} path
    335 * @property {SameSite=} sameSite
    336 * @property {boolean=} secure
    337 */
    338 
    339 /**
    340 * @typedef {object} URLPatternPattern
    341 * @property {'pattern'} type
    342 * @property {string=} protocol
    343 * @property {string=} hostname
    344 * @property {string=} port
    345 * @property {string=} pathname
    346 * @property {string=} search
    347 */
    348 
    349 /**
    350 * @typedef {object} URLPatternString
    351 * @property {'string'} type
    352 * @property {string} pattern
    353 */
    354 
    355 /**
    356 * @typedef {(URLPatternPattern|URLPatternString)} URLPattern
    357 */
    358 
    359 /* eslint-disable jsdoc/valid-types */
    360 /**
    361 * Parameters for the ResponseCompleted event
    362 *
    363 * @typedef {BaseParameters & ResponseCompletedParametersProperties} ResponseCompletedParameters
    364 */
    365 /* eslint-enable jsdoc/valid-types */
    366 
    367 // @see https://searchfox.org/mozilla-central/rev/527d691a542ccc0f333e36689bd665cb000360b2/netwerk/protocol/http/HttpBaseChannel.cpp#2083-2088
    368 const IMMUTABLE_RESPONSE_HEADERS = [
    369  "content-encoding",
    370  "content-length",
    371  "content-type",
    372  "trailer",
    373  "transfer-encoding",
    374 ];
    375 
    376 const UNAVAILABLE_DATA_ERROR_REASON = {
    377  Aborted: "aborted",
    378  Evicted: "evicted",
    379 };
    380 
    381 class NetworkModule extends RootBiDiModule {
    382  #blockedRequests;
    383  #collectedNetworkData;
    384  #decodedBodySizeMap;
    385  #extraHeaders;
    386  #interceptMap;
    387  #networkCollectors;
    388  #networkListener;
    389  #redirectedRequests;
    390  #subscribedEvents;
    391 
    392  constructor(messageHandler) {
    393    super(messageHandler);
    394 
    395    // Map of request id to BlockedRequest
    396    this.#blockedRequests = new Map();
    397 
    398    // Map of collected network Data, from a composite key `${requestId}-${dataType}`
    399    // to a network Data struct.
    400    // https://w3c.github.io/webdriver-bidi/#collected-network-data
    401    // TODO: This is a property of the remote end per spec, not of the session.
    402    // At the moment, each network module starts its own network observer. This
    403    // makes it impossible to have a session agnostic step when receiving a new
    404    // network event.
    405    // Note: Implemented as a Map. A Map is guaranteed to iterate in the order
    406    // of insertion, but still provides fast lookup.
    407    this.#collectedNetworkData = new Map();
    408 
    409    // Implements the BiDi Session extra headers.
    410    // https://w3c.github.io/webdriver-bidi/#session-extra-headers
    411    this.#extraHeaders = {
    412      // Array of Header objects, initially empty.
    413      defaultHeaders: [],
    414      // WeakMap between navigables and arrays of Header objects.
    415      // Due to technical limitations, navigables are represented via the
    416      // BrowsingContextWebProgress of the top level browsing context.
    417      navigableHeaders: new WeakMap(),
    418      // Map between user context ids and arrays of Header objects.
    419      userContextHeaders: new Map(),
    420    };
    421 
    422    // Map of intercept id to InterceptProperties
    423    this.#interceptMap = new Map();
    424 
    425    // Map of collector id to Collector
    426    this.#networkCollectors = new Map();
    427 
    428    // Set of request ids which are being redirected using continueRequest with
    429    // a url parameter. Those requests will lead to an additional beforeRequestSent
    430    // event which needs to be filtered out.
    431    this.#redirectedRequests = new Set();
    432 
    433    // Set of event names which have active subscriptions
    434    this.#subscribedEvents = new Set();
    435 
    436    this.#decodedBodySizeMap = new lazy.NetworkDecodedBodySizeMap();
    437 
    438    this.#networkListener = new lazy.NetworkListener(
    439      this.messageHandler.navigationManager,
    440      this.#decodedBodySizeMap
    441    );
    442    this.#networkListener.on("auth-required", this.#onAuthRequired);
    443    this.#networkListener.on("before-request-sent", this.#onBeforeRequestSent);
    444    this.#networkListener.on("fetch-error", this.#onFetchError);
    445    this.#networkListener.on("response-completed", this.#onResponseEvent);
    446    this.#networkListener.on("response-started", this.#onResponseEvent);
    447 
    448    lazy.UserContextManager.on(
    449      "user-context-deleted",
    450      this.#onUserContextDeleted
    451    );
    452  }
    453 
    454  destroy() {
    455    lazy.UserContextManager.off(
    456      "user-context-deleted",
    457      this.#onUserContextDeleted
    458    );
    459 
    460    this.#networkListener.off("auth-required", this.#onAuthRequired);
    461    this.#networkListener.off("before-request-sent", this.#onBeforeRequestSent);
    462    this.#networkListener.off("fetch-error", this.#onFetchError);
    463    this.#networkListener.off("response-completed", this.#onResponseEvent);
    464    this.#networkListener.off("response-started", this.#onResponseEvent);
    465    this.#networkListener.destroy();
    466 
    467    this.#decodedBodySizeMap.destroy();
    468 
    469    // Network related session cleanup steps
    470    // https://w3c.github.io/webdriver-bidi/#cleanup-the-session
    471 
    472    // Resume blocked requests
    473    for (const [, { request }] of this.#blockedRequests) {
    474      try {
    475        request.wrappedChannel.resume();
    476      } catch {
    477        lazy.logger.warn(
    478          `Failed to resume request "${request.requestId}" when ending the session`
    479        );
    480      }
    481    }
    482 
    483    // Remove collectors from collected data.
    484    // TODO: This step is unnecessary until we support multiple sessions, because
    485    // the collectedNetworkData is attached to the session and is cleaned up
    486    // afterwards.
    487 
    488    this.#blockedRequests = null;
    489    this.#collectedNetworkData = null;
    490    this.#decodedBodySizeMap = null;
    491    this.#extraHeaders = null;
    492    this.#interceptMap = null;
    493    this.#networkCollectors = null;
    494    this.#subscribedEvents = null;
    495  }
    496 
    497  /**
    498   * Adds a data collector to collect network data.
    499   *
    500   * @param {object=} options
    501   * @param {Array<DataType>} options.dataTypes
    502   *     Maximum size of data to collect in bytes.
    503   * @param {number} options.maxEncodedDataSize
    504   *     Maximum size of data to collect in bytes.
    505   * @param {CollectorType=} options.collectorType
    506   *     The type of data to collect. Optional, defaults to "blob".
    507   * @param {Array<string>=} options.contexts
    508   *     Optional list of browsing context ids.
    509   * @param {Array<string>=} options.userContexts
    510   *     Optional list of user context ids.
    511   *
    512   * @returns {object}
    513   *     An object with the following property:
    514   *     - collector {string} The unique id of the data collector.
    515   *
    516   * @throws {InvalidArgumentError}
    517   *     Raised if an argument is of an invalid type or value.
    518   */
    519  addDataCollector(options = {}) {
    520    const {
    521      dataTypes,
    522      maxEncodedDataSize,
    523      collectorType = CollectorType.Blob,
    524      contexts: contextIds = null,
    525      userContexts: userContextIds = null,
    526    } = options;
    527 
    528    lazy.assert.positiveInteger(
    529      maxEncodedDataSize,
    530      lazy.pprint`Expected "maxEncodedDataSize" to be a positive integer, got ${maxEncodedDataSize}`
    531    );
    532 
    533    if (maxEncodedDataSize === 0) {
    534      throw new lazy.error.InvalidArgumentError(
    535        `Expected "maxEncodedDataSize" to be greater than 0, got ${maxEncodedDataSize}`
    536      );
    537    }
    538 
    539    if (maxEncodedDataSize > lazy.maxTotalDataSize) {
    540      throw new lazy.error.InvalidArgumentError(
    541        `Expected "maxEncodedDataSize" to be less than the max total data size available (${lazy.maxTotalDataSize}), got ${maxEncodedDataSize}`
    542      );
    543    }
    544 
    545    lazy.assert.isNonEmptyArray(
    546      dataTypes,
    547      `Expected "dataTypes" to be a non-empty array, got ${dataTypes}`
    548    );
    549 
    550    const supportedDataTypes = Object.values(DataType);
    551    for (const dataType of dataTypes) {
    552      if (!supportedDataTypes.includes(dataType)) {
    553        throw new lazy.error.InvalidArgumentError(
    554          `Expected "dataTypes" values to be one of ${supportedDataTypes},` +
    555            lazy.pprint` got ${dataType}`
    556        );
    557      }
    558    }
    559 
    560    const supportedCollectorTypes = Object.values(CollectorType);
    561    if (!supportedCollectorTypes.includes(collectorType)) {
    562      throw new lazy.error.InvalidArgumentError(
    563        `Expected "collectorType" to be one of ${supportedCollectorTypes},` +
    564          lazy.pprint` got ${collectorType}`
    565      );
    566    }
    567 
    568    const navigables = new Set();
    569    const userContexts = new Set();
    570    if (contextIds !== null) {
    571      lazy.assert.isNonEmptyArray(
    572        contextIds,
    573        lazy.pprint`Expected "contexts" to be a non-empty array, got ${contextIds}`
    574      );
    575 
    576      for (const contextId of contextIds) {
    577        lazy.assert.string(
    578          contextId,
    579          lazy.pprint`Expected elements of "contexts" to be a string, got ${contextId}`
    580        );
    581        const context = this._getNavigable(contextId);
    582 
    583        lazy.assert.topLevel(
    584          context,
    585          lazy.pprint`Browsing context with id ${contextId} is not top-level`
    586        );
    587 
    588        navigables.add(contextId);
    589      }
    590    }
    591 
    592    if (userContextIds !== null) {
    593      lazy.assert.isNonEmptyArray(
    594        userContextIds,
    595        lazy.pprint`Expected "userContexts" to be a non-empty array, got ${userContextIds}`
    596      );
    597 
    598      for (const userContextId of userContextIds) {
    599        lazy.assert.string(
    600          userContextId,
    601          lazy.pprint`Expected elements of "userContexts" to be a string, got ${userContextId}`
    602        );
    603 
    604        const internalId =
    605          lazy.UserContextManager.getInternalIdById(userContextId);
    606 
    607        if (internalId === null) {
    608          throw new lazy.error.NoSuchUserContextError(
    609            `User context with id: ${userContextId} doesn't exist`
    610          );
    611        }
    612 
    613        userContexts.add(userContextId);
    614      }
    615    }
    616 
    617    if (contextIds !== null && userContextIds !== null) {
    618      throw new lazy.error.InvalidArgumentError(
    619        `Providing both "contexts" and "userContexts" arguments is not supported`
    620      );
    621    }
    622 
    623    // Generate a unique collector ID
    624    const collectorId = lazy.generateUUID();
    625 
    626    const collector = {
    627      collector: collectorId,
    628      collectorType,
    629      contexts: navigables,
    630      dataTypes,
    631      maxEncodedDataSize,
    632      userContexts,
    633    };
    634 
    635    this.#networkCollectors.set(collectorId, collector);
    636 
    637    return {
    638      collector: collectorId,
    639    };
    640  }
    641 
    642  /**
    643   * Adds a network intercept, which allows to intercept and modify network
    644   * requests and responses.
    645   *
    646   * The network intercept will be created for the provided phases
    647   * (InterceptPhase) and for specific url patterns. When a network event
    648   * corresponding to an intercept phase has a URL which matches any url pattern
    649   * of any intercept, the request will be suspended.
    650   *
    651   * @param {object=} options
    652   * @param {Array<string>=} options.contexts
    653   *     The list of browsing context ids where this intercept should be used.
    654   *     Optional, defaults to null.
    655   * @param {Array<InterceptPhase>} options.phases
    656   *     The phases where this intercept should be checked.
    657   * @param {Array<URLPattern>=} options.urlPatterns
    658   *     The URL patterns for this intercept. Optional, defaults to empty array.
    659   *
    660   * @returns {object}
    661   *     An object with the following property:
    662   *     - intercept {string} The unique id of the network intercept.
    663   *
    664   * @throws {InvalidArgumentError}
    665   *     Raised if an argument is of an invalid type or value.
    666   */
    667  addIntercept(options = {}) {
    668    const { contexts = null, phases, urlPatterns = [] } = options;
    669 
    670    if (contexts !== null) {
    671      lazy.assert.isNonEmptyArray(
    672        contexts,
    673        `Expected "contexts" to be a non-empty array, got ${contexts}`
    674      );
    675 
    676      for (const contextId of contexts) {
    677        lazy.assert.string(
    678          contextId,
    679          `Expected elements of "contexts" to be a string, got ${contextId}`
    680        );
    681        const context = this._getNavigable(contextId);
    682 
    683        lazy.assert.topLevel(
    684          context,
    685          lazy.pprint`Browsing context with id ${contextId} is not top-level`
    686        );
    687      }
    688    }
    689 
    690    lazy.assert.isNonEmptyArray(
    691      phases,
    692      `Expected "phases" to be a non-empty array, got ${phases}`
    693    );
    694 
    695    const supportedInterceptPhases = Object.values(InterceptPhase);
    696    for (const phase of phases) {
    697      if (!supportedInterceptPhases.includes(phase)) {
    698        throw new lazy.error.InvalidArgumentError(
    699          `Expected "phases" values to be one of ${supportedInterceptPhases}, got ${phase}`
    700        );
    701      }
    702    }
    703 
    704    lazy.assert.array(
    705      urlPatterns,
    706      `Expected "urlPatterns" to be an array, got ${urlPatterns}`
    707    );
    708 
    709    const parsedPatterns = urlPatterns.map(urlPattern =>
    710      lazy.parseURLPattern(urlPattern)
    711    );
    712 
    713    const interceptId = lazy.generateUUID();
    714    this.#interceptMap.set(interceptId, {
    715      contexts,
    716      phases,
    717      urlPatterns: parsedPatterns,
    718    });
    719 
    720    return {
    721      intercept: interceptId,
    722    };
    723  }
    724 
    725  /**
    726   * Continues a request that is blocked by a network intercept at the
    727   * beforeRequestSent phase.
    728   *
    729   * @param {object=} options
    730   * @param {string} options.request
    731   *     The id of the blocked request that should be continued.
    732   * @param {BytesValue=} options.body
    733   *     Optional BytesValue to replace the body of the request.
    734   * @param {Array<CookieHeader>=} options.cookies
    735   *     Optional array of cookie header values to replace the cookie header of
    736   *     the request.
    737   * @param {Array<Header>=} options.headers
    738   *     Optional array of headers to replace the headers of the request.
    739   *     request.
    740   * @param {string=} options.method
    741   *     Optional string to replace the method of the request.
    742   * @param {string=} options.url
    743   *     Optional string to replace the url of the request. If the provided url
    744   *     is not a valid URL, an InvalidArgumentError will be thrown.
    745   *
    746   * @throws {InvalidArgumentError}
    747   *     Raised if an argument is of an invalid type or value.
    748   * @throws {NoSuchRequestError}
    749   *     Raised if the request id does not match any request in the blocked
    750   *     requests map.
    751   */
    752  async continueRequest(options = {}) {
    753    const {
    754      body = null,
    755      cookies = null,
    756      headers = null,
    757      method = null,
    758      url = null,
    759      request: requestId,
    760    } = options;
    761 
    762    lazy.assert.string(
    763      requestId,
    764      `Expected "request" to be a string, got ${requestId}`
    765    );
    766 
    767    if (body !== null) {
    768      this.#assertBytesValue(
    769        body,
    770        lazy.truncate`Expected "body" to be a network.BytesValue, got ${body}`
    771      );
    772    }
    773 
    774    if (cookies !== null) {
    775      lazy.assert.array(
    776        cookies,
    777        `Expected "cookies" to be an array got ${cookies}`
    778      );
    779 
    780      for (const cookie of cookies) {
    781        this.#assertHeader(
    782          cookie,
    783          `Expected values in "cookies" to be network.CookieHeader, got ${cookie}`
    784        );
    785      }
    786    }
    787 
    788    let deserializedHeaders = [];
    789    if (headers !== null) {
    790      deserializedHeaders = this.#deserializeHeaders(headers);
    791    }
    792 
    793    if (method !== null) {
    794      lazy.assert.string(
    795        method,
    796        `Expected "method" to be a string, got ${method}`
    797      );
    798      lazy.assert.that(
    799        value => this.#isValidHttpToken(value),
    800        `Expected "method" to be a valid HTTP token, got ${method}`
    801      )(method);
    802    }
    803 
    804    if (url !== null) {
    805      lazy.assert.string(url, `Expected "url" to be a string, got ${url}`);
    806 
    807      if (!URL.canParse(url)) {
    808        throw new lazy.error.InvalidArgumentError(
    809          `Expected "url" to be a valid URL, got ${url}`
    810        );
    811      }
    812    }
    813 
    814    if (!this.#blockedRequests.has(requestId)) {
    815      throw new lazy.error.NoSuchRequestError(
    816        `Blocked request with id ${requestId} not found`
    817      );
    818    }
    819 
    820    const { phase, request, resolveBlockedEvent } =
    821      this.#blockedRequests.get(requestId);
    822 
    823    if (phase !== InterceptPhase.BeforeRequestSent) {
    824      throw new lazy.error.InvalidArgumentError(
    825        `Expected blocked request to be in "beforeRequestSent" phase, got ${phase}`
    826      );
    827    }
    828 
    829    if (method !== null) {
    830      request.setRequestMethod(method);
    831    }
    832 
    833    if (headers !== null) {
    834      // Delete all existing request headers.
    835      request.headers.forEach(([name]) => {
    836        request.clearRequestHeader(name);
    837      });
    838 
    839      // Set all headers specified in the headers parameter.
    840      for (const [name, value] of deserializedHeaders) {
    841        request.setRequestHeader(name, value, { merge: true });
    842      }
    843    }
    844 
    845    if (cookies !== null) {
    846      let cookieHeader = "";
    847      for (const cookie of cookies) {
    848        if (cookieHeader != "") {
    849          cookieHeader += ";";
    850        }
    851        cookieHeader += this.#serializeCookieHeader(cookie);
    852      }
    853 
    854      let foundCookieHeader = false;
    855      for (const [name] of request.headers) {
    856        if (name.toLowerCase() == "cookie") {
    857          // If there is already a cookie header, use merge: false to override
    858          // the value.
    859          request.setRequestHeader(name, cookieHeader, { merge: false });
    860          foundCookieHeader = true;
    861          break;
    862        }
    863      }
    864 
    865      if (!foundCookieHeader) {
    866        request.setRequestHeader("Cookie", cookieHeader, { merge: false });
    867      }
    868    }
    869 
    870    if (body !== null) {
    871      const value = deserializeBytesValue(body);
    872      request.setRequestBody(value);
    873    }
    874 
    875    if (url !== null) {
    876      // Store the requestId in the redirectedRequests set to skip the extra
    877      // beforeRequestSent event.
    878      this.#redirectedRequests.add(requestId);
    879      request.redirectTo(url);
    880    }
    881 
    882    request.wrappedChannel.resume();
    883 
    884    resolveBlockedEvent();
    885  }
    886 
    887  /**
    888   * Continues a response that is blocked by a network intercept at the
    889   * responseStarted or authRequired phase.
    890   *
    891   * @param {object=} options
    892   * @param {string} options.request
    893   *     The id of the blocked request that should be continued.
    894   * @param {Array<SetCookieHeader>=} options.cookies
    895   *     Optional array of set-cookie header values to replace the set-cookie
    896   *     headers of the response.
    897   * @param {AuthCredentials=} options.credentials
    898   *     Optional AuthCredentials to use.
    899   * @param {Array<Header>=} options.headers
    900   *     Optional array of header values to replace the headers of the response.
    901   * @param {string=} options.reasonPhrase
    902   *     Optional string to replace the status message of the response.
    903   * @param {number=} options.statusCode
    904   *     Optional number to replace the status code of the response.
    905   *
    906   * @throws {InvalidArgumentError}
    907   *     Raised if an argument is of an invalid type or value.
    908   * @throws {NoSuchRequestError}
    909   *     Raised if the request id does not match any request in the blocked
    910   *     requests map.
    911   */
    912  async continueResponse(options = {}) {
    913    const {
    914      cookies = null,
    915      credentials = null,
    916      headers = null,
    917      reasonPhrase = null,
    918      request: requestId,
    919      statusCode = null,
    920    } = options;
    921 
    922    lazy.assert.string(
    923      requestId,
    924      `Expected "request" to be a string, got ${requestId}`
    925    );
    926 
    927    if (cookies !== null) {
    928      lazy.assert.array(
    929        cookies,
    930        `Expected "cookies" to be an array got ${cookies}`
    931      );
    932 
    933      for (const cookie of cookies) {
    934        this.#assertSetCookieHeader(cookie);
    935      }
    936    }
    937 
    938    if (credentials !== null) {
    939      this.#assertAuthCredentials(credentials);
    940    }
    941 
    942    let deserializedHeaders = [];
    943    if (headers !== null) {
    944      // For existing responses, are unable to update some response headers,
    945      // so we skip them for the time being and log a warning.
    946      // Bug 1914351 should remove this limitation.
    947      deserializedHeaders = this.#deserializeHeaders(headers).filter(
    948        ([name]) => {
    949          if (IMMUTABLE_RESPONSE_HEADERS.includes(name.toLowerCase())) {
    950            lazy.logger.warn(
    951              `network.continueResponse cannot currently modify the header "${name}", skipping (see Bug 1914351).`
    952            );
    953            return false;
    954          }
    955          return true;
    956        }
    957      );
    958    }
    959 
    960    if (reasonPhrase !== null) {
    961      lazy.assert.string(
    962        reasonPhrase,
    963        `Expected "reasonPhrase" to be a string, got ${reasonPhrase}`
    964      );
    965    }
    966 
    967    if (statusCode !== null) {
    968      lazy.assert.positiveInteger(
    969        statusCode,
    970        `Expected "statusCode" to be a positive integer, got ${statusCode}`
    971      );
    972    }
    973 
    974    if (!this.#blockedRequests.has(requestId)) {
    975      throw new lazy.error.NoSuchRequestError(
    976        `Blocked request with id ${requestId} not found`
    977      );
    978    }
    979 
    980    const { authCallbacks, phase, request, resolveBlockedEvent, response } =
    981      this.#blockedRequests.get(requestId);
    982 
    983    if (headers !== null) {
    984      // Delete all existing response headers.
    985      response.headers
    986        .filter(
    987          ([name]) =>
    988            // All headers in IMMUTABLE_RESPONSE_HEADERS cannot be changed and
    989            // will lead to a NS_ERROR_ILLEGAL_VALUE error.
    990            // Bug 1914351 should remove this limitation.
    991            !IMMUTABLE_RESPONSE_HEADERS.includes(name.toLowerCase())
    992        )
    993        .forEach(([name]) => response.clearResponseHeader(name));
    994 
    995      for (const [name, value] of deserializedHeaders) {
    996        response.setResponseHeader(name, value, { merge: true });
    997      }
    998    }
    999 
   1000    if (cookies !== null) {
   1001      for (const cookie of cookies) {
   1002        const headerValue = this.#serializeSetCookieHeader(cookie);
   1003        response.setResponseHeader("Set-Cookie", headerValue, { merge: true });
   1004      }
   1005    }
   1006 
   1007    if (statusCode !== null || reasonPhrase !== null) {
   1008      response.setResponseStatus({
   1009        status: statusCode,
   1010        statusText: reasonPhrase,
   1011      });
   1012    }
   1013 
   1014    if (
   1015      phase !== InterceptPhase.ResponseStarted &&
   1016      phase !== InterceptPhase.AuthRequired
   1017    ) {
   1018      throw new lazy.error.InvalidArgumentError(
   1019        `Expected blocked request to be in "responseStarted" or "authRequired" phase, got ${phase}`
   1020      );
   1021    }
   1022 
   1023    if (phase === InterceptPhase.AuthRequired) {
   1024      // Requests blocked in the AuthRequired phase should be resumed using
   1025      // authCallbacks.
   1026      if (credentials !== null) {
   1027        await authCallbacks.provideAuthCredentials(
   1028          credentials.username,
   1029          credentials.password
   1030        );
   1031      } else {
   1032        await authCallbacks.provideAuthCredentials();
   1033      }
   1034    } else {
   1035      request.wrappedChannel.resume();
   1036    }
   1037 
   1038    resolveBlockedEvent();
   1039  }
   1040 
   1041  /**
   1042   * Continues a response that is blocked by a network intercept at the
   1043   * authRequired phase.
   1044   *
   1045   * @param {object=} options
   1046   * @param {string} options.request
   1047   *     The id of the blocked request that should be continued.
   1048   * @param {string} options.action
   1049   *     The continueWithAuth action, one of ContinueWithAuthAction.
   1050   * @param {AuthCredentials=} options.credentials
   1051   *     The credentials to use for the ContinueWithAuthAction.ProvideCredentials
   1052   *     action.
   1053   *
   1054   * @throws {InvalidArgumentError}
   1055   *     Raised if an argument is of an invalid type or value.
   1056   * @throws {NoSuchRequestError}
   1057   *     Raised if the request id does not match any request in the blocked
   1058   *     requests map.
   1059   */
   1060  async continueWithAuth(options = {}) {
   1061    const { action, credentials, request: requestId } = options;
   1062 
   1063    lazy.assert.string(
   1064      requestId,
   1065      `Expected "request" to be a string, got ${requestId}`
   1066    );
   1067 
   1068    if (!Object.values(ContinueWithAuthAction).includes(action)) {
   1069      throw new lazy.error.InvalidArgumentError(
   1070        `Expected "action" to be one of ${Object.values(
   1071          ContinueWithAuthAction
   1072        )} got ${action}`
   1073      );
   1074    }
   1075 
   1076    if (action == ContinueWithAuthAction.ProvideCredentials) {
   1077      this.#assertAuthCredentials(credentials);
   1078    }
   1079 
   1080    if (!this.#blockedRequests.has(requestId)) {
   1081      throw new lazy.error.NoSuchRequestError(
   1082        `Blocked request with id ${requestId} not found`
   1083      );
   1084    }
   1085 
   1086    const { authCallbacks, phase, resolveBlockedEvent } =
   1087      this.#blockedRequests.get(requestId);
   1088 
   1089    if (phase !== InterceptPhase.AuthRequired) {
   1090      throw new lazy.error.InvalidArgumentError(
   1091        `Expected blocked request to be in "authRequired" phase, got ${phase}`
   1092      );
   1093    }
   1094 
   1095    switch (action) {
   1096      case ContinueWithAuthAction.Cancel: {
   1097        authCallbacks.cancelAuthPrompt();
   1098        break;
   1099      }
   1100      case ContinueWithAuthAction.Default: {
   1101        authCallbacks.forwardAuthPrompt();
   1102        break;
   1103      }
   1104      case ContinueWithAuthAction.ProvideCredentials: {
   1105        await authCallbacks.provideAuthCredentials(
   1106          credentials.username,
   1107          credentials.password
   1108        );
   1109 
   1110        break;
   1111      }
   1112    }
   1113 
   1114    resolveBlockedEvent();
   1115  }
   1116 
   1117  /**
   1118   * Releases a collected network data for a given collector and data type.
   1119   *
   1120   * @param {object=} options
   1121   * @param {string} options.collector
   1122   *     The collector from which the data should be disowned.
   1123   * @param {string} options.dataType
   1124   *     The data type of the data to disown.
   1125   * @param {string} options.request
   1126   *     The id of the request for which data should be disowned.
   1127   *
   1128   * @throws {InvalidArgumentError}
   1129   *     Raised if an argument is of an invalid type or value.
   1130   * @throws {NoSuchNetworkCollectorError}
   1131   *     Raised if the collector id could not be found in the internal collectors
   1132   *     map.
   1133   * @throws {NoSuchNetworkDataError}
   1134   *     If the network data could not be found for the provided parameters.
   1135   */
   1136  async disownData(options = {}) {
   1137    const { collector, dataType, request: requestId } = options;
   1138 
   1139    lazy.assert.string(
   1140      requestId,
   1141      lazy.pprint`Expected "request" to be a string, got ${requestId}`
   1142    );
   1143 
   1144    const supportedDataTypes = Object.values(DataType);
   1145    if (!supportedDataTypes.includes(dataType)) {
   1146      throw new lazy.error.InvalidArgumentError(
   1147        `Expected "dataType" to be one of ${supportedDataTypes},` +
   1148          lazy.pprint` got ${dataType}`
   1149      );
   1150    }
   1151 
   1152    lazy.assert.string(
   1153      collector,
   1154      lazy.pprint`Expected "collector" to be a string, got ${collector}`
   1155    );
   1156 
   1157    if (!this.#networkCollectors.has(collector)) {
   1158      throw new lazy.error.NoSuchNetworkCollectorError(
   1159        `Network data collector with id ${collector} not found`
   1160      );
   1161    }
   1162 
   1163    const collectedData = this.#getCollectedData(requestId, dataType);
   1164    if (!collectedData) {
   1165      throw new lazy.error.NoSuchNetworkDataError(
   1166        `Network data for request id ${requestId} and DataType ${dataType} not found`
   1167      );
   1168    }
   1169 
   1170    if (!collectedData.collectors.has(collector)) {
   1171      throw new lazy.error.NoSuchNetworkDataError(
   1172        `Network data for request id ${requestId} and DataType ${dataType} does not match collector ${collector}`
   1173      );
   1174    }
   1175 
   1176    this.#removeCollectorFromData(collectedData, collector);
   1177  }
   1178 
   1179  /**
   1180   * An object that holds information about a network data content.
   1181   *
   1182   * @typedef NetworkGetDataResult
   1183   *
   1184   * @property {BytesValue} bytes
   1185   *     The network data content as BytesValue.
   1186   */
   1187 
   1188  /**
   1189   * Retrieve a network data if available.
   1190   *
   1191   * @param {object} options
   1192   * @param {string=} options.collector
   1193   *     Optional id of a collector. If provided, data will only be returned if
   1194   *     the collector is in the network data collectors.
   1195   * @param {DataType} options.dataType
   1196   *     The type of the data to retrieve.
   1197   * @param {boolean=} options.disown
   1198   *     Optional. If set to true, the collector parameter is mandatory and the
   1199   *     collector will be removed from the network data collectors. Defaults to
   1200   *     false.
   1201   * @param {string} options.request
   1202   *     The id of the request of the data to retrieve.
   1203   *
   1204   * @returns {NetworkGetDataResult}
   1205   *
   1206   * @throws {InvalidArgumentError}
   1207   *     Raised if an argument is of an invalid type or value.
   1208   * @throws {NoSuchNetworkCollectorError}
   1209   *     Raised if the collector id could not be found in the internal collectors
   1210   *     map.
   1211   * @throws {NoSuchNetworkDataError}
   1212   *     If the network data could not be found for the provided parameters.
   1213   * @throws {UnavailableNetworkDataError}
   1214   *     If the network data content is no longer available because it was
   1215   *     evicted.
   1216   */
   1217  async getData(options = {}) {
   1218    const {
   1219      collector = null,
   1220      dataType,
   1221      disown = null,
   1222      request: requestId,
   1223    } = options;
   1224 
   1225    lazy.assert.string(
   1226      requestId,
   1227      lazy.pprint`Expected "request" to be a string, got ${requestId}`
   1228    );
   1229 
   1230    const supportedDataTypes = Object.values(DataType);
   1231    if (!supportedDataTypes.includes(dataType)) {
   1232      throw new lazy.error.InvalidArgumentError(
   1233        `Expected "dataType" to be one of ${supportedDataTypes},` +
   1234          lazy.pprint` got ${dataType}`
   1235      );
   1236    }
   1237 
   1238    if (collector !== null) {
   1239      lazy.assert.string(
   1240        collector,
   1241        lazy.pprint`Expected "collector" to be a string, got ${collector}`
   1242      );
   1243 
   1244      if (!this.#networkCollectors.has(collector)) {
   1245        throw new lazy.error.NoSuchNetworkCollectorError(
   1246          `Network data collector with id ${collector} not found`
   1247        );
   1248      }
   1249    }
   1250 
   1251    if (disown !== null) {
   1252      lazy.assert.boolean(
   1253        disown,
   1254        lazy.pprint`Expected "disown" to be a boolean, got ${disown}`
   1255      );
   1256 
   1257      if (disown && collector === null) {
   1258        throw new lazy.error.InvalidArgumentError(
   1259          `Expected "collector" to be provided when using "disown"=true`
   1260        );
   1261      }
   1262    }
   1263 
   1264    const collectedData = this.#getCollectedData(requestId, dataType);
   1265    if (!collectedData) {
   1266      throw new lazy.error.NoSuchNetworkDataError(
   1267        `Network data for request id ${requestId} and DataType ${dataType} not found`
   1268      );
   1269    }
   1270 
   1271    if (collectedData.pending) {
   1272      await collectedData.networkDataCollected.promise;
   1273    }
   1274 
   1275    if (collector !== null && !collectedData.collectors.has(collector)) {
   1276      throw new lazy.error.NoSuchNetworkDataError(
   1277        `Network data for request id ${requestId} and DataType ${dataType} does not match collector ${collector}`
   1278      );
   1279    }
   1280 
   1281    if (collectedData.bytes === null) {
   1282      const reason = collectedData.unavailableReason;
   1283      throw new lazy.error.UnavailableNetworkDataError(
   1284        `Network data content for request id ${requestId} and DataType ${dataType} is unavailable (reason: ${reason})`
   1285      );
   1286    }
   1287 
   1288    const value = await collectedData.bytes.getBytesValue();
   1289    const type = collectedData.bytes.isBase64
   1290      ? BytesValueType.Base64
   1291      : BytesValueType.String;
   1292 
   1293    if (disown) {
   1294      this.#removeCollectorFromData(collectedData, collector);
   1295    }
   1296 
   1297    return { bytes: this.#serializeAsBytesValue(value, type) };
   1298  }
   1299 
   1300  /**
   1301   * Fails a request that is blocked by a network intercept.
   1302   *
   1303   * @param {object=} options
   1304   * @param {string} options.request
   1305   *     The id of the blocked request that should be continued.
   1306   *
   1307   * @throws {InvalidArgumentError}
   1308   *     Raised if an argument is of an invalid type or value.
   1309   * @throws {NoSuchRequestError}
   1310   *     Raised if the request id does not match any request in the blocked
   1311   *     requests map.
   1312   */
   1313  async failRequest(options = {}) {
   1314    const { request: requestId } = options;
   1315 
   1316    lazy.assert.string(
   1317      requestId,
   1318      `Expected "request" to be a string, got ${requestId}`
   1319    );
   1320 
   1321    if (!this.#blockedRequests.has(requestId)) {
   1322      throw new lazy.error.NoSuchRequestError(
   1323        `Blocked request with id ${requestId} not found`
   1324      );
   1325    }
   1326 
   1327    const { phase, request, resolveBlockedEvent } =
   1328      this.#blockedRequests.get(requestId);
   1329 
   1330    if (phase === InterceptPhase.AuthRequired) {
   1331      throw new lazy.error.InvalidArgumentError(
   1332        `Expected blocked request not to be in "authRequired" phase`
   1333      );
   1334    }
   1335 
   1336    request.wrappedChannel.resume();
   1337    request.wrappedChannel.cancel(
   1338      Cr.NS_ERROR_ABORT,
   1339      Ci.nsILoadInfo.BLOCKING_REASON_WEBDRIVER_BIDI
   1340    );
   1341 
   1342    resolveBlockedEvent();
   1343  }
   1344 
   1345  /**
   1346   * Continues a request that’s blocked by a network intercept, by providing a
   1347   * complete response.
   1348   *
   1349   * @param {object=} options
   1350   * @param {string} options.request
   1351   *     The id of the blocked request for which the response should be
   1352   *     provided.
   1353   * @param {BytesValue=} options.body
   1354   *     Optional BytesValue to replace the body of the response.
   1355   *     For now, only supported for requests blocked in beforeRequestSent.
   1356   * @param {Array<SetCookieHeader>=} options.cookies
   1357   *     Optional array of set-cookie header values to use for the provided
   1358   *     response.
   1359   *     For now, only supported for requests blocked in beforeRequestSent.
   1360   * @param {Array<Header>=} options.headers
   1361   *     Optional array of header values to use for the provided
   1362   *     response.
   1363   *     For now, only supported for requests blocked in beforeRequestSent.
   1364   * @param {string=} options.reasonPhrase
   1365   *     Optional string to use as the status message for the provided response.
   1366   *     For now, only supported for requests blocked in beforeRequestSent.
   1367   * @param {number=} options.statusCode
   1368   *     Optional number to use as the status code for the provided response.
   1369   *     For now, only supported for requests blocked in beforeRequestSent.
   1370   *
   1371   * @throws {InvalidArgumentError}
   1372   *     Raised if an argument is of an invalid type or value.
   1373   * @throws {NoSuchRequestError}
   1374   *     Raised if the request id does not match any request in the blocked
   1375   *     requests map.
   1376   */
   1377  async provideResponse(options = {}) {
   1378    const {
   1379      body = null,
   1380      cookies = null,
   1381      headers = null,
   1382      reasonPhrase = null,
   1383      request: requestId,
   1384      statusCode = null,
   1385    } = options;
   1386 
   1387    lazy.assert.string(
   1388      requestId,
   1389      `Expected "request" to be a string, got ${requestId}`
   1390    );
   1391 
   1392    if (body !== null) {
   1393      this.#assertBytesValue(
   1394        body,
   1395        `Expected "body" to be a network.BytesValue, got ${body}`
   1396      );
   1397    }
   1398 
   1399    if (cookies !== null) {
   1400      lazy.assert.array(
   1401        cookies,
   1402        `Expected "cookies" to be an array got ${cookies}`
   1403      );
   1404 
   1405      for (const cookie of cookies) {
   1406        this.#assertSetCookieHeader(cookie);
   1407      }
   1408    }
   1409 
   1410    let deserializedHeaders = [];
   1411    if (headers !== null) {
   1412      deserializedHeaders = this.#deserializeHeaders(headers);
   1413    }
   1414 
   1415    if (reasonPhrase !== null) {
   1416      lazy.assert.string(
   1417        reasonPhrase,
   1418        `Expected "reasonPhrase" to be a string, got ${reasonPhrase}`
   1419      );
   1420    }
   1421 
   1422    if (statusCode !== null) {
   1423      lazy.assert.positiveInteger(
   1424        statusCode,
   1425        `Expected "statusCode" to be a positive integer, got ${statusCode}`
   1426      );
   1427    }
   1428 
   1429    if (!this.#blockedRequests.has(requestId)) {
   1430      throw new lazy.error.NoSuchRequestError(
   1431        `Blocked request with id ${requestId} not found`
   1432      );
   1433    }
   1434 
   1435    const { authCallbacks, phase, request, resolveBlockedEvent } =
   1436      this.#blockedRequests.get(requestId);
   1437 
   1438    // Handle optional arguments for the beforeRequestSent phase.
   1439    // TODO: Support optional arguments in all phases, see Bug 1901055.
   1440    if (phase === InterceptPhase.BeforeRequestSent) {
   1441      // Create a new response.
   1442      const replacedHttpResponse = Cc[
   1443        "@mozilla.org/network/replaced-http-response;1"
   1444      ].createInstance(Ci.nsIReplacedHttpResponse);
   1445 
   1446      if (statusCode !== null) {
   1447        replacedHttpResponse.responseStatus = statusCode;
   1448      }
   1449 
   1450      if (reasonPhrase !== null) {
   1451        replacedHttpResponse.responseStatusText = reasonPhrase;
   1452      }
   1453 
   1454      if (body !== null) {
   1455        replacedHttpResponse.responseBody = deserializeBytesValue(body);
   1456      }
   1457 
   1458      if (headers !== null) {
   1459        for (const [name, value] of deserializedHeaders) {
   1460          replacedHttpResponse.setResponseHeader(name, value, true);
   1461        }
   1462      }
   1463 
   1464      if (cookies !== null) {
   1465        for (const cookie of cookies) {
   1466          const headerValue = this.#serializeSetCookieHeader(cookie);
   1467          replacedHttpResponse.setResponseHeader(
   1468            "Set-Cookie",
   1469            headerValue,
   1470            true
   1471          );
   1472        }
   1473      }
   1474 
   1475      request.setResponseOverride(replacedHttpResponse);
   1476      request.wrappedChannel.resume();
   1477    } else {
   1478      if (body !== null) {
   1479        throw new lazy.error.UnsupportedOperationError(
   1480          `The "body" parameter is only supported for the beforeRequestSent phase at the moment`
   1481        );
   1482      }
   1483 
   1484      if (cookies !== null) {
   1485        throw new lazy.error.UnsupportedOperationError(
   1486          `The "cookies" parameter is only supported for the beforeRequestSent phase at the moment`
   1487        );
   1488      }
   1489 
   1490      if (headers !== null) {
   1491        throw new lazy.error.UnsupportedOperationError(
   1492          `The "headers" parameter is only supported for the beforeRequestSent phase at the moment`
   1493        );
   1494      }
   1495 
   1496      if (reasonPhrase !== null) {
   1497        throw new lazy.error.UnsupportedOperationError(
   1498          `The "reasonPhrase" parameter is only supported for the beforeRequestSent phase at the moment`
   1499        );
   1500      }
   1501 
   1502      if (statusCode !== null) {
   1503        throw new lazy.error.UnsupportedOperationError(
   1504          `The "statusCode" parameter is only supported for the beforeRequestSent phase at the moment`
   1505        );
   1506      }
   1507 
   1508      if (phase === InterceptPhase.AuthRequired) {
   1509        // AuthRequired with no optional argument, resume the authentication.
   1510        await authCallbacks.provideAuthCredentials();
   1511      } else {
   1512        // Any phase other than AuthRequired with no optional argument, resume the
   1513        // request.
   1514        request.wrappedChannel.resume();
   1515      }
   1516    }
   1517 
   1518    resolveBlockedEvent();
   1519  }
   1520 
   1521  /**
   1522   * Removes a data collector.
   1523   *
   1524   * @param {object=} options
   1525   * @param {string} options.collector
   1526   *     The id of the collector to remove.
   1527   *
   1528   * @throws {InvalidArgumentError}
   1529   *     Raised if an argument is of an invalid type or value.
   1530   * @throws {NoSuchNetworkCollectorError}
   1531   *     Raised if the collector id could not be found in the internal collectors
   1532   *     map.
   1533   */
   1534  removeDataCollector(options = {}) {
   1535    const { collector } = options;
   1536 
   1537    lazy.assert.string(
   1538      collector,
   1539      lazy.pprint`Expected "collector" to be a string, got ${collector}`
   1540    );
   1541 
   1542    if (!this.#networkCollectors.has(collector)) {
   1543      throw new lazy.error.NoSuchNetworkCollectorError(
   1544        `Network data collector with id ${collector} not found`
   1545      );
   1546    }
   1547 
   1548    this.#networkCollectors.delete(collector);
   1549 
   1550    for (const [, collectedData] of this.#collectedNetworkData) {
   1551      this.#removeCollectorFromData(collectedData, collector);
   1552    }
   1553  }
   1554 
   1555  /**
   1556   * Removes an existing network intercept.
   1557   *
   1558   * @param {object=} options
   1559   * @param {string} options.intercept
   1560   *     The id of the intercept to remove.
   1561   *
   1562   * @throws {InvalidArgumentError}
   1563   *     Raised if an argument is of an invalid type or value.
   1564   * @throws {NoSuchInterceptError}
   1565   *     Raised if the intercept id could not be found in the internal intercept
   1566   *     map.
   1567   */
   1568  removeIntercept(options = {}) {
   1569    const { intercept } = options;
   1570 
   1571    lazy.assert.string(
   1572      intercept,
   1573      `Expected "intercept" to be a string, got ${intercept}`
   1574    );
   1575 
   1576    if (!this.#interceptMap.has(intercept)) {
   1577      throw new lazy.error.NoSuchInterceptError(
   1578        `Network intercept with id ${intercept} not found`
   1579      );
   1580    }
   1581 
   1582    this.#interceptMap.delete(intercept);
   1583  }
   1584 
   1585  /**
   1586   * Configures the network cache behavior for certain requests.
   1587   *
   1588   * @param {object=} options
   1589   * @param {CacheBehavior} options.cacheBehavior
   1590   *     An enum value to set the network cache behavior.
   1591   * @param {Array<string>=} options.contexts
   1592   *     The list of browsing context ids where the network cache
   1593   *     behavior should be updated.
   1594   *
   1595   * @throws {InvalidArgumentError}
   1596   *     Raised if an argument is of an invalid type or value.
   1597   * @throws {NoSuchFrameError}
   1598   *     If the browsing context cannot be found.
   1599   */
   1600  setCacheBehavior(options = {}) {
   1601    const { cacheBehavior: behavior, contexts: contextIds = null } = options;
   1602 
   1603    if (!Object.values(lazy.CacheBehavior).includes(behavior)) {
   1604      throw new lazy.error.InvalidArgumentError(
   1605        `Expected "cacheBehavior" to be one of ${Object.values(
   1606          lazy.CacheBehavior
   1607        )}` + lazy.pprint` got ${behavior}`
   1608      );
   1609    }
   1610 
   1611    if (contextIds === null) {
   1612      // Set the default behavior if no specific context is specified.
   1613      lazy.updateCacheBehavior(behavior);
   1614      return;
   1615    }
   1616 
   1617    lazy.assert.isNonEmptyArray(
   1618      contextIds,
   1619      lazy.pprint`Expected "contexts" to be a non-empty array, got ${contextIds}`
   1620    );
   1621 
   1622    const contexts = new Set();
   1623    for (const contextId of contextIds) {
   1624      lazy.assert.string(
   1625        contextId,
   1626        lazy.pprint`Expected elements of "contexts" to be a string, got ${contextId}`
   1627      );
   1628      const context = this._getNavigable(contextId);
   1629 
   1630      lazy.assert.topLevel(
   1631        context,
   1632        lazy.pprint`Browsing context with id ${contextId} is not top-level`
   1633      );
   1634 
   1635      contexts.add(context);
   1636    }
   1637 
   1638    lazy.updateCacheBehavior(behavior, contexts);
   1639  }
   1640 
   1641  /**
   1642   * Allows to specify headers that will extend, or overwrite, existing request
   1643   * headers.
   1644   *
   1645   * @param {object=} options
   1646   * @param {Array<Header>} options.headers
   1647   *     Array of header values to replace the headers of the response.
   1648   * @param {Array<string>=} options.contexts
   1649   *     Optional list of browsing context ids.
   1650   * @param {Array<string>=} options.userContexts
   1651   *     Optional list of user context ids.
   1652   *
   1653   * @throws {InvalidArgumentError}
   1654   *     Raised if an argument is of an invalid type or value.
   1655   * @throws {NoSuchFrameError}
   1656   *     If the browsing context cannot be found.
   1657   */
   1658  setExtraHeaders(options = {}) {
   1659    const {
   1660      headers,
   1661      contexts: contextIds = NULL,
   1662      userContexts: userContextIds = NULL,
   1663    } = options;
   1664 
   1665    lazy.assert.array(
   1666      headers,
   1667      lazy.pprint`Expected "headers" to be an array, got ${headers}`
   1668    );
   1669 
   1670    const deserializedHeaders = this.#deserializeHeaders(headers);
   1671 
   1672    if (contextIds !== NULL && userContextIds !== NULL) {
   1673      throw new lazy.error.InvalidArgumentError(
   1674        `Providing both "contexts" and "userContexts" arguments is not supported`
   1675      );
   1676    }
   1677 
   1678    const navigables = new Set();
   1679    const userContexts = new Set();
   1680    if (userContextIds !== NULL) {
   1681      lazy.assert.isNonEmptyArray(
   1682        userContextIds,
   1683        lazy.pprint`Expected "userContexts" to be a non-empty array, got ${userContextIds}`
   1684      );
   1685 
   1686      for (const userContextId of userContextIds) {
   1687        lazy.assert.string(
   1688          userContextId,
   1689          lazy.pprint`Expected elements of "userContexts" to be a string, got ${userContextId}`
   1690        );
   1691 
   1692        const internalId =
   1693          lazy.UserContextManager.getInternalIdById(userContextId);
   1694 
   1695        if (internalId === null) {
   1696          throw new lazy.error.NoSuchUserContextError(
   1697            `User context with id: ${userContextId} doesn't exist`
   1698          );
   1699        }
   1700 
   1701        userContexts.add(userContextId);
   1702      }
   1703    }
   1704 
   1705    if (contextIds !== NULL) {
   1706      lazy.assert.isNonEmptyArray(
   1707        contextIds,
   1708        lazy.pprint`Expected "contexts" to be a non-empty array, got ${contextIds}`
   1709      );
   1710 
   1711      for (const contextId of contextIds) {
   1712        lazy.assert.string(
   1713          contextId,
   1714          lazy.pprint`Expected elements of "contexts" to be a string, got ${contextId}`
   1715        );
   1716        const context = this._getNavigable(contextId);
   1717 
   1718        lazy.assert.topLevel(
   1719          context,
   1720          lazy.pprint`Browsing context with id ${contextId} is not top-level`
   1721        );
   1722 
   1723        navigables.add(contextId);
   1724      }
   1725    }
   1726 
   1727    if (userContextIds !== NULL) {
   1728      for (const userContextId of userContexts) {
   1729        this.#extraHeaders.userContextHeaders.set(
   1730          userContextId,
   1731          deserializedHeaders
   1732        );
   1733      }
   1734    } else if (contextIds !== NULL) {
   1735      for (const contextId of navigables) {
   1736        const context = this._getNavigable(contextId);
   1737        this.#extraHeaders.navigableHeaders.set(
   1738          context.webProgress,
   1739          deserializedHeaders
   1740        );
   1741      }
   1742    } else {
   1743      this.#extraHeaders.defaultHeaders = deserializedHeaders;
   1744    }
   1745 
   1746    this.#networkListener.startListening();
   1747  }
   1748 
   1749  /**
   1750   * Add a new request in the blockedRequests map.
   1751   *
   1752   * @param {string} requestId
   1753   *     The request id.
   1754   * @param {InterceptPhase} phase
   1755   *     The phase where the request is blocked.
   1756   * @param {object=} options
   1757   * @param {object=} options.authCallbacks
   1758   *     Only defined for requests blocked in the authRequired phase.
   1759   *     Provides callbacks to handle the authentication.
   1760   * @param {nsIChannel=} options.requestChannel
   1761   *     The request channel.
   1762   * @param {nsIChannel=} options.responseChannel
   1763   *     The response channel.
   1764   */
   1765  #addBlockedRequest(requestId, phase, options = {}) {
   1766    const { authCallbacks, request, response } = options;
   1767    const { promise: blockedEventPromise, resolve: resolveBlockedEvent } =
   1768      Promise.withResolvers();
   1769 
   1770    this.#blockedRequests.set(requestId, {
   1771      authCallbacks,
   1772      request,
   1773      response,
   1774      resolveBlockedEvent,
   1775      phase,
   1776    });
   1777 
   1778    blockedEventPromise.finally(() => {
   1779      this.#blockedRequests.delete(requestId);
   1780    });
   1781  }
   1782 
   1783  /**
   1784   * Implements https://w3c.github.io/webdriver-bidi/#allocate-size-to-record-data
   1785   *
   1786   * @param {number} size
   1787   *     The size to allocate in bytes.
   1788   */
   1789  #allocateSizeToRecordData(size) {
   1790    let availableSize = lazy.maxTotalDataSize;
   1791    const alreadyCollectedData = [];
   1792    for (const [, collectedData] of this.#collectedNetworkData) {
   1793      if (collectedData.bytes !== null) {
   1794        availableSize = availableSize - collectedData.size;
   1795        alreadyCollectedData.push(collectedData);
   1796      }
   1797    }
   1798 
   1799    if (size > availableSize) {
   1800      for (const collectedData of alreadyCollectedData) {
   1801        availableSize = availableSize + collectedData.size;
   1802        collectedData.bytes = null;
   1803        collectedData.unavailableReason = UNAVAILABLE_DATA_ERROR_REASON.Evicted;
   1804        collectedData.size = null;
   1805 
   1806        if (size <= availableSize) {
   1807          return;
   1808        }
   1809      }
   1810    }
   1811  }
   1812 
   1813  #assertAuthCredentials(credentials) {
   1814    lazy.assert.object(
   1815      credentials,
   1816      `Expected "credentials" to be an object, got ${credentials}`
   1817    );
   1818 
   1819    if (credentials.type !== "password") {
   1820      throw new lazy.error.InvalidArgumentError(
   1821        `Expected credentials "type" to be "password" got ${credentials.type}`
   1822      );
   1823    }
   1824 
   1825    lazy.assert.string(
   1826      credentials.username,
   1827      `Expected credentials "username" to be a string, got ${credentials.username}`
   1828    );
   1829    lazy.assert.string(
   1830      credentials.password,
   1831      `Expected credentials "password" to be a string, got ${credentials.password}`
   1832    );
   1833  }
   1834 
   1835  #assertBytesValue(obj, msg) {
   1836    lazy.assert.object(obj, msg);
   1837    lazy.assert.string(obj.value, msg);
   1838    lazy.assert.in(obj.type, Object.values(BytesValueType), msg);
   1839  }
   1840 
   1841  #assertHeader(value, msg) {
   1842    lazy.assert.object(value, msg);
   1843    lazy.assert.string(value.name, msg);
   1844    this.#assertBytesValue(value.value, msg);
   1845  }
   1846 
   1847  #assertSetCookieHeader(setCookieHeader) {
   1848    lazy.assert.object(
   1849      setCookieHeader,
   1850      `Expected set-cookie header to be an object, got ${setCookieHeader}`
   1851    );
   1852 
   1853    const {
   1854      name,
   1855      value,
   1856      domain = null,
   1857      httpOnly = null,
   1858      expiry = null,
   1859      maxAge = null,
   1860      path = null,
   1861      sameSite = null,
   1862      secure = null,
   1863    } = setCookieHeader;
   1864 
   1865    lazy.assert.string(
   1866      name,
   1867      `Expected set-cookie header "name" to be a string, got ${name}`
   1868    );
   1869 
   1870    this.#assertBytesValue(
   1871      value,
   1872      `Expected set-cookie header "value" to be a BytesValue, got ${name}`
   1873    );
   1874 
   1875    if (domain !== null) {
   1876      lazy.assert.string(
   1877        domain,
   1878        `Expected set-cookie header "domain" to be a string, got ${domain}`
   1879      );
   1880    }
   1881    if (httpOnly !== null) {
   1882      lazy.assert.boolean(
   1883        httpOnly,
   1884        `Expected set-cookie header "httpOnly" to be a boolean, got ${httpOnly}`
   1885      );
   1886    }
   1887    if (expiry !== null) {
   1888      lazy.assert.string(
   1889        expiry,
   1890        `Expected set-cookie header "expiry" to be a string, got ${expiry}`
   1891      );
   1892    }
   1893    if (maxAge !== null) {
   1894      lazy.assert.integer(
   1895        maxAge,
   1896        `Expected set-cookie header "maxAge" to be an integer, got ${maxAge}`
   1897      );
   1898    }
   1899    if (path !== null) {
   1900      lazy.assert.string(
   1901        path,
   1902        `Expected set-cookie header "path" to be a string, got ${path}`
   1903      );
   1904    }
   1905    if (sameSite !== null) {
   1906      lazy.assert.in(
   1907        sameSite,
   1908        Object.values(SameSite),
   1909        `Expected set-cookie header "sameSite" to be one of ${Object.values(
   1910          SameSite
   1911        )}, got ${sameSite}`
   1912      );
   1913    }
   1914    if (secure !== null) {
   1915      lazy.assert.boolean(
   1916        secure,
   1917        `Expected set-cookie header "secure" to be a boolean, got ${secure}`
   1918      );
   1919    }
   1920  }
   1921 
   1922  #cloneNetworkRequestBody(request) {
   1923    if (!this.#networkCollectors.size) {
   1924      return;
   1925    }
   1926 
   1927    // If request body is missing or null, do not store any collected data.
   1928    if (!request.postData || request.postData === null) {
   1929      return;
   1930    }
   1931 
   1932    const collectedData = {
   1933      bytes: null,
   1934      collectors: new Set(),
   1935      pending: true,
   1936      // This allows to implement the await/resume on "network data collected"
   1937      // described in the specification.
   1938      networkDataCollected: Promise.withResolvers(),
   1939      request: request.requestId,
   1940      size: null,
   1941      type: DataType.Request,
   1942    };
   1943 
   1944    // The actual cloning is already handled by the DevTools
   1945    // NetworkResponseListener, here we just have to prepare the networkData and
   1946    // add it to the array.
   1947    this.#collectedNetworkData.set(
   1948      `${request.requestId}-${DataType.Request}`,
   1949      collectedData
   1950    );
   1951  }
   1952 
   1953  #cloneNetworkResponseBody(request) {
   1954    if (!this.#networkCollectors.size) {
   1955      return;
   1956    }
   1957 
   1958    const collectedData = {
   1959      bytes: null,
   1960      // The cloned body is fully handled by DevTools' NetworkResponseListener
   1961      // so it will not explicitly be stored here.
   1962      collectors: new Set(),
   1963      pending: true,
   1964      // This allows to implement the await/resume on "network data collected"
   1965      // described in the specification.
   1966      networkDataCollected: Promise.withResolvers(),
   1967      request: request.requestId,
   1968      size: null,
   1969      type: DataType.Response,
   1970      // Internal string used in the UnavailableNetworkData error message.
   1971      unavailableReason: null,
   1972    };
   1973 
   1974    // The actual cloning is already handled by the DevTools
   1975    // NetworkResponseListener, here we just have to prepare the networkData and
   1976    // add it to the array.
   1977    this.#collectedNetworkData.set(
   1978      `${request.requestId}-${DataType.Response}`,
   1979      collectedData
   1980    );
   1981  }
   1982 
   1983  #deserializeHeader(protocolHeader) {
   1984    const name = protocolHeader.name;
   1985    const value = deserializeBytesValue(protocolHeader.value);
   1986    return [name, value];
   1987  }
   1988 
   1989  #deserializeHeaders(headers) {
   1990    const deserializedHeaders = [];
   1991    lazy.assert.array(
   1992      headers,
   1993      lazy.pprint`Expected "headers" to be an array got ${headers}`
   1994    );
   1995 
   1996    for (const header of headers) {
   1997      this.#assertHeader(
   1998        header,
   1999        lazy.pprint`Expected values in "headers" to be network.Header, got ${header}`
   2000      );
   2001 
   2002      // Deserialize headers immediately to validate the value
   2003      const deserializedHeader = this.#deserializeHeader(header);
   2004      lazy.assert.that(
   2005        value => this.#isValidHttpToken(value),
   2006        lazy.pprint`Expected "header" name to be a valid HTTP token, got ${deserializedHeader[0]}`
   2007      )(deserializedHeader[0]);
   2008      lazy.assert.that(
   2009        value => this.#isValidHeaderValue(value),
   2010        lazy.pprint`Expected "header" value to be a valid header value, got ${deserializedHeader[1]}`
   2011      )(deserializedHeader[1]);
   2012 
   2013      deserializedHeaders.push(deserializedHeader);
   2014    }
   2015 
   2016    return deserializedHeaders;
   2017  }
   2018 
   2019  #extractChallenges(response) {
   2020    let headerName;
   2021 
   2022    // Using case-insensitive match for header names, so we use the lowercase
   2023    // version of the "WWW-Authenticate" / "Proxy-Authenticate" strings.
   2024    if (response.status === 401) {
   2025      headerName = "www-authenticate";
   2026    } else if (response.status === 407) {
   2027      headerName = "proxy-authenticate";
   2028    } else {
   2029      return null;
   2030    }
   2031 
   2032    const challenges = [];
   2033    for (const [name, value] of response.headers) {
   2034      if (name.toLowerCase() === headerName) {
   2035        // A single header can contain several challenges.
   2036        const headerChallenges = lazy.parseChallengeHeader(value);
   2037        for (const headerChallenge of headerChallenges) {
   2038          const realmParam = headerChallenge.params.find(
   2039            param => param.name == "realm"
   2040          );
   2041          const realm = realmParam ? realmParam.value : undefined;
   2042          const challenge = {
   2043            scheme: headerChallenge.scheme,
   2044            realm,
   2045          };
   2046          challenges.push(challenge);
   2047        }
   2048      }
   2049    }
   2050 
   2051    return challenges;
   2052  }
   2053 
   2054  #getCollectedData(requestId, dataType) {
   2055    return this.#collectedNetworkData.get(`${requestId}-${dataType}`) || null;
   2056  }
   2057 
   2058  #getNetworkIntercepts(event, request, topContextId) {
   2059    if (!request.supportsInterception) {
   2060      // For requests which do not support interception (such as data URIs or
   2061      // cached resources), do not attempt to match intercepts.
   2062      return [];
   2063    }
   2064 
   2065    const intercepts = [];
   2066 
   2067    let phase;
   2068    switch (event) {
   2069      case "network.beforeRequestSent":
   2070        phase = InterceptPhase.BeforeRequestSent;
   2071        break;
   2072      case "network.responseStarted":
   2073        phase = InterceptPhase.ResponseStarted;
   2074        break;
   2075      case "network.authRequired":
   2076        phase = InterceptPhase.AuthRequired;
   2077        break;
   2078      case "network.responseCompleted":
   2079        // The network.responseCompleted event does not match any interception
   2080        // phase. Return immediately.
   2081        return intercepts;
   2082    }
   2083 
   2084    const url = request.serializedURL;
   2085    for (const [interceptId, intercept] of this.#interceptMap) {
   2086      if (
   2087        intercept.contexts !== null &&
   2088        !intercept.contexts.includes(topContextId)
   2089      ) {
   2090        // Skip this intercept if the event's context does not match the list
   2091        // of contexts for this intercept.
   2092        continue;
   2093      }
   2094 
   2095      if (intercept.phases.includes(phase)) {
   2096        const urlPatterns = intercept.urlPatterns;
   2097        if (
   2098          !urlPatterns.length ||
   2099          urlPatterns.some(pattern => lazy.matchURLPattern(pattern, url))
   2100        ) {
   2101          intercepts.push(interceptId);
   2102        }
   2103      }
   2104    }
   2105 
   2106    return intercepts;
   2107  }
   2108 
   2109  #getRequestData(request) {
   2110    const requestId = request.requestId;
   2111 
   2112    // "Let url be the result of running the URL serializer with request’s URL"
   2113    // request.serializedURL is already serialized.
   2114    const url = request.serializedURL;
   2115    const method = request.method;
   2116 
   2117    const bodySize = request.postDataSize;
   2118    const headersSize = request.headersSize;
   2119    const headers = [];
   2120    const cookies = [];
   2121 
   2122    for (const [name, value] of request.headers) {
   2123      headers.push(this.#serializeHeader(name, value));
   2124      if (name.toLowerCase() == "cookie") {
   2125        // TODO: Retrieve the actual cookies from the cookie store.
   2126        const headerCookies = value.split(";");
   2127        for (const cookie of headerCookies) {
   2128          const equal = cookie.indexOf("=");
   2129          const cookieName = cookie.substr(0, equal);
   2130          const cookieValue = cookie.substr(equal + 1);
   2131          const serializedCookie = this.#serializeHeader(
   2132            unescape(cookieName.trim()),
   2133            unescape(cookieValue.trim())
   2134          );
   2135          cookies.push(serializedCookie);
   2136        }
   2137      }
   2138    }
   2139 
   2140    const destination = request.destination;
   2141    const initiatorType = request.initiatorType;
   2142    const timings = request.timings;
   2143 
   2144    return {
   2145      request: requestId,
   2146      url,
   2147      method,
   2148      bodySize,
   2149      headersSize,
   2150      headers,
   2151      cookies,
   2152      destination,
   2153      initiatorType,
   2154      timings,
   2155    };
   2156  }
   2157 
   2158  #getResponseContentInfo(response) {
   2159    return {
   2160      size: response.decodedBodySize,
   2161    };
   2162  }
   2163 
   2164  #getResponseData(response) {
   2165    const url = response.serializedURL;
   2166    const protocol = response.protocol;
   2167    const status = response.status;
   2168    const statusText = response.statusMessage;
   2169    // TODO: Ideally we should have a `isCacheStateLocal` getter
   2170    // const fromCache = response.isCacheStateLocal();
   2171    const fromCache = response.fromCache;
   2172    const mimeType = response.mimeType;
   2173    const headers = [];
   2174    for (const [name, value] of response.headers) {
   2175      headers.push(this.#serializeHeader(name, value));
   2176    }
   2177 
   2178    const bytesReceived = response.totalTransmittedSize;
   2179    const headersSize = response.headersTransmittedSize;
   2180    const bodySize = response.encodedBodySize;
   2181    const content = this.#getResponseContentInfo(response);
   2182    const authChallenges = this.#extractChallenges(response);
   2183 
   2184    const params = {
   2185      url,
   2186      protocol,
   2187      status,
   2188      statusText,
   2189      fromCache,
   2190      headers,
   2191      mimeType,
   2192      bytesReceived,
   2193      headersSize,
   2194      bodySize,
   2195      content,
   2196    };
   2197 
   2198    if (authChallenges !== null) {
   2199      params.authChallenges = authChallenges;
   2200    }
   2201 
   2202    return params;
   2203  }
   2204 
   2205  #getSuspendMarkerText(requestData, phase) {
   2206    return `Request (id: ${requestData.request}) suspended by WebDriver BiDi in ${phase} phase`;
   2207  }
   2208 
   2209  #isValidHeaderValue(value) {
   2210    if (!value.length) {
   2211      return true;
   2212    }
   2213 
   2214    // For non-empty strings check against:
   2215    // - leading or trailing tabs & spaces
   2216    // - new lines and null bytes
   2217    const chars = value.split("");
   2218    const tabOrSpace = [" ", "\t"];
   2219    const forbiddenChars = ["\r", "\n", "\0"];
   2220    return (
   2221      !tabOrSpace.includes(chars.at(0)) &&
   2222      !tabOrSpace.includes(chars.at(-1)) &&
   2223      forbiddenChars.every(c => !chars.includes(c))
   2224    );
   2225  }
   2226 
   2227  /**
   2228   * This helper is adapted from a C++ validation helper in nsHttp.cpp.
   2229   *
   2230   * @see https://searchfox.org/mozilla-central/rev/445a6e86233c733c5557ef44e1d33444adaddefc/netwerk/protocol/http/nsHttp.cpp#169
   2231   */
   2232  #isValidHttpToken(token) {
   2233    // prettier-ignore
   2234    // This array corresponds to all char codes between 0 and 127, which is the
   2235    // range of supported char codes for HTTP tokens. Within this range,
   2236    // accepted char codes are marked with a 1, forbidden char codes with a 0.
   2237    const validTokenMap = [
   2238      0, 0, 0, 0, 0, 0, 0, 0,  //   0
   2239      0, 0, 0, 0, 0, 0, 0, 0,  //   8
   2240      0, 0, 0, 0, 0, 0, 0, 0,  //  16
   2241      0, 0, 0, 0, 0, 0, 0, 0,  //  24
   2242 
   2243      0, 1, 0, 1, 1, 1, 1, 1,  //  32
   2244      0, 0, 1, 1, 0, 1, 1, 0,  //  40
   2245      1, 1, 1, 1, 1, 1, 1, 1,  //  48
   2246      1, 1, 0, 0, 0, 0, 0, 0,  //  56
   2247 
   2248      0, 1, 1, 1, 1, 1, 1, 1,  //  64
   2249      1, 1, 1, 1, 1, 1, 1, 1,  //  72
   2250      1, 1, 1, 1, 1, 1, 1, 1,  //  80
   2251      1, 1, 1, 0, 0, 0, 1, 1,  //  88
   2252 
   2253      1, 1, 1, 1, 1, 1, 1, 1,  //  96
   2254      1, 1, 1, 1, 1, 1, 1, 1,  // 104
   2255      1, 1, 1, 1, 1, 1, 1, 1,  // 112
   2256      1, 1, 1, 0, 1, 0, 1, 0   // 120
   2257    ];
   2258 
   2259    if (!token.length) {
   2260      return false;
   2261    }
   2262    return token
   2263      .split("")
   2264      .map(s => s.charCodeAt(0))
   2265      .every(c => validTokenMap[c]);
   2266  }
   2267 
   2268  /**
   2269   * Implements https://w3c.github.io/webdriver-bidi/#match-collector-for-navigable
   2270   *
   2271   * @param {Collector} collector
   2272   *     The collector to match
   2273   * @param {BrowsingContext} navigable
   2274   *     The navigable (BrowsingContext) to match
   2275   * @returns {boolean}
   2276   *     True if the collector corresponds to the provided navigable. False
   2277   *     otherwise.
   2278   */
   2279  #matchCollectorForNavigable(collector, navigable) {
   2280    if (collector.contexts.size) {
   2281      const navigableId =
   2282        lazy.NavigableManager.getIdForBrowsingContext(navigable);
   2283      return collector.contexts.has(navigableId);
   2284    }
   2285 
   2286    if (collector.userContexts.size) {
   2287      const userContext =
   2288        lazy.UserContextManager.getIdByBrowsingContext(navigable);
   2289      return collector.userContexts.has(userContext);
   2290    }
   2291 
   2292    // Return true.
   2293    return true;
   2294  }
   2295 
   2296  /**
   2297   * Implements https://w3c.github.io/webdriver-bidi/#maybe-abort-network-response-body-collection
   2298   *
   2299   * @param {NetworkRequest} request
   2300   *     The request object for which we want to abort the body collection.
   2301   */
   2302  #maybeAbortNetworkResponseBodyCollection(request) {
   2303    const collectedData = this.#getCollectedData(
   2304      request.requestId,
   2305      DataType.Response
   2306    );
   2307    if (collectedData === null) {
   2308      return;
   2309    }
   2310 
   2311    lazy.logger.trace(
   2312      `Network data not collected for request "${request.requestId}" and data type "${DataType.Response}"` +
   2313        `: fetch error`
   2314    );
   2315    collectedData.pending = false;
   2316    collectedData.unavailableReason = UNAVAILABLE_DATA_ERROR_REASON.Aborted;
   2317    collectedData.networkDataCollected.resolve();
   2318  }
   2319 
   2320  /**
   2321   * Implements https://w3c.github.io/webdriver-bidi/#maybe-collect-network-request-body
   2322   *
   2323   * @param {NetworkRequest} request
   2324   *     The request object for which we want to collect the body.
   2325   */
   2326  async #maybeCollectNetworkRequestBody(request) {
   2327    const collectedData = this.#getCollectedData(
   2328      request.requestId,
   2329      DataType.Request
   2330    );
   2331 
   2332    if (collectedData === null) {
   2333      return;
   2334    }
   2335 
   2336    this.#maybeCollectNetworkData({
   2337      collectedData,
   2338      dataType: DataType.Request,
   2339      request,
   2340      readAndProcessBodyFn: request.readAndProcessRequestBody,
   2341      size: request.postDataSize,
   2342    });
   2343  }
   2344 
   2345  /**
   2346   * Implements https://www.w3.org/TR/webdriver-bidi/#maybe-collect-network-response-body
   2347   *
   2348   * @param {NetworkRequest} request
   2349   *     The request object for which we want to collect the body.
   2350   * @param {NetworkResponse} response
   2351   *     The response object for which we want to collect the body.
   2352   */
   2353  async #maybeCollectNetworkResponseBody(request, response) {
   2354    if (response.willRedirect) {
   2355      return;
   2356    }
   2357 
   2358    const collectedData = this.#getCollectedData(
   2359      request.requestId,
   2360      DataType.Response
   2361    );
   2362 
   2363    if (collectedData === null) {
   2364      return;
   2365    }
   2366 
   2367    if (!(response instanceof lazy.NetworkResponse) && !response.isDataURL) {
   2368      lazy.logger.trace(
   2369        `Network data not collected for request "${request.requestId}" and data type "${DataType.Response}"` +
   2370          `: unsupported response (read from memory cache)`
   2371      );
   2372      // Cached stencils do not return any response body.
   2373      collectedData.pending = false;
   2374      collectedData.networkDataCollected.resolve();
   2375      this.#collectedNetworkData.delete(
   2376        `${collectedData.request}-${collectedData.type}`
   2377      );
   2378      return;
   2379    }
   2380 
   2381    let readAndProcessBodyFn, size;
   2382    if (response.isDataURL) {
   2383      // Handle data URLs as a special case since the response is not provided
   2384      // by the DevTools ResponseListener in this case.
   2385      const url = request.serializedURL;
   2386      const body = url.substring(url.indexOf(",") + 1);
   2387      const isText =
   2388        response.mimeType &&
   2389        lazy.NetworkHelper.isTextMimeType(response.mimeType);
   2390 
   2391      readAndProcessBodyFn = () =>
   2392        new lazy.NetworkDataBytes({
   2393          getBytesValue: () => body,
   2394          isBase64: !isText,
   2395        });
   2396      size = body.length;
   2397    } else {
   2398      readAndProcessBodyFn = response.readAndProcessResponseBody;
   2399      size = response.encodedBodySize;
   2400    }
   2401 
   2402    this.#maybeCollectNetworkData({
   2403      collectedData,
   2404      dataType: DataType.Response,
   2405      request,
   2406      readAndProcessBodyFn,
   2407      size,
   2408    });
   2409  }
   2410 
   2411  /**
   2412   * Implements https://www.w3.org/TR/webdriver-bidi/#maybe-collect-network-data
   2413   *
   2414   * @param {object} options
   2415   * @param {Data} options.collectedData
   2416   * @param {DataType} options.dataType
   2417   * @param {NetworkRequest} options.request
   2418   * @param {Function} options.readAndProcessBodyFn
   2419   * @param {number} options.size
   2420   */
   2421  async #maybeCollectNetworkData(options) {
   2422    const {
   2423      collectedData,
   2424      dataType,
   2425      request,
   2426      // Note: this parameter is not present in
   2427      // https://www.w3.org/TR/webdriver-bidi/#maybe-collect-network-data
   2428      // Each caller is responsible for providing a callable which will return
   2429      // a NetworkDataBytes instance corresponding to the collected data.
   2430      readAndProcessBodyFn,
   2431      // Note: the spec assumes that in some cases the size can be computed
   2432      // dynamically. But in practice we might be storing encoding data in a
   2433      // format which makes it hard to get the size. So here we always expect
   2434      // callers to provide a size.
   2435      size,
   2436    } = options;
   2437 
   2438    const browsingContext = lazy.NavigableManager.getBrowsingContextById(
   2439      request.contextId
   2440    );
   2441    if (!browsingContext) {
   2442      lazy.logger.trace(
   2443        `Network data not collected for request "${request.requestId}" and data type "${dataType}"` +
   2444          `: navigable no longer available`
   2445      );
   2446      collectedData.pending = false;
   2447      this.#collectedNetworkData.delete(
   2448        `${collectedData.request}-${collectedData.type}`
   2449      );
   2450      collectedData.networkDataCollected.resolve();
   2451      return;
   2452    }
   2453 
   2454    const topNavigable = browsingContext.top;
   2455    let collectors = [];
   2456    for (const [, collector] of this.#networkCollectors) {
   2457      if (
   2458        collector.dataTypes.includes(dataType) &&
   2459        this.#matchCollectorForNavigable(collector, topNavigable)
   2460      ) {
   2461        collectors.push(collector);
   2462      }
   2463    }
   2464 
   2465    if (!collectors.length) {
   2466      lazy.logger.trace(
   2467        `Network data not collected for request "${request.requestId}" and data type "${dataType}"` +
   2468          `: no matching collector`
   2469      );
   2470      collectedData.pending = false;
   2471      this.#collectedNetworkData.delete(
   2472        `${collectedData.request}-${collectedData.type}`
   2473      );
   2474      collectedData.networkDataCollected.resolve();
   2475      return;
   2476    }
   2477 
   2478    let bytes = null;
   2479 
   2480    // At this point, the specification expects to processBody for the cloned
   2481    // body. Here we do not explicitly clone the bodies.
   2482    // For responses, DevTools' NetworkResponseListener clones the stream.
   2483    // For requests, NetworkHelper.readPostTextFromRequest clones the stream on
   2484    // the fly to read it as text.
   2485    try {
   2486      const bytesOrNull = await readAndProcessBodyFn();
   2487      if (bytesOrNull !== null) {
   2488        bytes = bytesOrNull;
   2489      }
   2490    } catch {
   2491      // Let processBodyError be this step: Do nothing.
   2492    }
   2493 
   2494    // If the network module was destroyed while waiting to read the response
   2495    // body, the session has been destroyed. Resolve the promise and bail out.
   2496    if (!this.#collectedNetworkData) {
   2497      collectedData.networkDataCollected.resolve();
   2498      return;
   2499    }
   2500 
   2501    if (bytes !== null) {
   2502      for (const collector of collectors) {
   2503        if (size <= collector.maxEncodedDataSize) {
   2504          collectedData.collectors.add(collector.collector);
   2505        }
   2506      }
   2507 
   2508      if (collectedData.collectors.size) {
   2509        this.#allocateSizeToRecordData(size);
   2510        collectedData.bytes = bytes;
   2511        collectedData.size = size;
   2512      }
   2513    }
   2514 
   2515    // Note: specification flips `collectedData.pending` to false earlier, but
   2516    // the implementation is async with `await response.readResponseBody()`.
   2517    // `collectedData.pending` is only flipped before returning here - and in
   2518    // early returns above.
   2519    collectedData.pending = false;
   2520    if (!collectedData.collectors.size) {
   2521      this.#collectedNetworkData.delete(
   2522        `${collectedData.request}-${collectedData.type}`
   2523      );
   2524    }
   2525    collectedData.networkDataCollected.resolve();
   2526  }
   2527 
   2528  #onAuthRequired = (name, data) => {
   2529    const { authCallbacks, request, response } = data;
   2530 
   2531    let isBlocked = false;
   2532    try {
   2533      const browsingContext = lazy.NavigableManager.getBrowsingContextById(
   2534        request.contextId
   2535      );
   2536      if (!browsingContext) {
   2537        // Do not emit events if the context id does not match any existing
   2538        // browsing context.
   2539        return;
   2540      }
   2541 
   2542      const protocolEventName = "network.authRequired";
   2543 
   2544      const isListening = this._hasListener(protocolEventName, {
   2545        contextId: browsingContext.id,
   2546      });
   2547      if (!isListening) {
   2548        // If there are no listeners subscribed to this event and this context,
   2549        // bail out.
   2550        return;
   2551      }
   2552 
   2553      const baseParameters = this.#processNetworkEvent(
   2554        protocolEventName,
   2555        request
   2556      );
   2557 
   2558      const responseData = this.#getResponseData(response);
   2559      const authRequiredEvent = {
   2560        ...baseParameters,
   2561        response: responseData,
   2562      };
   2563 
   2564      this._emitEventForBrowsingContext(
   2565        browsingContext.id,
   2566        protocolEventName,
   2567        authRequiredEvent
   2568      );
   2569 
   2570      if (authRequiredEvent.isBlocked) {
   2571        isBlocked = true;
   2572 
   2573        // requestChannel.suspend() is not needed here because the request is
   2574        // already blocked on the authentication prompt notification until
   2575        // one of the authCallbacks is called.
   2576        this.#addBlockedRequest(
   2577          authRequiredEvent.request.request,
   2578          InterceptPhase.AuthRequired,
   2579          {
   2580            authCallbacks,
   2581            request,
   2582            response,
   2583          }
   2584        );
   2585      }
   2586    } finally {
   2587      if (!isBlocked) {
   2588        // If the request was not blocked, forward the auth prompt notification
   2589        // to the next consumer.
   2590        authCallbacks.forwardAuthPrompt();
   2591      }
   2592    }
   2593  };
   2594 
   2595  #onBeforeRequestSent = (name, data) => {
   2596    const { request } = data;
   2597 
   2598    if (this.#redirectedRequests.has(request.requestId)) {
   2599      // If this beforeRequestSent event corresponds to a request that has
   2600      // just been redirected using continueRequest, skip the event and remove
   2601      // it from the redirectedRequests set.
   2602      this.#redirectedRequests.delete(request.requestId);
   2603      return;
   2604    }
   2605 
   2606    const browsingContext = lazy.NavigableManager.getBrowsingContextById(
   2607      request.contextId
   2608    );
   2609    if (!browsingContext) {
   2610      // Do not emit events if the context id does not match any existing
   2611      // browsing context.
   2612      return;
   2613    }
   2614 
   2615    // Make sure a collected data is created for the request.
   2616    // Note: this is supposed to be triggered from fetch and doesn't depend on
   2617    // whether network events are used or not.
   2618    this.#cloneNetworkRequestBody(request);
   2619 
   2620    const relatedNavigables = [browsingContext];
   2621    this.#updateRequestHeaders(request, relatedNavigables);
   2622 
   2623    const protocolEventName = "network.beforeRequestSent";
   2624 
   2625    const isListening = this._hasListener(protocolEventName, {
   2626      contextId: browsingContext.id,
   2627    });
   2628    if (!isListening) {
   2629      // If there are no listeners subscribed to this event and this context,
   2630      // bail out.
   2631      return;
   2632    }
   2633 
   2634    this.#maybeCollectNetworkRequestBody(request);
   2635 
   2636    const baseParameters = this.#processNetworkEvent(
   2637      protocolEventName,
   2638      request
   2639    );
   2640 
   2641    // Bug 1805479: Handle the initiator, including stacktrace details.
   2642    const initiator = {
   2643      type: InitiatorType.Other,
   2644    };
   2645 
   2646    const beforeRequestSentEvent = {
   2647      ...baseParameters,
   2648      initiator,
   2649    };
   2650 
   2651    this._emitEventForBrowsingContext(
   2652      browsingContext.id,
   2653      protocolEventName,
   2654      beforeRequestSentEvent
   2655    );
   2656    if (beforeRequestSentEvent.isBlocked) {
   2657      request.wrappedChannel.suspend(
   2658        this.#getSuspendMarkerText(request, "beforeRequestSent")
   2659      );
   2660 
   2661      this.#addBlockedRequest(
   2662        beforeRequestSentEvent.request.request,
   2663        InterceptPhase.BeforeRequestSent,
   2664        {
   2665          request,
   2666        }
   2667      );
   2668    }
   2669  };
   2670 
   2671  #onFetchError = (name, data) => {
   2672    const { request } = data;
   2673 
   2674    const browsingContext = lazy.NavigableManager.getBrowsingContextById(
   2675      request.contextId
   2676    );
   2677    if (!browsingContext) {
   2678      // Do not emit events if the context id does not match any existing
   2679      // browsing context.
   2680      return;
   2681    }
   2682 
   2683    const protocolEventName = "network.fetchError";
   2684 
   2685    const isListening = this._hasListener(protocolEventName, {
   2686      contextId: browsingContext.id,
   2687    });
   2688    if (!isListening) {
   2689      // If there are no listeners subscribed to this event and this context,
   2690      // bail out.
   2691      return;
   2692    }
   2693 
   2694    this.#maybeAbortNetworkResponseBodyCollection(request);
   2695 
   2696    const baseParameters = this.#processNetworkEvent(
   2697      protocolEventName,
   2698      request
   2699    );
   2700 
   2701    const fetchErrorEvent = {
   2702      ...baseParameters,
   2703      errorText: request.errorText,
   2704    };
   2705 
   2706    this._emitEventForBrowsingContext(
   2707      browsingContext.id,
   2708      protocolEventName,
   2709      fetchErrorEvent
   2710    );
   2711  };
   2712 
   2713  #onResponseEvent = async (name, data) => {
   2714    const { request, response } = data;
   2715 
   2716    const browsingContext = lazy.NavigableManager.getBrowsingContextById(
   2717      request.contextId
   2718    );
   2719    if (!browsingContext) {
   2720      // Do not emit events if the context id does not match any existing
   2721      // browsing context.
   2722      return;
   2723    }
   2724 
   2725    const protocolEventName =
   2726      name === "response-started"
   2727        ? "network.responseStarted"
   2728        : "network.responseCompleted";
   2729 
   2730    if (protocolEventName === "network.responseStarted") {
   2731      this.#cloneNetworkResponseBody(request);
   2732    }
   2733 
   2734    const isListening = this._hasListener(protocolEventName, {
   2735      contextId: browsingContext.id,
   2736    });
   2737    if (!isListening) {
   2738      // If there are no listeners subscribed to this event and this context,
   2739      // bail out.
   2740      return;
   2741    }
   2742 
   2743    if (protocolEventName === "network.responseCompleted") {
   2744      this.#maybeCollectNetworkResponseBody(request, response);
   2745    }
   2746 
   2747    const baseParameters = this.#processNetworkEvent(
   2748      protocolEventName,
   2749      request
   2750    );
   2751 
   2752    const responseData = this.#getResponseData(response);
   2753 
   2754    const responseEvent = {
   2755      ...baseParameters,
   2756      response: responseData,
   2757    };
   2758 
   2759    this._emitEventForBrowsingContext(
   2760      browsingContext.id,
   2761      protocolEventName,
   2762      responseEvent
   2763    );
   2764 
   2765    if (
   2766      protocolEventName === "network.responseStarted" &&
   2767      responseEvent.isBlocked &&
   2768      request.supportsInterception
   2769    ) {
   2770      request.wrappedChannel.suspend(
   2771        this.#getSuspendMarkerText(request, "responseStarted")
   2772      );
   2773 
   2774      this.#addBlockedRequest(
   2775        responseEvent.request.request,
   2776        InterceptPhase.ResponseStarted,
   2777        {
   2778          request,
   2779          response,
   2780        }
   2781      );
   2782    }
   2783  };
   2784 
   2785  #onUserContextDeleted = (name, data) => {
   2786    const userContextId = data.userContextId;
   2787    if (this.#extraHeaders.userContextHeaders.has(userContextId)) {
   2788      this.#extraHeaders.userContextHeaders.delete(userContextId);
   2789    }
   2790  };
   2791 
   2792  #processNetworkEvent(event, request) {
   2793    const requestData = this.#getRequestData(request);
   2794    const navigation = request.navigationId;
   2795    let contextId = null;
   2796    let topContextId = null;
   2797    if (request.contextId) {
   2798      // Retrieve the top browsing context id for this network event.
   2799      contextId = request.contextId;
   2800      const browsingContext =
   2801        lazy.NavigableManager.getBrowsingContextById(contextId);
   2802      topContextId = lazy.NavigableManager.getIdForBrowsingContext(
   2803        browsingContext.top
   2804      );
   2805    }
   2806 
   2807    const intercepts = this.#getNetworkIntercepts(event, request, topContextId);
   2808    const redirectCount = request.redirectCount;
   2809    const timestamp = Date.now();
   2810    const isBlocked = !!intercepts.length;
   2811    const params = {
   2812      context: contextId,
   2813      isBlocked,
   2814      navigation,
   2815      redirectCount,
   2816      request: requestData,
   2817      timestamp,
   2818    };
   2819 
   2820    if (isBlocked) {
   2821      params.intercepts = intercepts;
   2822    }
   2823 
   2824    return params;
   2825  }
   2826 
   2827  /**
   2828   * Implements https://w3c.github.io/webdriver-bidi/#remove-collector-from-data
   2829   *
   2830   * @param {Data} collectedData
   2831   *     The Data from which the collector should be removed.
   2832   * @param {string} collectorId
   2833   *     The collector id to remove.
   2834   */
   2835  #removeCollectorFromData(collectedData, collectorId) {
   2836    if (collectedData.collectors.has(collectorId)) {
   2837      collectedData.collectors.delete(collectorId);
   2838      if (!collectedData.collectors.size) {
   2839        this.#collectedNetworkData.delete(
   2840          `${collectedData.request}-${collectedData.type}`
   2841        );
   2842      }
   2843    }
   2844  }
   2845 
   2846  #serializeCookieHeader(cookieHeader) {
   2847    const name = cookieHeader.name;
   2848    const value = deserializeBytesValue(cookieHeader.value);
   2849    return `${name}=${value}`;
   2850  }
   2851 
   2852  #serializeHeader(name, value) {
   2853    return {
   2854      name,
   2855      // TODO: For now, we handle all headers and cookies with the "string" type.
   2856      // See Bug 1835216 to add support for "base64" type and handle non-utf8
   2857      // values.
   2858      value: this.#serializeAsBytesValue(value, BytesValueType.String),
   2859    };
   2860  }
   2861 
   2862  #serializeSetCookieHeader(setCookieHeader) {
   2863    const {
   2864      name,
   2865      value,
   2866      domain = null,
   2867      httpOnly = null,
   2868      expiry = null,
   2869      maxAge = null,
   2870      path = null,
   2871      sameSite = null,
   2872      secure = null,
   2873    } = setCookieHeader;
   2874 
   2875    let headerValue = `${name}=${deserializeBytesValue(value)}`;
   2876 
   2877    if (expiry !== null) {
   2878      headerValue += `;Expires=${expiry}`;
   2879    }
   2880    if (maxAge !== null) {
   2881      headerValue += `;Max-Age=${maxAge}`;
   2882    }
   2883    if (domain !== null) {
   2884      headerValue += `;Domain=${domain}`;
   2885    }
   2886    if (path !== null) {
   2887      headerValue += `;Path=${path}`;
   2888    }
   2889    if (secure === true) {
   2890      headerValue += `;Secure`;
   2891    }
   2892    if (httpOnly === true) {
   2893      headerValue += `;HttpOnly`;
   2894    }
   2895    if (sameSite !== null) {
   2896      headerValue += `;SameSite=${sameSite}`;
   2897    }
   2898    return headerValue;
   2899  }
   2900 
   2901  /**
   2902   * Serialize a value as BytesValue.
   2903   *
   2904   * Note: This does not attempt to fully implement serialize protocol bytes
   2905   * (https://w3c.github.io/webdriver-bidi/#serialize-protocol-bytes) as the
   2906   * header values read from the Channel are already serialized as strings at
   2907   * the moment.
   2908   *
   2909   * @param {string} value
   2910   *     The value to serialize.
   2911   */
   2912  #serializeAsBytesValue(value, type) {
   2913    return {
   2914      type,
   2915      value,
   2916    };
   2917  }
   2918 
   2919  #startListening(event) {
   2920    if (this.#subscribedEvents.size == 0) {
   2921      this.#networkListener.startListening();
   2922    }
   2923    this.#subscribedEvents.add(event);
   2924  }
   2925 
   2926  #stopListening(event) {
   2927    this.#subscribedEvents.delete(event);
   2928    if (this.#subscribedEvents.size == 0) {
   2929      this.#networkListener.stopListening();
   2930    }
   2931  }
   2932 
   2933  #subscribeEvent(event) {
   2934    if (this.constructor.supportedEvents.includes(event)) {
   2935      this.#startListening(event);
   2936    }
   2937  }
   2938 
   2939  #unsubscribeEvent(event) {
   2940    if (this.constructor.supportedEvents.includes(event)) {
   2941      this.#stopListening(event);
   2942    }
   2943  }
   2944 
   2945  /**
   2946   * Implements https://w3c.github.io/webdriver-bidi/#update-headers
   2947   */
   2948  #updateHeaders(request, headers) {
   2949    for (const [name, value] of headers) {
   2950      // Use merge: false to always override the value
   2951      request.setRequestHeader(name, value, { merge: false });
   2952    }
   2953  }
   2954 
   2955  /**
   2956   * Implements https://w3c.github.io/webdriver-bidi/#update-request-headers
   2957   */
   2958  #updateRequestHeaders(request, navigables) {
   2959    for (const browsingContext of navigables) {
   2960      this.#updateHeaders(request, this.#extraHeaders.defaultHeaders);
   2961 
   2962      const userContextHeaders = this.#extraHeaders.userContextHeaders;
   2963      const userContext =
   2964        lazy.UserContextManager.getIdByBrowsingContext(browsingContext);
   2965      if (userContextHeaders.has(userContext)) {
   2966        this.#updateHeaders(request, userContextHeaders.get(userContext));
   2967      }
   2968 
   2969      const navigableHeaders = this.#extraHeaders.navigableHeaders;
   2970      const topNavigableWebProgress = browsingContext.top.webProgress;
   2971      if (navigableHeaders.has(topNavigableWebProgress)) {
   2972        this.#updateHeaders(
   2973          request,
   2974          navigableHeaders.get(topNavigableWebProgress)
   2975        );
   2976      }
   2977    }
   2978  }
   2979 
   2980  /**
   2981   * Internal commands
   2982   */
   2983 
   2984  _applySessionData(params) {
   2985    // TODO: Bug 1775231. Move this logic to a shared module or an abstract
   2986    // class.
   2987    const { category } = params;
   2988    if (category === "event") {
   2989      const filteredSessionData = params.sessionData.filter(item =>
   2990        this.messageHandler.matchesContext(item.contextDescriptor)
   2991      );
   2992      for (const event of this.#subscribedEvents.values()) {
   2993        const hasSessionItem = filteredSessionData.some(
   2994          item => item.value === event
   2995        );
   2996        // If there are no session items for this context, we should unsubscribe from the event.
   2997        if (!hasSessionItem) {
   2998          this.#unsubscribeEvent(event);
   2999        }
   3000      }
   3001 
   3002      // Subscribe to all events, which have an item in SessionData.
   3003      for (const { value } of filteredSessionData) {
   3004        this.#subscribeEvent(value);
   3005      }
   3006    }
   3007  }
   3008 
   3009  _sendEventsForWindowGlobalNetworkResource(params) {
   3010    this.#onBeforeRequestSent("before-request-sent", params);
   3011    this.#onResponseEvent("response-started", params);
   3012    this.#onResponseEvent("response-completed", params);
   3013  }
   3014 
   3015  _setDecodedBodySize(params) {
   3016    const { channelId, decodedBodySize } = params;
   3017    this.#decodedBodySizeMap.setDecodedBodySize(channelId, decodedBodySize);
   3018  }
   3019 
   3020  static get supportedEvents() {
   3021    return [
   3022      "network.authRequired",
   3023      "network.beforeRequestSent",
   3024      "network.fetchError",
   3025      "network.responseCompleted",
   3026      "network.responseStarted",
   3027    ];
   3028  }
   3029 }
   3030 
   3031 /**
   3032 * Deserialize a network BytesValue.
   3033 *
   3034 * @param {BytesValue} bytesValue
   3035 *     The BytesValue to deserialize.
   3036 * @returns {string}
   3037 *     The deserialized value.
   3038 */
   3039 export function deserializeBytesValue(bytesValue) {
   3040  const { type, value } = bytesValue;
   3041 
   3042  if (type === BytesValueType.String) {
   3043    return value;
   3044  }
   3045 
   3046  // For type === BytesValueType.Base64.
   3047  return atob(value);
   3048 }
   3049 
   3050 export const network = NetworkModule;