1
0
Fork 0
firefox/remote/webdriver-bidi/modules/root/network.sys.mjs
Daniel Baumann 5e9a113729
Adding upstream version 140.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-25 09:37:52 +02:00

2024 lines
56 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { RootBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/RootBiDiModule.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
CacheBehavior: "chrome://remote/content/shared/NetworkCacheManager.sys.mjs",
NetworkDecodedBodySizeMap:
"chrome://remote/content/shared/NetworkDecodedBodySizeMap.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
Log: "chrome://remote/content/shared/Log.sys.mjs",
matchURLPattern:
"chrome://remote/content/shared/webdriver/URLPattern.sys.mjs",
NetworkListener:
"chrome://remote/content/shared/listeners/NetworkListener.sys.mjs",
parseChallengeHeader:
"chrome://remote/content/shared/ChallengeHeaderParser.sys.mjs",
parseURLPattern:
"chrome://remote/content/shared/webdriver/URLPattern.sys.mjs",
pprint: "chrome://remote/content/shared/Format.sys.mjs",
TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
truncate: "chrome://remote/content/shared/Format.sys.mjs",
updateCacheBehavior:
"chrome://remote/content/shared/NetworkCacheManager.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "logger", () =>
lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI)
);
/**
* @typedef {object} AuthChallenge
* @property {string} scheme
* @property {string} realm
*/
/**
* @typedef {object} AuthCredentials
* @property {'password'} type
* @property {string} username
* @property {string} password
*/
/**
* @typedef {object} BaseParameters
* @property {string=} context
* @property {Array<string>?} intercepts
* @property {boolean} isBlocked
* @property {Navigation=} navigation
* @property {number} redirectCount
* @property {RequestData} request
* @property {number} timestamp
*/
/**
* @typedef {object} BlockedRequest
* @property {NetworkEventRecord} networkEventRecord
* @property {InterceptPhase} phase
*/
/**
* Enum of possible BytesValue types.
*
* @readonly
* @enum {BytesValueType}
*/
export const BytesValueType = {
Base64: "base64",
String: "string",
};
/**
* @typedef {object} BytesValue
* @property {BytesValueType} type
* @property {string} value
*/
/**
* Enum of possible continueWithAuth actions.
*
* @readonly
* @enum {ContinueWithAuthAction}
*/
const ContinueWithAuthAction = {
Cancel: "cancel",
Default: "default",
ProvideCredentials: "provideCredentials",
};
/**
* @typedef {object} Cookie
* @property {string} domain
* @property {number=} expires
* @property {boolean} httpOnly
* @property {string} name
* @property {string} path
* @property {SameSite} sameSite
* @property {boolean} secure
* @property {number} size
* @property {BytesValue} value
*/
/**
* @typedef {object} CookieHeader
* @property {string} name
* @property {BytesValue} value
*/
/**
* @typedef {object} FetchTimingInfo
* @property {number} timeOrigin
* @property {number} requestTime
* @property {number} redirectStart
* @property {number} redirectEnd
* @property {number} fetchStart
* @property {number} dnsStart
* @property {number} dnsEnd
* @property {number} connectStart
* @property {number} connectEnd
* @property {number} tlsStart
* @property {number} requestStart
* @property {number} responseStart
* @property {number} responseEnd
*/
/**
* @typedef {object} Header
* @property {string} name
* @property {BytesValue} value
*/
/**
* @typedef {string} InitiatorType
*/
/**
* Enum of possible initiator types.
*
* @readonly
* @enum {InitiatorType}
*/
const InitiatorType = {
Other: "other",
Parser: "parser",
Preflight: "preflight",
Script: "script",
};
/**
* @typedef {object} Initiator
* @property {InitiatorType} type
* @property {number=} columnNumber
* @property {number=} lineNumber
* @property {string=} request
* @property {StackTrace=} stackTrace
*/
/**
* Enum of intercept phases.
*
* @readonly
* @enum {InterceptPhase}
*/
const InterceptPhase = {
AuthRequired: "authRequired",
BeforeRequestSent: "beforeRequestSent",
ResponseStarted: "responseStarted",
};
/**
* @typedef {object} InterceptProperties
* @property {Array<InterceptPhase>} phases
* @property {Array<URLPattern>} urlPatterns
*/
/**
* @typedef {object} RequestData
* @property {number|null} bodySize
* Defaults to null.
* @property {Array<Cookie>} cookies
* @property {Array<Header>} headers
* @property {number} headersSize
* @property {string} method
* @property {string} request
* @property {FetchTimingInfo} timings
* @property {string} url
*/
/**
* @typedef {object} BeforeRequestSentParametersProperties
* @property {Initiator} initiator
*/
/* eslint-disable jsdoc/valid-types */
/**
* Parameters for the BeforeRequestSent event
*
* @typedef {BaseParameters & BeforeRequestSentParametersProperties} BeforeRequestSentParameters
*/
/* eslint-enable jsdoc/valid-types */
/**
* @typedef {object} ResponseContent
* @property {number|null} size
* Defaults to null.
*/
/**
* @typedef {object} ResponseData
* @property {string} url
* @property {string} protocol
* @property {number} status
* @property {string} statusText
* @property {boolean} fromCache
* @property {Array<Header>} headers
* @property {string} mimeType
* @property {number} bytesReceived
* @property {number|null} headersSize
* Defaults to null.
* @property {number|null} bodySize
* Defaults to null.
* @property {ResponseContent} content
* @property {Array<AuthChallenge>=} authChallenges
*/
/**
* @typedef {object} ResponseStartedParametersProperties
* @property {ResponseData} response
*/
/* eslint-disable jsdoc/valid-types */
/**
* Parameters for the ResponseStarted event
*
* @typedef {BaseParameters & ResponseStartedParametersProperties} ResponseStartedParameters
*/
/* eslint-enable jsdoc/valid-types */
/**
* @typedef {object} ResponseCompletedParametersProperties
* @property {ResponseData} response
*/
/**
* Enum of possible sameSite values.
*
* @readonly
* @enum {SameSite}
*/
const SameSite = {
Lax: "lax",
None: "none",
Strict: "strict",
};
/**
* @typedef {object} SetCookieHeader
* @property {string} name
* @property {BytesValue} value
* @property {string=} domain
* @property {boolean=} httpOnly
* @property {string=} expiry
* @property {number=} maxAge
* @property {string=} path
* @property {SameSite=} sameSite
* @property {boolean=} secure
*/
/**
* @typedef {object} URLPatternPattern
* @property {'pattern'} type
* @property {string=} protocol
* @property {string=} hostname
* @property {string=} port
* @property {string=} pathname
* @property {string=} search
*/
/**
* @typedef {object} URLPatternString
* @property {'string'} type
* @property {string} pattern
*/
/**
* @typedef {(URLPatternPattern|URLPatternString)} URLPattern
*/
/* eslint-disable jsdoc/valid-types */
/**
* Parameters for the ResponseCompleted event
*
* @typedef {BaseParameters & ResponseCompletedParametersProperties} ResponseCompletedParameters
*/
/* eslint-enable jsdoc/valid-types */
// @see https://searchfox.org/mozilla-central/rev/527d691a542ccc0f333e36689bd665cb000360b2/netwerk/protocol/http/HttpBaseChannel.cpp#2083-2088
const IMMUTABLE_RESPONSE_HEADERS = [
"content-encoding",
"content-length",
"content-type",
"trailer",
"transfer-encoding",
];
class NetworkModule extends RootBiDiModule {
#blockedRequests;
#decodedBodySizeMap;
#interceptMap;
#networkListener;
#redirectedRequests;
#subscribedEvents;
constructor(messageHandler) {
super(messageHandler);
// Map of request id to BlockedRequest
this.#blockedRequests = new Map();
// Map of intercept id to InterceptProperties
this.#interceptMap = new Map();
// Set of request ids which are being redirected using continueRequest with
// a url parameter. Those requests will lead to an additional beforeRequestSent
// event which needs to be filtered out.
this.#redirectedRequests = new Set();
// Set of event names which have active subscriptions
this.#subscribedEvents = new Set();
this.#decodedBodySizeMap = new lazy.NetworkDecodedBodySizeMap();
this.#networkListener = new lazy.NetworkListener(
this.messageHandler.navigationManager,
this.#decodedBodySizeMap
);
this.#networkListener.on("auth-required", this.#onAuthRequired);
this.#networkListener.on("before-request-sent", this.#onBeforeRequestSent);
this.#networkListener.on("fetch-error", this.#onFetchError);
this.#networkListener.on("response-completed", this.#onResponseEvent);
this.#networkListener.on("response-started", this.#onResponseEvent);
}
destroy() {
this.#networkListener.off("auth-required", this.#onAuthRequired);
this.#networkListener.off("before-request-sent", this.#onBeforeRequestSent);
this.#networkListener.off("fetch-error", this.#onFetchError);
this.#networkListener.off("response-completed", this.#onResponseEvent);
this.#networkListener.off("response-started", this.#onResponseEvent);
this.#networkListener.destroy();
this.#decodedBodySizeMap.destroy();
this.#decodedBodySizeMap = null;
this.#blockedRequests = null;
this.#interceptMap = null;
this.#subscribedEvents = null;
}
/**
* Adds a network intercept, which allows to intercept and modify network
* requests and responses.
*
* The network intercept will be created for the provided phases
* (InterceptPhase) and for specific url patterns. When a network event
* corresponding to an intercept phase has a URL which matches any url pattern
* of any intercept, the request will be suspended.
*
* @param {object=} options
* @param {Array<string>=} options.contexts
* The list of browsing context ids where this intercept should be used.
* Optional, defaults to null.
* @param {Array<InterceptPhase>} options.phases
* The phases where this intercept should be checked.
* @param {Array<URLPattern>=} options.urlPatterns
* The URL patterns for this intercept. Optional, defaults to empty array.
*
* @returns {object}
* An object with the following property:
* - intercept {string} The unique id of the network intercept.
*
* @throws {InvalidArgumentError}
* Raised if an argument is of an invalid type or value.
*/
addIntercept(options = {}) {
const { contexts = null, phases, urlPatterns = [] } = options;
if (contexts !== null) {
lazy.assert.isNonEmptyArray(
contexts,
`Expected "contexts" to be a non-empty array, got ${contexts}`
);
for (const contextId of contexts) {
lazy.assert.string(
contextId,
`Expected elements of "contexts" to be a string, got ${contextId}`
);
const context = this.#getBrowsingContext(contextId);
lazy.assert.topLevel(
context,
lazy.pprint`Browsing context with id ${contextId} is not top-level`
);
}
}
lazy.assert.isNonEmptyArray(
phases,
`Expected "phases" to be a non-empty array, got ${phases}`
);
const supportedInterceptPhases = Object.values(InterceptPhase);
for (const phase of phases) {
if (!supportedInterceptPhases.includes(phase)) {
throw new lazy.error.InvalidArgumentError(
`Expected "phases" values to be one of ${supportedInterceptPhases}, got ${phase}`
);
}
}
lazy.assert.array(
urlPatterns,
`Expected "urlPatterns" to be an array, got ${urlPatterns}`
);
const parsedPatterns = urlPatterns.map(urlPattern =>
lazy.parseURLPattern(urlPattern)
);
const interceptId = lazy.generateUUID();
this.#interceptMap.set(interceptId, {
contexts,
phases,
urlPatterns: parsedPatterns,
});
return {
intercept: interceptId,
};
}
/**
* Continues a request that is blocked by a network intercept at the
* beforeRequestSent phase.
*
* @param {object=} options
* @param {string} options.request
* The id of the blocked request that should be continued.
* @param {BytesValue=} options.body
* Optional BytesValue to replace the body of the request.
* @param {Array<CookieHeader>=} options.cookies
* Optional array of cookie header values to replace the cookie header of
* the request.
* @param {Array<Header>=} options.headers
* Optional array of headers to replace the headers of the request.
* request.
* @param {string=} options.method
* Optional string to replace the method of the request.
* @param {string=} options.url
* Optional string to replace the url of the request. If the provided url
* is not a valid URL, an InvalidArgumentError will be thrown.
*
* @throws {InvalidArgumentError}
* Raised if an argument is of an invalid type or value.
* @throws {NoSuchRequestError}
* Raised if the request id does not match any request in the blocked
* requests map.
*/
async continueRequest(options = {}) {
const {
body = null,
cookies = null,
headers = null,
method = null,
url = null,
request: requestId,
} = options;
lazy.assert.string(
requestId,
`Expected "request" to be a string, got ${requestId}`
);
if (body !== null) {
this.#assertBytesValue(
body,
lazy.truncate`Expected "body" to be a network.BytesValue, got ${body}`
);
}
if (cookies !== null) {
lazy.assert.array(
cookies,
`Expected "cookies" to be an array got ${cookies}`
);
for (const cookie of cookies) {
this.#assertHeader(
cookie,
`Expected values in "cookies" to be network.CookieHeader, got ${cookie}`
);
}
}
let deserializedHeaders = [];
if (headers !== null) {
deserializedHeaders = this.#deserializeHeaders(headers);
}
if (method !== null) {
lazy.assert.string(
method,
`Expected "method" to be a string, got ${method}`
);
lazy.assert.that(
value => this.#isValidHttpToken(value),
`Expected "method" to be a valid HTTP token, got ${method}`
)(method);
}
if (url !== null) {
lazy.assert.string(url, `Expected "url" to be a string, got ${url}`);
if (!URL.canParse(url)) {
throw new lazy.error.InvalidArgumentError(
`Expected "url" to be a valid URL, got ${url}`
);
}
}
if (!this.#blockedRequests.has(requestId)) {
throw new lazy.error.NoSuchRequestError(
`Blocked request with id ${requestId} not found`
);
}
const { phase, request, resolveBlockedEvent } =
this.#blockedRequests.get(requestId);
if (phase !== InterceptPhase.BeforeRequestSent) {
throw new lazy.error.InvalidArgumentError(
`Expected blocked request to be in "beforeRequestSent" phase, got ${phase}`
);
}
if (method !== null) {
request.setRequestMethod(method);
}
if (headers !== null) {
// Delete all existing request headers.
request.headers.forEach(([name]) => {
request.clearRequestHeader(name);
});
// Set all headers specified in the headers parameter.
for (const [name, value] of deserializedHeaders) {
request.setRequestHeader(name, value, { merge: true });
}
}
if (cookies !== null) {
let cookieHeader = "";
for (const cookie of cookies) {
if (cookieHeader != "") {
cookieHeader += ";";
}
cookieHeader += this.#serializeCookieHeader(cookie);
}
let foundCookieHeader = false;
for (const [name] of request.headers) {
if (name.toLowerCase() == "cookie") {
// If there is already a cookie header, use merge: false to override
// the value.
request.setRequestHeader(name, cookieHeader, { merge: false });
foundCookieHeader = true;
break;
}
}
if (!foundCookieHeader) {
request.setRequestHeader("Cookie", cookieHeader, { merge: false });
}
}
if (body !== null) {
const value = deserializeBytesValue(body);
request.setRequestBody(value);
}
if (url !== null) {
// Store the requestId in the redirectedRequests set to skip the extra
// beforeRequestSent event.
this.#redirectedRequests.add(requestId);
request.redirectTo(url);
}
request.wrappedChannel.resume();
resolveBlockedEvent();
}
/**
* Continues a response that is blocked by a network intercept at the
* responseStarted or authRequired phase.
*
* @param {object=} options
* @param {string} options.request
* The id of the blocked request that should be continued.
* @param {Array<SetCookieHeader>=} options.cookies [unsupported]
* Optional array of set-cookie header values to replace the set-cookie
* headers of the response.
* @param {AuthCredentials=} options.credentials
* Optional AuthCredentials to use.
* @param {Array<Header>=} options.headers [unsupported]
* Optional array of header values to replace the headers of the response.
* @param {string=} options.reasonPhrase [unsupported]
* Optional string to replace the status message of the response.
* @param {number=} options.statusCode [unsupported]
* Optional number to replace the status code of the response.
*
* @throws {InvalidArgumentError}
* Raised if an argument is of an invalid type or value.
* @throws {NoSuchRequestError}
* Raised if the request id does not match any request in the blocked
* requests map.
*/
async continueResponse(options = {}) {
const {
cookies = null,
credentials = null,
headers = null,
reasonPhrase = null,
request: requestId,
statusCode = null,
} = options;
lazy.assert.string(
requestId,
`Expected "request" to be a string, got ${requestId}`
);
if (cookies !== null) {
lazy.assert.array(
cookies,
`Expected "cookies" to be an array got ${cookies}`
);
for (const cookie of cookies) {
this.#assertSetCookieHeader(cookie);
}
}
if (credentials !== null) {
this.#assertAuthCredentials(credentials);
}
let deserializedHeaders = [];
if (headers !== null) {
// For existing responses, are unable to update some response headers,
// so we skip them for the time being and log a warning.
// Bug 1914351 should remove this limitation.
deserializedHeaders = this.#deserializeHeaders(headers).filter(
([name]) => {
if (IMMUTABLE_RESPONSE_HEADERS.includes(name.toLowerCase())) {
lazy.logger.warn(
`network.continueResponse cannot currently modify the header "${name}", skipping (see Bug 1914351).`
);
return false;
}
return true;
}
);
}
if (reasonPhrase !== null) {
lazy.assert.string(
reasonPhrase,
`Expected "reasonPhrase" to be a string, got ${reasonPhrase}`
);
}
if (statusCode !== null) {
lazy.assert.positiveInteger(
statusCode,
`Expected "statusCode" to be a positive integer, got ${statusCode}`
);
}
if (!this.#blockedRequests.has(requestId)) {
throw new lazy.error.NoSuchRequestError(
`Blocked request with id ${requestId} not found`
);
}
const { authCallbacks, phase, request, resolveBlockedEvent, response } =
this.#blockedRequests.get(requestId);
if (headers !== null) {
// Delete all existing response headers.
response.headers
.filter(
([name]) =>
// All headers in IMMUTABLE_RESPONSE_HEADERS cannot be changed and
// will lead to a NS_ERROR_ILLEGAL_VALUE error.
// Bug 1914351 should remove this limitation.
!IMMUTABLE_RESPONSE_HEADERS.includes(name.toLowerCase())
)
.forEach(([name]) => response.clearResponseHeader(name));
for (const [name, value] of deserializedHeaders) {
response.setResponseHeader(name, value, { merge: true });
}
}
if (cookies !== null) {
for (const cookie of cookies) {
const headerValue = this.#serializeSetCookieHeader(cookie);
response.setResponseHeader("Set-Cookie", headerValue, { merge: true });
}
}
if (statusCode !== null || reasonPhrase !== null) {
response.setResponseStatus({
status: statusCode,
statusText: reasonPhrase,
});
}
if (
phase !== InterceptPhase.ResponseStarted &&
phase !== InterceptPhase.AuthRequired
) {
throw new lazy.error.InvalidArgumentError(
`Expected blocked request to be in "responseStarted" or "authRequired" phase, got ${phase}`
);
}
if (phase === InterceptPhase.AuthRequired) {
// Requests blocked in the AuthRequired phase should be resumed using
// authCallbacks.
if (credentials !== null) {
await authCallbacks.provideAuthCredentials(
credentials.username,
credentials.password
);
} else {
await authCallbacks.provideAuthCredentials();
}
} else {
request.wrappedChannel.resume();
}
resolveBlockedEvent();
}
/**
* Continues a response that is blocked by a network intercept at the
* authRequired phase.
*
* @param {object=} options
* @param {string} options.request
* The id of the blocked request that should be continued.
* @param {string} options.action
* The continueWithAuth action, one of ContinueWithAuthAction.
* @param {AuthCredentials=} options.credentials
* The credentials to use for the ContinueWithAuthAction.ProvideCredentials
* action.
*
* @throws {InvalidArgumentError}
* Raised if an argument is of an invalid type or value.
* @throws {NoSuchRequestError}
* Raised if the request id does not match any request in the blocked
* requests map.
*/
async continueWithAuth(options = {}) {
const { action, credentials, request: requestId } = options;
lazy.assert.string(
requestId,
`Expected "request" to be a string, got ${requestId}`
);
if (!Object.values(ContinueWithAuthAction).includes(action)) {
throw new lazy.error.InvalidArgumentError(
`Expected "action" to be one of ${Object.values(
ContinueWithAuthAction
)} got ${action}`
);
}
if (action == ContinueWithAuthAction.ProvideCredentials) {
this.#assertAuthCredentials(credentials);
}
if (!this.#blockedRequests.has(requestId)) {
throw new lazy.error.NoSuchRequestError(
`Blocked request with id ${requestId} not found`
);
}
const { authCallbacks, phase, resolveBlockedEvent } =
this.#blockedRequests.get(requestId);
if (phase !== InterceptPhase.AuthRequired) {
throw new lazy.error.InvalidArgumentError(
`Expected blocked request to be in "authRequired" phase, got ${phase}`
);
}
switch (action) {
case ContinueWithAuthAction.Cancel: {
authCallbacks.cancelAuthPrompt();
break;
}
case ContinueWithAuthAction.Default: {
authCallbacks.forwardAuthPrompt();
break;
}
case ContinueWithAuthAction.ProvideCredentials: {
await authCallbacks.provideAuthCredentials(
credentials.username,
credentials.password
);
break;
}
}
resolveBlockedEvent();
}
/**
* Fails a request that is blocked by a network intercept.
*
* @param {object=} options
* @param {string} options.request
* The id of the blocked request that should be continued.
*
* @throws {InvalidArgumentError}
* Raised if an argument is of an invalid type or value.
* @throws {NoSuchRequestError}
* Raised if the request id does not match any request in the blocked
* requests map.
*/
async failRequest(options = {}) {
const { request: requestId } = options;
lazy.assert.string(
requestId,
`Expected "request" to be a string, got ${requestId}`
);
if (!this.#blockedRequests.has(requestId)) {
throw new lazy.error.NoSuchRequestError(
`Blocked request with id ${requestId} not found`
);
}
const { phase, request, resolveBlockedEvent } =
this.#blockedRequests.get(requestId);
if (phase === InterceptPhase.AuthRequired) {
throw new lazy.error.InvalidArgumentError(
`Expected blocked request not to be in "authRequired" phase`
);
}
request.wrappedChannel.resume();
request.wrappedChannel.cancel(
Cr.NS_ERROR_ABORT,
Ci.nsILoadInfo.BLOCKING_REASON_WEBDRIVER_BIDI
);
resolveBlockedEvent();
}
/**
* Continues a request thats blocked by a network intercept, by providing a
* complete response.
*
* @param {object=} options
* @param {string} options.request
* The id of the blocked request for which the response should be
* provided.
* @param {BytesValue=} options.body
* Optional BytesValue to replace the body of the response.
* For now, only supported for requests blocked in beforeRequestSent.
* @param {Array<SetCookieHeader>=} options.cookies
* Optional array of set-cookie header values to use for the provided
* response.
* For now, only supported for requests blocked in beforeRequestSent.
* @param {Array<Header>=} options.headers
* Optional array of header values to use for the provided
* response.
* For now, only supported for requests blocked in beforeRequestSent.
* @param {string=} options.reasonPhrase
* Optional string to use as the status message for the provided response.
* For now, only supported for requests blocked in beforeRequestSent.
* @param {number=} options.statusCode
* Optional number to use as the status code for the provided response.
* For now, only supported for requests blocked in beforeRequestSent.
*
* @throws {InvalidArgumentError}
* Raised if an argument is of an invalid type or value.
* @throws {NoSuchRequestError}
* Raised if the request id does not match any request in the blocked
* requests map.
*/
async provideResponse(options = {}) {
const {
body = null,
cookies = null,
headers = null,
reasonPhrase = null,
request: requestId,
statusCode = null,
} = options;
lazy.assert.string(
requestId,
`Expected "request" to be a string, got ${requestId}`
);
if (body !== null) {
this.#assertBytesValue(
body,
`Expected "body" to be a network.BytesValue, got ${body}`
);
}
if (cookies !== null) {
lazy.assert.array(
cookies,
`Expected "cookies" to be an array got ${cookies}`
);
for (const cookie of cookies) {
this.#assertSetCookieHeader(cookie);
}
}
let deserializedHeaders = [];
if (headers !== null) {
deserializedHeaders = this.#deserializeHeaders(headers);
}
if (reasonPhrase !== null) {
lazy.assert.string(
reasonPhrase,
`Expected "reasonPhrase" to be a string, got ${reasonPhrase}`
);
}
if (statusCode !== null) {
lazy.assert.positiveInteger(
statusCode,
`Expected "statusCode" to be a positive integer, got ${statusCode}`
);
}
if (!this.#blockedRequests.has(requestId)) {
throw new lazy.error.NoSuchRequestError(
`Blocked request with id ${requestId} not found`
);
}
const { authCallbacks, phase, request, resolveBlockedEvent } =
this.#blockedRequests.get(requestId);
// Handle optional arguments for the beforeRequestSent phase.
// TODO: Support optional arguments in all phases, see Bug 1901055.
if (phase === InterceptPhase.BeforeRequestSent) {
// Create a new response.
const replacedHttpResponse = Cc[
"@mozilla.org/network/replaced-http-response;1"
].createInstance(Ci.nsIReplacedHttpResponse);
if (statusCode !== null) {
replacedHttpResponse.responseStatus = statusCode;
}
if (reasonPhrase !== null) {
replacedHttpResponse.responseStatusText = reasonPhrase;
}
if (body !== null) {
replacedHttpResponse.responseBody = deserializeBytesValue(body);
}
if (headers !== null) {
for (const [name, value] of deserializedHeaders) {
replacedHttpResponse.setResponseHeader(name, value, true);
}
}
if (cookies !== null) {
for (const cookie of cookies) {
const headerValue = this.#serializeSetCookieHeader(cookie);
replacedHttpResponse.setResponseHeader(
"Set-Cookie",
headerValue,
true
);
}
}
request.setResponseOverride(replacedHttpResponse);
request.wrappedChannel.resume();
} else {
if (body !== null) {
throw new lazy.error.UnsupportedOperationError(
`The "body" parameter is only supported for the beforeRequestSent phase at the moment`
);
}
if (cookies !== null) {
throw new lazy.error.UnsupportedOperationError(
`The "cookies" parameter is only supported for the beforeRequestSent phase at the moment`
);
}
if (headers !== null) {
throw new lazy.error.UnsupportedOperationError(
`The "headers" parameter is only supported for the beforeRequestSent phase at the moment`
);
}
if (reasonPhrase !== null) {
throw new lazy.error.UnsupportedOperationError(
`The "reasonPhrase" parameter is only supported for the beforeRequestSent phase at the moment`
);
}
if (statusCode !== null) {
throw new lazy.error.UnsupportedOperationError(
`The "statusCode" parameter is only supported for the beforeRequestSent phase at the moment`
);
}
if (phase === InterceptPhase.AuthRequired) {
// AuthRequired with no optional argument, resume the authentication.
await authCallbacks.provideAuthCredentials();
} else {
// Any phase other than AuthRequired with no optional argument, resume the
// request.
request.wrappedChannel.resume();
}
}
resolveBlockedEvent();
}
/**
* Removes an existing network intercept.
*
* @param {object=} options
* @param {string} options.intercept
* The id of the intercept to remove.
*
* @throws {InvalidArgumentError}
* Raised if an argument is of an invalid type or value.
* @throws {NoSuchInterceptError}
* Raised if the intercept id could not be found in the internal intercept
* map.
*/
removeIntercept(options = {}) {
const { intercept } = options;
lazy.assert.string(
intercept,
`Expected "intercept" to be a string, got ${intercept}`
);
if (!this.#interceptMap.has(intercept)) {
throw new lazy.error.NoSuchInterceptError(
`Network intercept with id ${intercept} not found`
);
}
this.#interceptMap.delete(intercept);
}
/**
* Configures the network cache behavior for certain requests.
*
* @param {object=} options
* @param {CacheBehavior} options.cacheBehavior
* An enum value to set the network cache behavior.
* @param {Array<string>=} options.contexts
* The list of browsing context ids where the network cache
* behavior should be updated.
*
* @throws {InvalidArgumentError}
* Raised if an argument is of an invalid type or value.
* @throws {NoSuchFrameError}
* If the browsing context cannot be found.
*/
setCacheBehavior(options = {}) {
const { cacheBehavior: behavior, contexts: contextIds = null } = options;
if (!Object.values(lazy.CacheBehavior).includes(behavior)) {
throw new lazy.error.InvalidArgumentError(
`Expected "cacheBehavior" to be one of ${Object.values(
lazy.CacheBehavior
)}` + lazy.pprint` got ${behavior}`
);
}
if (contextIds === null) {
// Set the default behavior if no specific context is specified.
lazy.updateCacheBehavior(behavior);
return;
}
lazy.assert.isNonEmptyArray(
contextIds,
lazy.pprint`Expected "contexts" to be a non-empty array, got ${contextIds}`
);
const contexts = new Set();
for (const contextId of contextIds) {
lazy.assert.string(
contextId,
lazy.pprint`Expected elements of "contexts" to be a string, got ${contextId}`
);
const context = this.#getBrowsingContext(contextId);
lazy.assert.topLevel(
context,
lazy.pprint`Browsing context with id ${contextId} is not top-level`
);
contexts.add(context);
}
lazy.updateCacheBehavior(behavior, contexts);
}
/**
* Add a new request in the blockedRequests map.
*
* @param {string} requestId
* The request id.
* @param {InterceptPhase} phase
* The phase where the request is blocked.
* @param {object=} options
* @param {object=} options.authCallbacks
* Only defined for requests blocked in the authRequired phase.
* Provides callbacks to handle the authentication.
* @param {nsIChannel=} options.requestChannel
* The request channel.
* @param {nsIChannel=} options.responseChannel
* The response channel.
*/
#addBlockedRequest(requestId, phase, options = {}) {
const { authCallbacks, request, response } = options;
const { promise: blockedEventPromise, resolve: resolveBlockedEvent } =
Promise.withResolvers();
this.#blockedRequests.set(requestId, {
authCallbacks,
request,
response,
resolveBlockedEvent,
phase,
});
blockedEventPromise.finally(() => {
this.#blockedRequests.delete(requestId);
});
}
#assertAuthCredentials(credentials) {
lazy.assert.object(
credentials,
`Expected "credentials" to be an object, got ${credentials}`
);
if (credentials.type !== "password") {
throw new lazy.error.InvalidArgumentError(
`Expected credentials "type" to be "password" got ${credentials.type}`
);
}
lazy.assert.string(
credentials.username,
`Expected credentials "username" to be a string, got ${credentials.username}`
);
lazy.assert.string(
credentials.password,
`Expected credentials "password" to be a string, got ${credentials.password}`
);
}
#assertBytesValue(obj, msg) {
lazy.assert.object(obj, msg);
lazy.assert.string(obj.value, msg);
lazy.assert.in(obj.type, Object.values(BytesValueType), msg);
}
#assertHeader(value, msg) {
lazy.assert.object(value, msg);
lazy.assert.string(value.name, msg);
this.#assertBytesValue(value.value, msg);
}
#assertSetCookieHeader(setCookieHeader) {
lazy.assert.object(
setCookieHeader,
`Expected set-cookie header to be an object, got ${setCookieHeader}`
);
const {
name,
value,
domain = null,
httpOnly = null,
expiry = null,
maxAge = null,
path = null,
sameSite = null,
secure = null,
} = setCookieHeader;
lazy.assert.string(
name,
`Expected set-cookie header "name" to be a string, got ${name}`
);
this.#assertBytesValue(
value,
`Expected set-cookie header "value" to be a BytesValue, got ${name}`
);
if (domain !== null) {
lazy.assert.string(
domain,
`Expected set-cookie header "domain" to be a string, got ${domain}`
);
}
if (httpOnly !== null) {
lazy.assert.boolean(
httpOnly,
`Expected set-cookie header "httpOnly" to be a boolean, got ${httpOnly}`
);
}
if (expiry !== null) {
lazy.assert.string(
expiry,
`Expected set-cookie header "expiry" to be a string, got ${expiry}`
);
}
if (maxAge !== null) {
lazy.assert.integer(
maxAge,
`Expected set-cookie header "maxAge" to be an integer, got ${maxAge}`
);
}
if (path !== null) {
lazy.assert.string(
path,
`Expected set-cookie header "path" to be a string, got ${path}`
);
}
if (sameSite !== null) {
lazy.assert.in(
sameSite,
Object.values(SameSite),
`Expected set-cookie header "sameSite" to be one of ${Object.values(
SameSite
)}, got ${sameSite}`
);
}
if (secure !== null) {
lazy.assert.boolean(
secure,
`Expected set-cookie header "secure" to be a boolean, got ${secure}`
);
}
}
#deserializeHeader(protocolHeader) {
const name = protocolHeader.name;
const value = deserializeBytesValue(protocolHeader.value);
return [name, value];
}
#deserializeHeaders(headers) {
const deserializedHeaders = [];
lazy.assert.array(
headers,
lazy.pprint`Expected "headers" to be an array got ${headers}`
);
for (const header of headers) {
this.#assertHeader(
header,
lazy.pprint`Expected values in "headers" to be network.Header, got ${header}`
);
// Deserialize headers immediately to validate the value
const deserializedHeader = this.#deserializeHeader(header);
lazy.assert.that(
value => this.#isValidHttpToken(value),
lazy.pprint`Expected "header" name to be a valid HTTP token, got ${deserializedHeader[0]}`
)(deserializedHeader[0]);
lazy.assert.that(
value => this.#isValidHeaderValue(value),
lazy.pprint`Expected "header" value to be a valid header value, got ${deserializedHeader[1]}`
)(deserializedHeader[1]);
deserializedHeaders.push(deserializedHeader);
}
return deserializedHeaders;
}
#extractChallenges(response) {
let headerName;
// Using case-insensitive match for header names, so we use the lowercase
// version of the "WWW-Authenticate" / "Proxy-Authenticate" strings.
if (response.status === 401) {
headerName = "www-authenticate";
} else if (response.status === 407) {
headerName = "proxy-authenticate";
} else {
return null;
}
const challenges = [];
for (const [name, value] of response.headers) {
if (name.toLowerCase() === headerName) {
// A single header can contain several challenges.
const headerChallenges = lazy.parseChallengeHeader(value);
for (const headerChallenge of headerChallenges) {
const realmParam = headerChallenge.params.find(
param => param.name == "realm"
);
const realm = realmParam ? realmParam.value : undefined;
const challenge = {
scheme: headerChallenge.scheme,
realm,
};
challenges.push(challenge);
}
}
}
return challenges;
}
#getBrowsingContext(contextId) {
const context = lazy.TabManager.getBrowsingContextById(contextId);
if (context === null) {
throw new lazy.error.NoSuchFrameError(
`Browsing Context with id ${contextId} not found`
);
}
if (!context.currentWindowGlobal) {
throw new lazy.error.NoSuchFrameError(
`No window found for BrowsingContext with id ${contextId}`
);
}
return context;
}
#getNetworkIntercepts(event, request, topContextId) {
const intercepts = [];
let phase;
switch (event) {
case "network.beforeRequestSent":
phase = InterceptPhase.BeforeRequestSent;
break;
case "network.responseStarted":
phase = InterceptPhase.ResponseStarted;
break;
case "network.authRequired":
phase = InterceptPhase.AuthRequired;
break;
case "network.responseCompleted":
// The network.responseCompleted event does not match any interception
// phase. Return immediately.
return intercepts;
}
const url = request.serializedURL;
for (const [interceptId, intercept] of this.#interceptMap) {
if (
intercept.contexts !== null &&
!intercept.contexts.includes(topContextId)
) {
// Skip this intercept if the event's context does not match the list
// of contexts for this intercept.
continue;
}
if (intercept.phases.includes(phase)) {
const urlPatterns = intercept.urlPatterns;
if (
!urlPatterns.length ||
urlPatterns.some(pattern => lazy.matchURLPattern(pattern, url))
) {
intercepts.push(interceptId);
}
}
}
return intercepts;
}
#getRequestData(request) {
const requestId = request.requestId;
// "Let url be the result of running the URL serializer with requests URL"
// request.serializedURL is already serialized.
const url = request.serializedURL;
const method = request.method;
const bodySize = request.postDataSize;
const headersSize = request.headersSize;
const headers = [];
const cookies = [];
for (const [name, value] of request.headers) {
headers.push(this.#serializeHeader(name, value));
if (name.toLowerCase() == "cookie") {
// TODO: Retrieve the actual cookies from the cookie store.
const headerCookies = value.split(";");
for (const cookie of headerCookies) {
const equal = cookie.indexOf("=");
const cookieName = cookie.substr(0, equal);
const cookieValue = cookie.substr(equal + 1);
const serializedCookie = this.#serializeHeader(
unescape(cookieName.trim()),
unescape(cookieValue.trim())
);
cookies.push(serializedCookie);
}
}
}
const destination = request.destination;
const initiatorType = request.initiatorType;
const timings = request.timings;
return {
request: requestId,
url,
method,
bodySize,
headersSize,
headers,
cookies,
destination,
initiatorType,
timings,
};
}
#getResponseContentInfo(response) {
return {
size: response.decodedBodySize,
};
}
#getResponseData(response) {
const url = response.serializedURL;
const protocol = response.protocol;
const status = response.status;
const statusText = response.statusMessage;
// TODO: Ideally we should have a `isCacheStateLocal` getter
// const fromCache = response.isCacheStateLocal();
const fromCache = response.fromCache;
const mimeType = response.mimeType;
const headers = [];
for (const [name, value] of response.headers) {
headers.push(this.#serializeHeader(name, value));
}
const bytesReceived = response.totalTransmittedSize;
const headersSize = response.headersTransmittedSize;
const bodySize = response.encodedBodySize;
const content = this.#getResponseContentInfo(response);
const authChallenges = this.#extractChallenges(response);
const params = {
url,
protocol,
status,
statusText,
fromCache,
headers,
mimeType,
bytesReceived,
headersSize,
bodySize,
content,
};
if (authChallenges !== null) {
params.authChallenges = authChallenges;
}
return params;
}
#getSuspendMarkerText(requestData, phase) {
return `Request (id: ${requestData.request}) suspended by WebDriver BiDi in ${phase} phase`;
}
#isValidHeaderValue(value) {
if (!value.length) {
return true;
}
// For non-empty strings check against:
// - leading or trailing tabs & spaces
// - new lines and null bytes
const chars = value.split("");
const tabOrSpace = [" ", "\t"];
const forbiddenChars = ["\r", "\n", "\0"];
return (
!tabOrSpace.includes(chars.at(0)) &&
!tabOrSpace.includes(chars.at(-1)) &&
forbiddenChars.every(c => !chars.includes(c))
);
}
/**
* This helper is adapted from a C++ validation helper in nsHttp.cpp.
*
* @see https://searchfox.org/mozilla-central/rev/445a6e86233c733c5557ef44e1d33444adaddefc/netwerk/protocol/http/nsHttp.cpp#169
*/
#isValidHttpToken(token) {
// prettier-ignore
// This array corresponds to all char codes between 0 and 127, which is the
// range of supported char codes for HTTP tokens. Within this range,
// accepted char codes are marked with a 1, forbidden char codes with a 0.
const validTokenMap = [
0, 0, 0, 0, 0, 0, 0, 0, // 0
0, 0, 0, 0, 0, 0, 0, 0, // 8
0, 0, 0, 0, 0, 0, 0, 0, // 16
0, 0, 0, 0, 0, 0, 0, 0, // 24
0, 1, 0, 1, 1, 1, 1, 1, // 32
0, 0, 1, 1, 0, 1, 1, 0, // 40
1, 1, 1, 1, 1, 1, 1, 1, // 48
1, 1, 0, 0, 0, 0, 0, 0, // 56
0, 1, 1, 1, 1, 1, 1, 1, // 64
1, 1, 1, 1, 1, 1, 1, 1, // 72
1, 1, 1, 1, 1, 1, 1, 1, // 80
1, 1, 1, 0, 0, 0, 1, 1, // 88
1, 1, 1, 1, 1, 1, 1, 1, // 96
1, 1, 1, 1, 1, 1, 1, 1, // 104
1, 1, 1, 1, 1, 1, 1, 1, // 112
1, 1, 1, 0, 1, 0, 1, 0 // 120
];
if (!token.length) {
return false;
}
return token
.split("")
.map(s => s.charCodeAt(0))
.every(c => validTokenMap[c]);
}
#onAuthRequired = (name, data) => {
const { authCallbacks, request, response } = data;
let isBlocked = false;
try {
const browsingContext = lazy.TabManager.getBrowsingContextById(
request.contextId
);
if (!browsingContext) {
// Do not emit events if the context id does not match any existing
// browsing context.
return;
}
const protocolEventName = "network.authRequired";
const isListening = this._hasListener(protocolEventName, {
contextId: browsingContext.id,
});
if (!isListening) {
// If there are no listeners subscribed to this event and this context,
// bail out.
return;
}
const baseParameters = this.#processNetworkEvent(
protocolEventName,
request
);
const responseData = this.#getResponseData(response);
const authRequiredEvent = {
...baseParameters,
response: responseData,
};
this._emitEventForBrowsingContext(
browsingContext.id,
protocolEventName,
authRequiredEvent
);
if (authRequiredEvent.isBlocked) {
isBlocked = true;
// requestChannel.suspend() is not needed here because the request is
// already blocked on the authentication prompt notification until
// one of the authCallbacks is called.
this.#addBlockedRequest(
authRequiredEvent.request.request,
InterceptPhase.AuthRequired,
{
authCallbacks,
request,
response,
}
);
}
} finally {
if (!isBlocked) {
// If the request was not blocked, forward the auth prompt notification
// to the next consumer.
authCallbacks.forwardAuthPrompt();
}
}
};
#onBeforeRequestSent = (name, data) => {
const { request } = data;
if (this.#redirectedRequests.has(request.requestId)) {
// If this beforeRequestSent event corresponds to a request that has
// just been redirected using continueRequest, skip the event and remove
// it from the redirectedRequests set.
this.#redirectedRequests.delete(request.requestId);
return;
}
const browsingContext = lazy.TabManager.getBrowsingContextById(
request.contextId
);
if (!browsingContext) {
// Do not emit events if the context id does not match any existing
// browsing context.
return;
}
const protocolEventName = "network.beforeRequestSent";
const isListening = this._hasListener(protocolEventName, {
contextId: browsingContext.id,
});
if (!isListening) {
// If there are no listeners subscribed to this event and this context,
// bail out.
return;
}
const baseParameters = this.#processNetworkEvent(
protocolEventName,
request
);
// Bug 1805479: Handle the initiator, including stacktrace details.
const initiator = {
type: InitiatorType.Other,
};
const beforeRequestSentEvent = {
...baseParameters,
initiator,
};
this._emitEventForBrowsingContext(
browsingContext.id,
protocolEventName,
beforeRequestSentEvent
);
if (beforeRequestSentEvent.isBlocked && request.supportsInterception) {
// TODO: Requests suspended in beforeRequestSent still reach the server at
// the moment. https://bugzilla.mozilla.org/show_bug.cgi?id=1849686
request.wrappedChannel.suspend(
this.#getSuspendMarkerText(request, "beforeRequestSent")
);
this.#addBlockedRequest(
beforeRequestSentEvent.request.request,
InterceptPhase.BeforeRequestSent,
{
request,
}
);
}
};
#onFetchError = (name, data) => {
const { request } = data;
const browsingContext = lazy.TabManager.getBrowsingContextById(
request.contextId
);
if (!browsingContext) {
// Do not emit events if the context id does not match any existing
// browsing context.
return;
}
const protocolEventName = "network.fetchError";
const isListening = this._hasListener(protocolEventName, {
contextId: browsingContext.id,
});
if (!isListening) {
// If there are no listeners subscribed to this event and this context,
// bail out.
return;
}
const baseParameters = this.#processNetworkEvent(
protocolEventName,
request
);
const fetchErrorEvent = {
...baseParameters,
errorText: request.errorText,
};
this._emitEventForBrowsingContext(
browsingContext.id,
protocolEventName,
fetchErrorEvent
);
};
#onResponseEvent = async (name, data) => {
const { request, response } = data;
const browsingContext = lazy.TabManager.getBrowsingContextById(
request.contextId
);
if (!browsingContext) {
// Do not emit events if the context id does not match any existing
// browsing context.
return;
}
const protocolEventName =
name === "response-started"
? "network.responseStarted"
: "network.responseCompleted";
const isListening = this._hasListener(protocolEventName, {
contextId: browsingContext.id,
});
if (!isListening) {
// If there are no listeners subscribed to this event and this context,
// bail out.
return;
}
const baseParameters = this.#processNetworkEvent(
protocolEventName,
request
);
const responseData = this.#getResponseData(response);
const responseEvent = {
...baseParameters,
response: responseData,
};
this._emitEventForBrowsingContext(
browsingContext.id,
protocolEventName,
responseEvent
);
if (
protocolEventName === "network.responseStarted" &&
responseEvent.isBlocked &&
request.supportsInterception
) {
request.wrappedChannel.suspend(
this.#getSuspendMarkerText(request, "responseStarted")
);
this.#addBlockedRequest(
responseEvent.request.request,
InterceptPhase.ResponseStarted,
{
request,
response,
}
);
}
};
#processNetworkEvent(event, request) {
const requestData = this.#getRequestData(request);
const navigation = request.navigationId;
let contextId = null;
let topContextId = null;
if (request.contextId) {
// Retrieve the top browsing context id for this network event.
contextId = request.contextId;
const browsingContext = lazy.TabManager.getBrowsingContextById(contextId);
topContextId = lazy.TabManager.getIdForBrowsingContext(
browsingContext.top
);
}
const intercepts = this.#getNetworkIntercepts(event, request, topContextId);
const redirectCount = request.redirectCount;
const timestamp = Date.now();
const isBlocked = !!intercepts.length;
const params = {
context: contextId,
isBlocked,
navigation,
redirectCount,
request: requestData,
timestamp,
};
if (isBlocked) {
params.intercepts = intercepts;
}
return params;
}
#serializeCookieHeader(cookieHeader) {
const name = cookieHeader.name;
const value = deserializeBytesValue(cookieHeader.value);
return `${name}=${value}`;
}
#serializeHeader(name, value) {
return {
name,
value: this.#serializeStringAsBytesValue(value),
};
}
#serializeSetCookieHeader(setCookieHeader) {
const {
name,
value,
domain = null,
httpOnly = null,
expiry = null,
maxAge = null,
path = null,
sameSite = null,
secure = null,
} = setCookieHeader;
let headerValue = `${name}=${deserializeBytesValue(value)}`;
if (expiry !== null) {
headerValue += `;Expires=${expiry}`;
}
if (maxAge !== null) {
headerValue += `;Max-Age=${maxAge}`;
}
if (domain !== null) {
headerValue += `;Domain=${domain}`;
}
if (path !== null) {
headerValue += `;Path=${path}`;
}
if (secure === true) {
headerValue += `;Secure`;
}
if (httpOnly === true) {
headerValue += `;HttpOnly`;
}
if (sameSite !== null) {
headerValue += `;SameSite=${sameSite}`;
}
return headerValue;
}
/**
* Serialize a string value as BytesValue.
*
* Note: This does not attempt to fully implement serialize protocol bytes
* (https://w3c.github.io/webdriver-bidi/#serialize-protocol-bytes) as the
* header values read from the Channel are already serialized as strings at
* the moment.
*
* @param {string} value
* The value to serialize.
*/
#serializeStringAsBytesValue(value) {
// TODO: For now, we handle all headers and cookies with the "string" type.
// See Bug 1835216 to add support for "base64" type and handle non-utf8
// values.
return {
type: BytesValueType.String,
value,
};
}
#startListening(event) {
if (this.#subscribedEvents.size == 0) {
this.#networkListener.startListening();
}
this.#subscribedEvents.add(event);
}
#stopListening(event) {
this.#subscribedEvents.delete(event);
if (this.#subscribedEvents.size == 0) {
this.#networkListener.stopListening();
}
}
#subscribeEvent(event) {
if (this.constructor.supportedEvents.includes(event)) {
this.#startListening(event);
}
}
#unsubscribeEvent(event) {
if (this.constructor.supportedEvents.includes(event)) {
this.#stopListening(event);
}
}
/**
* Internal commands
*/
_applySessionData(params) {
// TODO: Bug 1775231. Move this logic to a shared module or an abstract
// class.
const { category } = params;
if (category === "event") {
const filteredSessionData = params.sessionData.filter(item =>
this.messageHandler.matchesContext(item.contextDescriptor)
);
for (const event of this.#subscribedEvents.values()) {
const hasSessionItem = filteredSessionData.some(
item => item.value === event
);
// If there are no session items for this context, we should unsubscribe from the event.
if (!hasSessionItem) {
this.#unsubscribeEvent(event);
}
}
// Subscribe to all events, which have an item in SessionData.
for (const { value } of filteredSessionData) {
this.#subscribeEvent(value);
}
}
}
_sendEventsForWindowGlobalNetworkResource(params) {
this.#onBeforeRequestSent("before-request-sent", params);
this.#onResponseEvent("response-started", params);
this.#onResponseEvent("response-completed", params);
}
_setDecodedBodySize(params) {
const { channelId, decodedBodySize } = params;
this.#decodedBodySizeMap.setDecodedBodySize(channelId, decodedBodySize);
}
static get supportedEvents() {
return [
"network.authRequired",
"network.beforeRequestSent",
"network.fetchError",
"network.responseCompleted",
"network.responseStarted",
];
}
}
/**
* Deserialize a network BytesValue.
*
* @param {BytesValue} bytesValue
* The BytesValue to deserialize.
* @returns {string}
* The deserialized value.
*/
export function deserializeBytesValue(bytesValue) {
const { type, value } = bytesValue;
if (type === BytesValueType.String) {
return value;
}
// For type === BytesValueType.Base64.
return atob(value);
}
export const network = NetworkModule;