This commit is contained in:
nik
2025-10-03 22:27:28 +03:00
parent 829fad0e17
commit 871cf7e792
16520 changed files with 2967597 additions and 3 deletions

View File

@@ -0,0 +1,45 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Network, type EmptyResult } from '../../../protocol/protocol.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { NetworkStorage } from './NetworkStorage.js';
import { type ParsedUrlPattern } from './NetworkUtils.js';
/** Dispatches Network module commands. */
export declare class NetworkProcessor {
#private;
constructor(browsingContextStorage: BrowsingContextStorage, networkStorage: NetworkStorage);
addIntercept(params: Network.AddInterceptParameters): Promise<Network.AddInterceptResult>;
continueRequest(params: Network.ContinueRequestParameters): Promise<EmptyResult>;
continueResponse(params: Network.ContinueResponseParameters): Promise<EmptyResult>;
continueWithAuth(params: Network.ContinueWithAuthParameters): Promise<EmptyResult>;
failRequest({ request: networkId, }: Network.FailRequestParameters): Promise<EmptyResult>;
provideResponse(params: Network.ProvideResponseParameters): Promise<EmptyResult>;
removeIntercept(params: Network.RemoveInterceptParameters): Promise<EmptyResult>;
setCacheBehavior(params: Network.SetCacheBehaviorParameters): Promise<EmptyResult>;
/**
* Validate https://fetch.spec.whatwg.org/#header-value
*/
static validateHeaders(headers: Network.Header[]): void;
static isMethodValid(method: string): boolean;
/**
* Attempts to parse the given url.
* Throws an InvalidArgumentException if the url is invalid.
*/
static parseUrlString(url: string): URL;
static parseUrlPatterns(urlPatterns: Network.UrlPattern[]): ParsedUrlPattern[];
static wrapInterceptionError(error: any): any;
}

View File

@@ -0,0 +1,365 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NoSuchRequestException, InvalidArgumentException, } from '../../../protocol/protocol.js';
import { isSpecialScheme } from './NetworkUtils.js';
/** Dispatches Network module commands. */
export class NetworkProcessor {
#browsingContextStorage;
#networkStorage;
constructor(browsingContextStorage, networkStorage) {
this.#browsingContextStorage = browsingContextStorage;
this.#networkStorage = networkStorage;
}
async addIntercept(params) {
this.#browsingContextStorage.verifyTopLevelContextsList(params.contexts);
const urlPatterns = params.urlPatterns ?? [];
const parsedUrlPatterns = NetworkProcessor.parseUrlPatterns(urlPatterns);
const intercept = this.#networkStorage.addIntercept({
urlPatterns: parsedUrlPatterns,
phases: params.phases,
contexts: params.contexts,
});
await Promise.all(this.#browsingContextStorage.getAllContexts().map((context) => {
return context.cdpTarget.toggleNetwork();
}));
return {
intercept,
};
}
async continueRequest(params) {
if (params.url !== undefined) {
NetworkProcessor.parseUrlString(params.url);
}
if (params.method !== undefined) {
if (!NetworkProcessor.isMethodValid(params.method)) {
throw new InvalidArgumentException(`Method '${params.method}' is invalid.`);
}
}
if (params.headers) {
NetworkProcessor.validateHeaders(params.headers);
}
const request = this.#getBlockedRequestOrFail(params.request, [
"beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */,
]);
try {
await request.continueRequest(params);
}
catch (error) {
throw NetworkProcessor.wrapInterceptionError(error);
}
return {};
}
async continueResponse(params) {
if (params.headers) {
NetworkProcessor.validateHeaders(params.headers);
}
const request = this.#getBlockedRequestOrFail(params.request, [
"authRequired" /* Network.InterceptPhase.AuthRequired */,
"responseStarted" /* Network.InterceptPhase.ResponseStarted */,
]);
try {
await request.continueResponse(params);
}
catch (error) {
throw NetworkProcessor.wrapInterceptionError(error);
}
return {};
}
async continueWithAuth(params) {
const networkId = params.request;
const request = this.#getBlockedRequestOrFail(networkId, [
"authRequired" /* Network.InterceptPhase.AuthRequired */,
]);
await request.continueWithAuth(params);
return {};
}
async failRequest({ request: networkId, }) {
const request = this.#getRequestOrFail(networkId);
if (request.interceptPhase === "authRequired" /* Network.InterceptPhase.AuthRequired */) {
throw new InvalidArgumentException(`Request '${networkId}' in 'authRequired' phase cannot be failed`);
}
if (!request.interceptPhase) {
throw new NoSuchRequestException(`No blocked request found for network id '${networkId}'`);
}
await request.failRequest('Failed');
return {};
}
async provideResponse(params) {
if (params.headers) {
NetworkProcessor.validateHeaders(params.headers);
}
const request = this.#getBlockedRequestOrFail(params.request, [
"beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */,
"responseStarted" /* Network.InterceptPhase.ResponseStarted */,
"authRequired" /* Network.InterceptPhase.AuthRequired */,
]);
try {
await request.provideResponse(params);
}
catch (error) {
throw NetworkProcessor.wrapInterceptionError(error);
}
return {};
}
async removeIntercept(params) {
this.#networkStorage.removeIntercept(params.intercept);
await Promise.all(this.#browsingContextStorage.getAllContexts().map((context) => {
return context.cdpTarget.toggleNetwork();
}));
return {};
}
async setCacheBehavior(params) {
const contexts = this.#browsingContextStorage.verifyTopLevelContextsList(params.contexts);
// Change all targets
if (contexts.size === 0) {
this.#networkStorage.defaultCacheBehavior = params.cacheBehavior;
await Promise.all(this.#browsingContextStorage.getAllContexts().map((context) => {
return context.cdpTarget.toggleSetCacheDisabled();
}));
return {};
}
const cacheDisabled = params.cacheBehavior === 'bypass';
await Promise.all([...contexts.values()].map((context) => {
return context.cdpTarget.toggleSetCacheDisabled(cacheDisabled);
}));
return {};
}
#getRequestOrFail(id) {
const request = this.#networkStorage.getRequestById(id);
if (!request) {
throw new NoSuchRequestException(`Network request with ID '${id}' doesn't exist`);
}
return request;
}
#getBlockedRequestOrFail(id, phases) {
const request = this.#getRequestOrFail(id);
if (!request.interceptPhase) {
throw new NoSuchRequestException(`No blocked request found for network id '${id}'`);
}
if (request.interceptPhase && !phases.includes(request.interceptPhase)) {
throw new InvalidArgumentException(`Blocked request for network id '${id}' is in '${request.interceptPhase}' phase`);
}
return request;
}
/**
* Validate https://fetch.spec.whatwg.org/#header-value
*/
static validateHeaders(headers) {
for (const header of headers) {
let headerValue;
if (header.value.type === 'string') {
headerValue = header.value.value;
}
else {
headerValue = atob(header.value.value);
}
if (headerValue !== headerValue.trim() ||
headerValue.includes('\n') ||
headerValue.includes('\0')) {
throw new InvalidArgumentException(`Header value '${headerValue}' is not acceptable value`);
}
}
}
static isMethodValid(method) {
// https://httpwg.org/specs/rfc9110.html#method.overview
return /^[!#$%&'*+\-.^_`|~a-zA-Z\d]+$/.test(method);
}
/**
* Attempts to parse the given url.
* Throws an InvalidArgumentException if the url is invalid.
*/
static parseUrlString(url) {
try {
return new URL(url);
}
catch (error) {
throw new InvalidArgumentException(`Invalid URL '${url}': ${error}`);
}
}
static parseUrlPatterns(urlPatterns) {
return urlPatterns.map((urlPattern) => {
let patternUrl = '';
let hasProtocol = true;
let hasHostname = true;
let hasPort = true;
let hasPathname = true;
let hasSearch = true;
switch (urlPattern.type) {
case 'string': {
patternUrl = unescapeURLPattern(urlPattern.pattern);
break;
}
case 'pattern': {
if (urlPattern.protocol === undefined) {
hasProtocol = false;
patternUrl += 'http';
}
else {
if (urlPattern.protocol === '') {
throw new InvalidArgumentException('URL pattern must specify a protocol');
}
urlPattern.protocol = unescapeURLPattern(urlPattern.protocol);
if (!urlPattern.protocol.match(/^[a-zA-Z+-.]+$/)) {
throw new InvalidArgumentException('Forbidden characters');
}
patternUrl += urlPattern.protocol;
}
const scheme = patternUrl.toLocaleLowerCase();
patternUrl += ':';
if (isSpecialScheme(scheme)) {
patternUrl += '//';
}
if (urlPattern.hostname === undefined) {
if (scheme !== 'file') {
patternUrl += 'placeholder';
}
hasHostname = false;
}
else {
if (urlPattern.hostname === '') {
throw new InvalidArgumentException('URL pattern must specify a hostname');
}
if (urlPattern.protocol === 'file') {
throw new InvalidArgumentException(`URL pattern protocol cannot be 'file'`);
}
urlPattern.hostname = unescapeURLPattern(urlPattern.hostname);
let insideBrackets = false;
for (const c of urlPattern.hostname) {
if (c === '/' || c === '?' || c === '#') {
throw new InvalidArgumentException(`'/', '?', '#' are forbidden in hostname`);
}
if (!insideBrackets && c === ':') {
throw new InvalidArgumentException(`':' is only allowed inside brackets in hostname`);
}
if (c === '[') {
insideBrackets = true;
}
if (c === ']') {
insideBrackets = false;
}
}
patternUrl += urlPattern.hostname;
}
if (urlPattern.port === undefined) {
hasPort = false;
}
else {
if (urlPattern.port === '') {
throw new InvalidArgumentException(`URL pattern must specify a port`);
}
urlPattern.port = unescapeURLPattern(urlPattern.port);
patternUrl += ':';
if (!urlPattern.port.match(/^\d+$/)) {
throw new InvalidArgumentException('Forbidden characters');
}
patternUrl += urlPattern.port;
}
if (urlPattern.pathname === undefined) {
hasPathname = false;
}
else {
urlPattern.pathname = unescapeURLPattern(urlPattern.pathname);
if (urlPattern.pathname[0] !== '/') {
patternUrl += '/';
}
if (urlPattern.pathname.includes('#') ||
urlPattern.pathname.includes('?')) {
throw new InvalidArgumentException('Forbidden characters');
}
patternUrl += urlPattern.pathname;
}
if (urlPattern.search === undefined) {
hasSearch = false;
}
else {
urlPattern.search = unescapeURLPattern(urlPattern.search);
if (urlPattern.search[0] !== '?') {
patternUrl += '?';
}
if (urlPattern.search.includes('#')) {
throw new InvalidArgumentException('Forbidden characters');
}
patternUrl += urlPattern.search;
}
break;
}
}
const serializePort = (url) => {
const defaultPorts = {
'ftp:': 21,
'file:': null,
'http:': 80,
'https:': 443,
'ws:': 80,
'wss:': 443,
};
if (isSpecialScheme(url.protocol) &&
defaultPorts[url.protocol] !== null &&
(!url.port || String(defaultPorts[url.protocol]) === url.port)) {
return '';
}
else if (url.port) {
return url.port;
}
return undefined;
};
try {
const url = new URL(patternUrl);
return {
protocol: hasProtocol ? url.protocol.replace(/:$/, '') : undefined,
hostname: hasHostname ? url.hostname : undefined,
port: hasPort ? serializePort(url) : undefined,
pathname: hasPathname && url.pathname ? url.pathname : undefined,
search: hasSearch ? url.search : undefined,
};
}
catch (err) {
throw new InvalidArgumentException(`${err.message} '${patternUrl}'`);
}
});
}
static wrapInterceptionError(error) {
// https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/protocol/fetch_handler.cc;l=169
if (error?.message.includes('Invalid header')) {
return new InvalidArgumentException('Invalid header');
}
return error;
}
}
/**
* See https://w3c.github.io/webdriver-bidi/#unescape-url-pattern
*/
function unescapeURLPattern(pattern) {
const forbidden = new Set(['(', ')', '*', '{', '}']);
let result = '';
let isEscaped = false;
for (const c of pattern) {
if (!isEscaped) {
if (forbidden.has(c)) {
throw new InvalidArgumentException('Forbidden characters');
}
if (c === '\\') {
isEscaped = true;
continue;
}
}
result += c;
isEscaped = false;
}
return result;
}
//# sourceMappingURL=NetworkProcessor.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,49 @@
/**
* @fileoverview `NetworkRequest` represents a single network request and keeps
* track of all the related CDP events.
*/
import type { Protocol } from 'devtools-protocol';
import { Network } from '../../../protocol/protocol.js';
import { Deferred } from '../../../utils/Deferred.js';
import { type LoggerFn } from '../../../utils/log.js';
import type { CdpTarget } from '../cdp/CdpTarget.js';
import type { EventManager } from '../session/EventManager.js';
import type { NetworkStorage } from './NetworkStorage.js';
/** Abstracts one individual network request. */
export declare class NetworkRequest {
#private;
static unknownParameter: string;
waitNextPhase: Deferred<void>;
constructor(id: Network.Request, eventManager: EventManager, networkStorage: NetworkStorage, cdpTarget: CdpTarget, redirectCount?: number, logger?: LoggerFn);
get id(): string;
get fetchId(): string | undefined;
/**
* When blocked returns the phase for it
*/
get interceptPhase(): Network.InterceptPhase | undefined;
get url(): string;
get redirectCount(): number;
get cdpTarget(): CdpTarget;
get cdpClient(): import("../../BidiMapper.js").CdpClient;
isRedirecting(): boolean;
handleRedirect(event: Protocol.Network.RequestWillBeSentEvent): void;
onRequestWillBeSentEvent(event: Protocol.Network.RequestWillBeSentEvent): void;
onRequestWillBeSentExtraInfoEvent(event: Protocol.Network.RequestWillBeSentExtraInfoEvent): void;
onResponseReceivedExtraInfoEvent(event: Protocol.Network.ResponseReceivedExtraInfoEvent): void;
onResponseReceivedEvent(event: Protocol.Network.ResponseReceivedEvent): void;
onServedFromCache(): void;
onLoadingFailedEvent(event: Protocol.Network.LoadingFailedEvent): void;
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-failRequest */
failRequest(errorReason: Protocol.Network.ErrorReason): Promise<void>;
onRequestPaused(event: Protocol.Fetch.RequestPausedEvent): void;
onAuthRequired(event: Protocol.Fetch.AuthRequiredEvent): void;
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-continueRequest */
continueRequest(overrides?: Omit<Network.ContinueRequestParameters, 'request'>): Promise<void>;
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-continueResponse */
continueResponse(overrides?: Omit<Network.ContinueResponseParameters, 'request'>): Promise<void>;
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-continueWithAuth */
continueWithAuth(authChallenge: Omit<Network.ContinueWithAuthParameters, 'request'>): Promise<void>;
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-provideResponse */
provideResponse(overrides: Omit<Network.ProvideResponseParameters, 'request'>): Promise<void>;
dispose(): void;
}

View File

@@ -0,0 +1,723 @@
/*
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
var _a;
import { ChromiumBidi, } from '../../../protocol/protocol.js';
import { assert } from '../../../utils/assert.js';
import { Deferred } from '../../../utils/Deferred.js';
import { LogType } from '../../../utils/log.js';
import { computeHeadersSize, bidiNetworkHeadersFromCdpNetworkHeaders, cdpToBiDiCookie, cdpFetchHeadersFromBidiNetworkHeaders, cdpAuthChallengeResponseFromBidiAuthContinueWithAuthAction, bidiBodySizeFromCdpPostDataEntries, networkHeaderFromCookieHeaders, getTiming, } from './NetworkUtils.js';
const REALM_REGEX = /(?<=realm=").*(?=")/;
/** Abstracts one individual network request. */
export class NetworkRequest {
static unknownParameter = 'UNKNOWN';
/**
* Each network request has an associated request id, which is a string
* uniquely identifying that request.
*
* The identifier for a request resulting from a redirect matches that of the
* request that initiated it.
*/
#id;
#fetchId;
/**
* Indicates the network intercept phase, if the request is currently blocked.
* Undefined necessarily implies that the request is not blocked.
*/
#interceptPhase;
#servedFromCache = false;
#redirectCount;
#request = {};
#requestOverrides;
#responseOverrides;
#response = {};
#eventManager;
#networkStorage;
#cdpTarget;
#logger;
#emittedEvents = {
[ChromiumBidi.Network.EventNames.AuthRequired]: false,
[ChromiumBidi.Network.EventNames.BeforeRequestSent]: false,
[ChromiumBidi.Network.EventNames.FetchError]: false,
[ChromiumBidi.Network.EventNames.ResponseCompleted]: false,
[ChromiumBidi.Network.EventNames.ResponseStarted]: false,
};
waitNextPhase = new Deferred();
constructor(id, eventManager, networkStorage, cdpTarget, redirectCount = 0, logger) {
this.#id = id;
this.#eventManager = eventManager;
this.#networkStorage = networkStorage;
this.#cdpTarget = cdpTarget;
this.#redirectCount = redirectCount;
this.#logger = logger;
}
get id() {
return this.#id;
}
get fetchId() {
return this.#fetchId;
}
/**
* When blocked returns the phase for it
*/
get interceptPhase() {
return this.#interceptPhase;
}
get url() {
const fragment = this.#request.info?.request.urlFragment ??
this.#request.paused?.request.urlFragment ??
'';
const url = this.#response.info?.url ??
this.#response.paused?.request.url ??
this.#requestOverrides?.url ??
this.#request.auth?.request.url ??
this.#request.info?.request.url ??
this.#request.paused?.request.url ??
_a.unknownParameter;
return `${url}${fragment}`;
}
get redirectCount() {
return this.#redirectCount;
}
get cdpTarget() {
return this.#cdpTarget;
}
get cdpClient() {
return this.#cdpTarget.cdpClient;
}
isRedirecting() {
return Boolean(this.#request.info);
}
#isDataUrl() {
return this.url.startsWith('data:');
}
get #method() {
return (this.#requestOverrides?.method ??
this.#request.info?.request.method ??
this.#request.paused?.request.method ??
this.#request.auth?.request.method ??
this.#response.paused?.request.method);
}
get #navigationId() {
// Heuristic to determine if this is a navigation request, and if not return null.
if (!this.#request.info ||
!this.#request.info.loaderId ||
// When we navigate all CDP network events have `loaderId`
// CDP's `loaderId` and `requestId` match when
// that request triggered the loading
this.#request.info.loaderId !== this.#request.info.requestId) {
return null;
}
// Get virtual navigation ID from the browsing context.
return this.#networkStorage.getNavigationId(this.#context ?? undefined);
}
get #cookies() {
let cookies = [];
if (this.#request.extraInfo) {
cookies = this.#request.extraInfo.associatedCookies
.filter(({ blockedReasons }) => {
return !Array.isArray(blockedReasons) || blockedReasons.length === 0;
})
.map(({ cookie }) => cdpToBiDiCookie(cookie));
}
return cookies;
}
get #bodySize() {
let bodySize = 0;
if (typeof this.#requestOverrides?.bodySize === 'number') {
bodySize = this.#requestOverrides.bodySize;
}
else {
bodySize = bidiBodySizeFromCdpPostDataEntries(this.#request.info?.request.postDataEntries ?? []);
}
return bodySize;
}
get #context() {
return (this.#response.paused?.frameId ??
this.#request.info?.frameId ??
this.#request.paused?.frameId ??
this.#request.auth?.frameId ??
null);
}
/** Returns the HTTP status code associated with this request if any. */
get #statusCode() {
return (this.#responseOverrides?.statusCode ??
this.#response.paused?.responseStatusCode ??
this.#response.extraInfo?.statusCode ??
this.#response.info?.status);
}
get #requestHeaders() {
let headers = [];
if (this.#requestOverrides?.headers) {
headers = this.#requestOverrides.headers;
}
else {
headers = [
...bidiNetworkHeadersFromCdpNetworkHeaders(this.#request.info?.request.headers),
...bidiNetworkHeadersFromCdpNetworkHeaders(this.#request.extraInfo?.headers),
];
}
return headers;
}
get #authChallenges() {
// TODO: get headers from Fetch.requestPaused
if (!this.#response.info) {
return;
}
if (!(this.#statusCode === 401 || this.#statusCode === 407)) {
return undefined;
}
const headerName = this.#statusCode === 401 ? 'WWW-Authenticate' : 'Proxy-Authenticate';
const authChallenges = [];
for (const [header, value] of Object.entries(this.#response.info.headers)) {
// TODO: Do a proper match based on https://httpwg.org/specs/rfc9110.html#credentials
// Or verify this works
if (header.localeCompare(headerName, undefined, { sensitivity: 'base' }) === 0) {
authChallenges.push({
scheme: value.split(' ').at(0) ?? '',
realm: value.match(REALM_REGEX)?.at(0) ?? '',
});
}
}
return authChallenges;
}
get #timings() {
return {
// TODO: Verify this is correct
timeOrigin: getTiming(this.#response.info?.timing?.requestTime),
requestTime: getTiming(this.#response.info?.timing?.requestTime),
redirectStart: 0,
redirectEnd: 0,
// TODO: Verify this is correct
// https://source.chromium.org/chromium/chromium/src/+/main:net/base/load_timing_info.h;l=145
fetchStart: getTiming(this.#response.info?.timing?.requestTime),
dnsStart: getTiming(this.#response.info?.timing?.dnsStart),
dnsEnd: getTiming(this.#response.info?.timing?.dnsEnd),
connectStart: getTiming(this.#response.info?.timing?.connectStart),
connectEnd: getTiming(this.#response.info?.timing?.connectEnd),
tlsStart: getTiming(this.#response.info?.timing?.sslStart),
requestStart: getTiming(this.#response.info?.timing?.sendStart),
// https://source.chromium.org/chromium/chromium/src/+/main:net/base/load_timing_info.h;l=196
responseStart: getTiming(this.#response.info?.timing?.receiveHeadersStart),
responseEnd: getTiming(this.#response.info?.timing?.receiveHeadersEnd),
};
}
#phaseChanged() {
this.waitNextPhase.resolve();
this.waitNextPhase = new Deferred();
}
#interceptsInPhase(phase) {
if (!this.#cdpTarget.isSubscribedTo(`network.${phase}`)) {
return new Set();
}
return this.#networkStorage.getInterceptsForPhase(this, phase);
}
#isBlockedInPhase(phase) {
return this.#interceptsInPhase(phase).size > 0;
}
handleRedirect(event) {
// TODO: use event.redirectResponse;
// Temporary workaround to emit ResponseCompleted event for redirects
this.#response.hasExtraInfo = false;
this.#response.info = event.redirectResponse;
this.#emitEventsIfReady({
wasRedirected: true,
});
}
#emitEventsIfReady(options = {}) {
const requestExtraInfoCompleted =
// Flush redirects
options.wasRedirected ||
options.hasFailed ||
this.#isDataUrl() ||
Boolean(this.#request.extraInfo) ||
// Requests from cache don't have extra info
this.#servedFromCache ||
// Sometimes there is no extra info and the response
// is the only place we can find out
Boolean(this.#response.info && !this.#response.hasExtraInfo);
const noInterceptionExpected =
// We can't intercept data urls from CDP
this.#isDataUrl() ||
// Cached requests never hit the network
this.#servedFromCache;
const requestInterceptionExpected = !noInterceptionExpected &&
this.#isBlockedInPhase("beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */);
const requestInterceptionCompleted = !requestInterceptionExpected ||
(requestInterceptionExpected && Boolean(this.#request.paused));
if (Boolean(this.#request.info) &&
(requestInterceptionExpected
? requestInterceptionCompleted
: requestExtraInfoCompleted)) {
this.#emitEvent(this.#getBeforeRequestEvent.bind(this));
}
const responseExtraInfoCompleted = Boolean(this.#response.extraInfo) ||
// Response from cache don't have extra info
this.#servedFromCache ||
// Don't expect extra info if the flag is false
Boolean(this.#response.info && !this.#response.hasExtraInfo);
const responseInterceptionExpected = !noInterceptionExpected &&
this.#isBlockedInPhase("responseStarted" /* Network.InterceptPhase.ResponseStarted */);
if (this.#response.info ||
(responseInterceptionExpected && Boolean(this.#response.paused))) {
this.#emitEvent(this.#getResponseStartedEvent.bind(this));
}
const responseInterceptionCompleted = !responseInterceptionExpected ||
(responseInterceptionExpected && Boolean(this.#response.paused));
if (Boolean(this.#response.info) &&
responseExtraInfoCompleted &&
responseInterceptionCompleted) {
this.#emitEvent(this.#getResponseReceivedEvent.bind(this));
this.#networkStorage.deleteRequest(this.id);
}
}
onRequestWillBeSentEvent(event) {
this.#request.info = event;
this.#emitEventsIfReady();
}
onRequestWillBeSentExtraInfoEvent(event) {
this.#request.extraInfo = event;
this.#emitEventsIfReady();
}
onResponseReceivedExtraInfoEvent(event) {
if (event.statusCode >= 300 &&
event.statusCode <= 399 &&
this.#request.info &&
event.headers['location'] === this.#request.info.request.url) {
// We received the Response Extra info for the redirect
// Too late so we need to skip it as it will
// fire wrongly for the last one
return;
}
this.#response.extraInfo = event;
this.#emitEventsIfReady();
}
onResponseReceivedEvent(event) {
this.#response.hasExtraInfo = event.hasExtraInfo;
this.#response.info = event.response;
this.#emitEventsIfReady();
}
onServedFromCache() {
this.#servedFromCache = true;
this.#emitEventsIfReady();
}
onLoadingFailedEvent(event) {
this.#emitEventsIfReady({
hasFailed: true,
});
this.#emitEvent(() => {
return {
method: ChromiumBidi.Network.EventNames.FetchError,
params: {
...this.#getBaseEventParams(),
errorText: event.errorText,
},
};
});
}
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-failRequest */
async failRequest(errorReason) {
assert(this.#fetchId, 'Network Interception not set-up.');
await this.cdpClient.sendCommand('Fetch.failRequest', {
requestId: this.#fetchId,
errorReason,
});
this.#interceptPhase = undefined;
}
onRequestPaused(event) {
this.#fetchId = event.requestId;
// CDP https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#event-requestPaused
if (event.responseStatusCode || event.responseErrorReason) {
this.#response.paused = event;
if (this.#isBlockedInPhase("responseStarted" /* Network.InterceptPhase.ResponseStarted */) &&
// CDP may emit multiple events for a single request
!this.#emittedEvents[ChromiumBidi.Network.EventNames.ResponseStarted] &&
// Continue all response that have not enabled Network domain
this.#fetchId !== this.id) {
this.#interceptPhase = "responseStarted" /* Network.InterceptPhase.ResponseStarted */;
}
else {
void this.#continueResponse();
}
}
else {
this.#request.paused = event;
if (this.#isBlockedInPhase("beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */) &&
// CDP may emit multiple events for a single request
!this.#emittedEvents[ChromiumBidi.Network.EventNames.BeforeRequestSent] &&
// Continue all requests that have not enabled Network domain
this.#fetchId !== this.id) {
this.#interceptPhase = "beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */;
}
else {
void this.#continueRequest();
}
}
this.#emitEventsIfReady();
}
onAuthRequired(event) {
this.#fetchId = event.requestId;
this.#request.auth = event;
if (this.#isBlockedInPhase("authRequired" /* Network.InterceptPhase.AuthRequired */) &&
// Continue all auth requests that have not enabled Network domain
this.#fetchId !== this.id) {
this.#interceptPhase = "authRequired" /* Network.InterceptPhase.AuthRequired */;
}
else {
void this.#continueWithAuth({
response: 'Default',
});
}
this.#emitEvent(() => {
return {
method: ChromiumBidi.Network.EventNames.AuthRequired,
params: {
...this.#getBaseEventParams("authRequired" /* Network.InterceptPhase.AuthRequired */),
response: this.#getResponseEventParams(),
},
};
});
}
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-continueRequest */
async continueRequest(overrides = {}) {
const overrideHeaders = this.#getOverrideHeader(overrides.headers, overrides.cookies);
const headers = cdpFetchHeadersFromBidiNetworkHeaders(overrideHeaders);
const postData = getCdpBodyFromBiDiBytesValue(overrides.body);
await this.#continueRequest({
url: overrides.url,
method: overrides.method,
headers,
postData,
});
this.#requestOverrides = {
url: overrides.url,
method: overrides.method,
headers: overrides.headers,
cookies: overrides.cookies,
bodySize: getSizeFromBiDiBytesValue(overrides.body),
};
}
async #continueRequest(overrides = {}) {
assert(this.#fetchId, 'Network Interception not set-up.');
await this.cdpClient.sendCommand('Fetch.continueRequest', {
requestId: this.#fetchId,
url: overrides.url,
method: overrides.method,
headers: overrides.headers,
postData: overrides.postData,
});
this.#interceptPhase = undefined;
}
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-continueResponse */
async continueResponse(overrides = {}) {
if (this.interceptPhase === "authRequired" /* Network.InterceptPhase.AuthRequired */) {
if (overrides.credentials) {
await Promise.all([
this.waitNextPhase,
await this.#continueWithAuth({
response: 'ProvideCredentials',
username: overrides.credentials.username,
password: overrides.credentials.password,
}),
]);
}
else {
// We need to use `ProvideCredentials`
// As `Default` may cancel the request
return await this.#continueWithAuth({
response: 'ProvideCredentials',
});
}
}
if (this.#interceptPhase === "responseStarted" /* Network.InterceptPhase.ResponseStarted */) {
const overrideHeaders = this.#getOverrideHeader(overrides.headers, overrides.cookies);
const responseHeaders = cdpFetchHeadersFromBidiNetworkHeaders(overrideHeaders);
await this.#continueResponse({
responseCode: overrides.statusCode ?? this.#response.paused?.responseStatusCode,
responsePhrase: overrides.reasonPhrase ?? this.#response.paused?.responseStatusText,
responseHeaders: responseHeaders ?? this.#response.paused?.responseHeaders,
});
this.#responseOverrides = {
statusCode: overrides.statusCode,
headers: overrideHeaders,
};
}
}
async #continueResponse({ responseCode, responsePhrase, responseHeaders, } = {}) {
assert(this.#fetchId, 'Network Interception not set-up.');
await this.cdpClient.sendCommand('Fetch.continueResponse', {
requestId: this.#fetchId,
responseCode,
responsePhrase,
responseHeaders,
});
this.#interceptPhase = undefined;
}
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-continueWithAuth */
async continueWithAuth(authChallenge) {
let username;
let password;
if (authChallenge.action === 'provideCredentials') {
const { credentials } = authChallenge;
username = credentials.username;
password = credentials.password;
}
const response = cdpAuthChallengeResponseFromBidiAuthContinueWithAuthAction(authChallenge.action);
await this.#continueWithAuth({
response,
username,
password,
});
}
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-provideResponse */
async provideResponse(overrides) {
assert(this.#fetchId, 'Network Interception not set-up.');
// We need to pass through if the request is already in
// AuthRequired phase
if (this.interceptPhase === "authRequired" /* Network.InterceptPhase.AuthRequired */) {
// We need to use `ProvideCredentials`
// As `Default` may cancel the request
return await this.#continueWithAuth({
response: 'ProvideCredentials',
});
}
// If we don't modify the response
// just continue the request
if (!overrides.body && !overrides.headers) {
return await this.#continueRequest();
}
const overrideHeaders = this.#getOverrideHeader(overrides.headers, overrides.cookies);
const responseHeaders = cdpFetchHeadersFromBidiNetworkHeaders(overrideHeaders);
const responseCode = overrides.statusCode ?? this.#statusCode ?? 200;
await this.cdpClient.sendCommand('Fetch.fulfillRequest', {
requestId: this.#fetchId,
responseCode,
responsePhrase: overrides.reasonPhrase,
responseHeaders,
body: getCdpBodyFromBiDiBytesValue(overrides.body),
});
this.#interceptPhase = undefined;
}
dispose() {
this.waitNextPhase.reject(new Error('waitNextPhase disposed'));
}
async #continueWithAuth(authChallengeResponse) {
assert(this.#fetchId, 'Network Interception not set-up.');
await this.cdpClient.sendCommand('Fetch.continueWithAuth', {
requestId: this.#fetchId,
authChallengeResponse,
});
this.#interceptPhase = undefined;
}
#emitEvent(getEvent) {
let event;
try {
event = getEvent();
}
catch (error) {
this.#logger?.(LogType.debugError, error);
return;
}
if (this.#isIgnoredEvent() ||
(this.#emittedEvents[event.method] &&
// Special case this event can be emitted multiple times
event.method !== ChromiumBidi.Network.EventNames.AuthRequired)) {
return;
}
this.#phaseChanged();
this.#emittedEvents[event.method] = true;
this.#eventManager.registerEvent(Object.assign(event, {
type: 'event',
}), this.#context);
}
#getBaseEventParams(phase) {
const interceptProps = {
isBlocked: false,
};
if (phase) {
const blockedBy = this.#interceptsInPhase(phase);
interceptProps.isBlocked = blockedBy.size > 0;
if (interceptProps.isBlocked) {
interceptProps.intercepts = [...blockedBy];
}
}
return {
context: this.#context,
navigation: this.#navigationId,
redirectCount: this.#redirectCount,
request: this.#getRequestData(),
// Timestamp should be in milliseconds, while CDP provides it in seconds.
timestamp: Math.round(getTiming(this.#request.info?.wallTime) * 1000),
// Contains isBlocked and intercepts
...interceptProps,
};
}
#getResponseEventParams() {
// Chromium sends wrong extraInfo events for responses served from cache.
// See https://github.com/puppeteer/puppeteer/issues/9965 and
// https://crbug.com/1340398.
if (this.#response.info?.fromDiskCache) {
this.#response.extraInfo = undefined;
}
const headers = [
...bidiNetworkHeadersFromCdpNetworkHeaders(this.#response.info?.headers),
...bidiNetworkHeadersFromCdpNetworkHeaders(this.#response.extraInfo?.headers),
// TODO: Verify how to dedupe these
// ...bidiNetworkHeadersFromCdpNetworkHeadersEntries(
// this.#response.paused?.responseHeaders
// ),
];
const authChallenges = this.#authChallenges;
const response = {
url: this.url,
protocol: this.#response.info?.protocol ?? '',
status: this.#statusCode ?? -1, // TODO: Throw an exception or use some other status code?
statusText: this.#response.info?.statusText ||
this.#response.paused?.responseStatusText ||
'',
fromCache: this.#response.info?.fromDiskCache ||
this.#response.info?.fromPrefetchCache ||
this.#servedFromCache,
headers: this.#responseOverrides?.headers ?? headers,
mimeType: this.#response.info?.mimeType || '',
bytesReceived: this.#response.info?.encodedDataLength || 0,
headersSize: computeHeadersSize(headers),
// TODO: consider removing from spec.
bodySize: 0,
content: {
// TODO: consider removing from spec.
size: 0,
},
...(authChallenges ? { authChallenges } : {}),
};
return {
...response,
'goog:securityDetails': this.#response.info?.securityDetails,
};
}
#getRequestData() {
const headers = this.#requestHeaders;
const request = {
request: this.#id,
url: this.url,
method: this.#method ?? _a.unknownParameter,
headers,
cookies: this.#cookies,
headersSize: computeHeadersSize(headers),
bodySize: this.#bodySize,
// TODO: populate
destination: '',
// TODO: populate
initiatorType: null,
timings: this.#timings,
};
return {
...request,
'goog:postData': this.#request.info?.request?.postData,
'goog:hasPostData': this.#request.info?.request?.hasPostData,
'goog:resourceType': this.#request.info?.type,
};
}
#getBeforeRequestEvent() {
assert(this.#request.info, 'RequestWillBeSentEvent is not set');
return {
method: ChromiumBidi.Network.EventNames.BeforeRequestSent,
params: {
...this.#getBaseEventParams("beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */),
initiator: {
type: _a.#getInitiatorType(this.#request.info.initiator.type),
columnNumber: this.#request.info.initiator.columnNumber,
lineNumber: this.#request.info.initiator.lineNumber,
stackTrace: this.#request.info.initiator.stack,
request: this.#request.info.initiator.requestId,
},
},
};
}
#getResponseStartedEvent() {
return {
method: ChromiumBidi.Network.EventNames.ResponseStarted,
params: {
...this.#getBaseEventParams("responseStarted" /* Network.InterceptPhase.ResponseStarted */),
response: this.#getResponseEventParams(),
},
};
}
#getResponseReceivedEvent() {
return {
method: ChromiumBidi.Network.EventNames.ResponseCompleted,
params: {
...this.#getBaseEventParams(),
response: this.#getResponseEventParams(),
},
};
}
#isIgnoredEvent() {
const faviconUrl = '/favicon.ico';
return (this.#request.paused?.request.url.endsWith(faviconUrl) ??
this.#request.info?.request.url.endsWith(faviconUrl) ??
false);
}
#getOverrideHeader(headers, cookies) {
if (!headers && !cookies) {
return undefined;
}
let overrideHeaders = headers;
const cookieHeader = networkHeaderFromCookieHeaders(cookies);
if (cookieHeader && !overrideHeaders) {
overrideHeaders = this.#requestHeaders;
}
if (cookieHeader && overrideHeaders) {
overrideHeaders.filter((header) => header.name.localeCompare('cookie', undefined, {
sensitivity: 'base',
}) !== 0);
overrideHeaders.push(cookieHeader);
}
return overrideHeaders;
}
static #getInitiatorType(initiatorType) {
switch (initiatorType) {
case 'parser':
case 'script':
case 'preflight':
return initiatorType;
default:
return 'other';
}
}
}
_a = NetworkRequest;
function getCdpBodyFromBiDiBytesValue(body) {
let parsedBody;
if (body?.type === 'string') {
parsedBody = btoa(body.value);
}
else if (body?.type === 'base64') {
parsedBody = body.value;
}
return parsedBody;
}
function getSizeFromBiDiBytesValue(body) {
if (body?.type === 'string') {
return body.value.length;
}
else if (body?.type === 'base64') {
return atob(body.value).length;
}
return 0;
}
//# sourceMappingURL=NetworkRequest.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,48 @@
import { type BrowsingContext, Network } from '../../../protocol/protocol.js';
import type { LoggerFn } from '../../../utils/log.js';
import type { CdpClient } from '../../BidiMapper.js';
import type { CdpTarget } from '../cdp/CdpTarget.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage';
import type { EventManager } from '../session/EventManager.js';
import { NetworkRequest } from './NetworkRequest.js';
import { type ParsedUrlPattern } from './NetworkUtils.js';
type NetworkInterception = Omit<Network.AddInterceptParameters, 'urlPatterns'> & {
urlPatterns: ParsedUrlPattern[];
};
/** Stores network and intercept maps. */
export declare class NetworkStorage {
#private;
constructor(eventManager: EventManager, browsingContextStorage: BrowsingContextStorage, browserClient: CdpClient, logger?: LoggerFn);
onCdpTargetCreated(cdpTarget: CdpTarget): void;
getInterceptionStages(browsingContextId: BrowsingContext.BrowsingContext): {
request: boolean;
response: boolean;
auth: boolean;
};
getInterceptsForPhase(request: NetworkRequest, phase: Network.InterceptPhase): Set<Network.Intercept>;
disposeRequestMap(sessionId: string): void;
/**
* Adds the given entry to the intercept map.
* URL patterns are assumed to be parsed.
*
* @return The intercept ID.
*/
addIntercept(value: NetworkInterception): Network.Intercept;
/**
* Removes the given intercept from the intercept map.
* Throws NoSuchInterceptException if the intercept does not exist.
*/
removeIntercept(intercept: Network.Intercept): void;
getRequestsByTarget(target: CdpTarget): NetworkRequest[];
getRequestById(id: Network.Request): NetworkRequest | undefined;
getRequestByFetchId(fetchId: Network.Request): NetworkRequest | undefined;
addRequest(request: NetworkRequest): void;
deleteRequest(id: Network.Request): void;
/**
* Gets the virtual navigation ID for the given navigable ID.
*/
getNavigationId(contextId: string | undefined): string | null;
set defaultCacheBehavior(behavior: Network.SetCacheBehaviorParameters['cacheBehavior']);
get defaultCacheBehavior(): Network.SetCacheBehaviorParameters["cacheBehavior"];
}
export {};

View File

@@ -0,0 +1,222 @@
import { NoSuchInterceptException, } from '../../../protocol/protocol.js';
import { uuidv4 } from '../../../utils/uuid.js';
import { NetworkRequest } from './NetworkRequest.js';
import { matchUrlPattern } from './NetworkUtils.js';
/** Stores network and intercept maps. */
export class NetworkStorage {
#browsingContextStorage;
#eventManager;
#logger;
/**
* A map from network request ID to Network Request objects.
* Needed as long as information about requests comes from different events.
*/
#requests = new Map();
/** A map from intercept ID to track active network intercepts. */
#intercepts = new Map();
#defaultCacheBehavior = 'default';
constructor(eventManager, browsingContextStorage, browserClient, logger) {
this.#browsingContextStorage = browsingContextStorage;
this.#eventManager = eventManager;
browserClient.on('Target.detachedFromTarget', ({ sessionId }) => {
this.disposeRequestMap(sessionId);
});
this.#logger = logger;
}
/**
* Gets the network request with the given ID, if any.
* Otherwise, creates a new network request with the given ID and cdp target.
*/
#getOrCreateNetworkRequest(id, cdpTarget, redirectCount) {
let request = this.getRequestById(id);
if (request) {
return request;
}
request = new NetworkRequest(id, this.#eventManager, this, cdpTarget, redirectCount, this.#logger);
this.addRequest(request);
return request;
}
onCdpTargetCreated(cdpTarget) {
const cdpClient = cdpTarget.cdpClient;
// TODO: Wrap into object
const listeners = [
[
'Network.requestWillBeSent',
(params) => {
const request = this.getRequestById(params.requestId);
if (request && request.isRedirecting()) {
request.handleRedirect(params);
this.deleteRequest(params.requestId);
this.#getOrCreateNetworkRequest(params.requestId, cdpTarget, request.redirectCount + 1).onRequestWillBeSentEvent(params);
}
else {
this.#getOrCreateNetworkRequest(params.requestId, cdpTarget).onRequestWillBeSentEvent(params);
}
},
],
[
'Network.requestWillBeSentExtraInfo',
(params) => {
this.#getOrCreateNetworkRequest(params.requestId, cdpTarget).onRequestWillBeSentExtraInfoEvent(params);
},
],
[
'Network.responseReceived',
(params) => {
this.#getOrCreateNetworkRequest(params.requestId, cdpTarget).onResponseReceivedEvent(params);
},
],
[
'Network.responseReceivedExtraInfo',
(params) => {
this.#getOrCreateNetworkRequest(params.requestId, cdpTarget).onResponseReceivedExtraInfoEvent(params);
},
],
[
'Network.requestServedFromCache',
(params) => {
this.#getOrCreateNetworkRequest(params.requestId, cdpTarget).onServedFromCache();
},
],
[
'Network.loadingFailed',
(params) => {
this.#getOrCreateNetworkRequest(params.requestId, cdpTarget).onLoadingFailedEvent(params);
},
],
[
'Fetch.requestPaused',
(event) => {
this.#getOrCreateNetworkRequest(
// CDP quirk if the Network domain is not present this is undefined
event.networkId ?? event.requestId, cdpTarget).onRequestPaused(event);
},
],
[
'Fetch.authRequired',
(event) => {
let request = this.getRequestByFetchId(event.requestId);
if (!request) {
request = this.#getOrCreateNetworkRequest(event.requestId, cdpTarget);
}
request.onAuthRequired(event);
},
],
];
for (const [event, listener] of listeners) {
cdpClient.on(event, listener);
}
}
getInterceptionStages(browsingContextId) {
const stages = {
request: false,
response: false,
auth: false,
};
for (const intercept of this.#intercepts.values()) {
if (intercept.contexts &&
!intercept.contexts.includes(browsingContextId)) {
continue;
}
stages.request ||= intercept.phases.includes("beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */);
stages.response ||= intercept.phases.includes("responseStarted" /* Network.InterceptPhase.ResponseStarted */);
stages.auth ||= intercept.phases.includes("authRequired" /* Network.InterceptPhase.AuthRequired */);
}
return stages;
}
getInterceptsForPhase(request, phase) {
if (request.url === NetworkRequest.unknownParameter) {
return new Set();
}
const intercepts = new Set();
for (const [interceptId, intercept] of this.#intercepts.entries()) {
if (!intercept.phases.includes(phase) ||
(intercept.contexts &&
!intercept.contexts.includes(request.cdpTarget.topLevelId))) {
continue;
}
if (intercept.urlPatterns.length === 0) {
intercepts.add(interceptId);
continue;
}
for (const pattern of intercept.urlPatterns) {
if (matchUrlPattern(pattern, request.url)) {
intercepts.add(interceptId);
break;
}
}
}
return intercepts;
}
disposeRequestMap(sessionId) {
for (const request of this.#requests.values()) {
if (request.cdpClient.sessionId === sessionId) {
this.#requests.delete(request.id);
request.dispose();
}
}
}
/**
* Adds the given entry to the intercept map.
* URL patterns are assumed to be parsed.
*
* @return The intercept ID.
*/
addIntercept(value) {
const interceptId = uuidv4();
this.#intercepts.set(interceptId, value);
return interceptId;
}
/**
* Removes the given intercept from the intercept map.
* Throws NoSuchInterceptException if the intercept does not exist.
*/
removeIntercept(intercept) {
if (!this.#intercepts.has(intercept)) {
throw new NoSuchInterceptException(`Intercept '${intercept}' does not exist.`);
}
this.#intercepts.delete(intercept);
}
getRequestsByTarget(target) {
const requests = [];
for (const request of this.#requests.values()) {
if (request.cdpTarget === target) {
requests.push(request);
}
}
return requests;
}
getRequestById(id) {
return this.#requests.get(id);
}
getRequestByFetchId(fetchId) {
for (const request of this.#requests.values()) {
if (request.fetchId === fetchId) {
return request;
}
}
return;
}
addRequest(request) {
this.#requests.set(request.id, request);
}
deleteRequest(id) {
this.#requests.delete(id);
}
/**
* Gets the virtual navigation ID for the given navigable ID.
*/
getNavigationId(contextId) {
if (contextId === undefined) {
return null;
}
return (this.#browsingContextStorage.findContext(contextId)?.navigationId ?? null);
}
set defaultCacheBehavior(behavior) {
this.#defaultCacheBehavior = behavior;
}
get defaultCacheBehavior() {
return this.#defaultCacheBehavior;
}
}
//# sourceMappingURL=NetworkStorage.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,58 @@
/**
* @fileoverview Utility functions for the Network module.
*/
import type { Protocol } from 'devtools-protocol';
import { Network, type Storage } from '../../../protocol/protocol.js';
export declare function computeHeadersSize(headers: Network.Header[]): number;
/** Converts from CDP Network domain headers to BiDi network headers. */
export declare function bidiNetworkHeadersFromCdpNetworkHeaders(headers?: Protocol.Network.Headers): Network.Header[];
/** Converts from CDP Fetch domain headers to BiDi network headers. */
export declare function bidiNetworkHeadersFromCdpNetworkHeadersEntries(headers?: Protocol.Fetch.HeaderEntry[]): Network.Header[];
/** Converts from Bidi network headers to CDP Network domain headers. */
export declare function cdpNetworkHeadersFromBidiNetworkHeaders(headers?: Network.Header[]): Protocol.Network.Headers | undefined;
/** Converts from CDP Fetch domain header entries to Bidi network headers. */
export declare function bidiNetworkHeadersFromCdpFetchHeaders(headers?: Protocol.Fetch.HeaderEntry[]): Network.Header[];
/** Converts from Bidi network headers to CDP Fetch domain header entries. */
export declare function cdpFetchHeadersFromBidiNetworkHeaders(headers?: Network.Header[]): Protocol.Fetch.HeaderEntry[] | undefined;
export declare function networkHeaderFromCookieHeaders(headers?: Network.CookieHeader[]): Network.Header | undefined;
/** Converts from Bidi auth action to CDP auth challenge response. */
export declare function cdpAuthChallengeResponseFromBidiAuthContinueWithAuthAction(action: 'default' | 'cancel' | 'provideCredentials'): "Default" | "CancelAuth" | "ProvideCredentials";
/**
* Converts from CDP Network domain cookie to BiDi network cookie.
* * https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-Cookie
* * https://w3c.github.io/webdriver-bidi/#type-network-Cookie
*/
export declare function cdpToBiDiCookie(cookie: Protocol.Network.Cookie): Network.Cookie;
/**
* Decodes a byte value to a string.
* @param {Network.BytesValue} value
* @return {string}
*/
export declare function deserializeByteValue(value: Network.BytesValue): string;
/**
* Converts from BiDi set network cookie params to CDP Network domain cookie.
* * https://w3c.github.io/webdriver-bidi/#type-network-Cookie
* * https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-CookieParam
*/
export declare function bidiToCdpCookie(params: Storage.SetCookieParameters, partitionKey: Storage.PartitionKey): Protocol.Network.CookieParam;
export declare function sameSiteBiDiToCdp(sameSite: Network.SameSite): Protocol.Network.CookieSameSite;
/**
* Returns true if the given protocol is special.
* Special protocols are those that have a default port.
*
* Example inputs: 'http', 'http:'
*
* @see https://url.spec.whatwg.org/#special-scheme
*/
export declare function isSpecialScheme(protocol: string): boolean;
export type ParsedUrlPattern = {
protocol?: string;
hostname?: string;
port?: string;
pathname?: string;
search?: string;
};
/** Matches the given URLPattern against the given URL. */
export declare function matchUrlPattern(pattern: ParsedUrlPattern, url: string): boolean;
export declare function bidiBodySizeFromCdpPostDataEntries(entries: Protocol.Network.PostDataEntry[]): number;
export declare function getTiming(timing: number | undefined): number;

View File

@@ -0,0 +1,289 @@
/*
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { InvalidArgumentException } from '../../../protocol/ErrorResponse.js';
import { base64ToString } from '../../../utils/Base64.js';
export function computeHeadersSize(headers) {
const requestHeaders = headers.reduce((acc, header) => {
return `${acc}${header.name}: ${header.value.value}\r\n`;
}, '');
return new TextEncoder().encode(requestHeaders).length;
}
/** Converts from CDP Network domain headers to BiDi network headers. */
export function bidiNetworkHeadersFromCdpNetworkHeaders(headers) {
if (!headers) {
return [];
}
return Object.entries(headers).map(([name, value]) => ({
name,
value: {
type: 'string',
value,
},
}));
}
/** Converts from CDP Fetch domain headers to BiDi network headers. */
export function bidiNetworkHeadersFromCdpNetworkHeadersEntries(headers) {
if (!headers) {
return [];
}
return headers.map(({ name, value }) => ({
name,
value: {
type: 'string',
value,
},
}));
}
/** Converts from Bidi network headers to CDP Network domain headers. */
export function cdpNetworkHeadersFromBidiNetworkHeaders(headers) {
if (headers === undefined) {
return undefined;
}
return headers.reduce((result, header) => {
// TODO: Distinguish between string and bytes?
result[header.name] = header.value.value;
return result;
}, {});
}
/** Converts from CDP Fetch domain header entries to Bidi network headers. */
export function bidiNetworkHeadersFromCdpFetchHeaders(headers) {
if (!headers) {
return [];
}
return headers.map(({ name, value }) => ({
name,
value: {
type: 'string',
value,
},
}));
}
/** Converts from Bidi network headers to CDP Fetch domain header entries. */
export function cdpFetchHeadersFromBidiNetworkHeaders(headers) {
if (headers === undefined) {
return undefined;
}
return headers.map(({ name, value }) => ({
name,
value: value.value,
}));
}
export function networkHeaderFromCookieHeaders(headers) {
if (headers === undefined) {
return undefined;
}
const value = headers.reduce((acc, value, index) => {
if (index > 0) {
acc += ';';
}
const cookieValue = value.value.type === 'base64'
? btoa(value.value.value)
: value.value.value;
acc += `${value.name}=${cookieValue}`;
return acc;
}, '');
return {
name: 'Cookie',
value: {
type: 'string',
value,
},
};
}
/** Converts from Bidi auth action to CDP auth challenge response. */
export function cdpAuthChallengeResponseFromBidiAuthContinueWithAuthAction(action) {
switch (action) {
case 'default':
return 'Default';
case 'cancel':
return 'CancelAuth';
case 'provideCredentials':
return 'ProvideCredentials';
}
}
/**
* Converts from CDP Network domain cookie to BiDi network cookie.
* * https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-Cookie
* * https://w3c.github.io/webdriver-bidi/#type-network-Cookie
*/
export function cdpToBiDiCookie(cookie) {
const result = {
name: cookie.name,
value: { type: 'string', value: cookie.value },
domain: cookie.domain,
path: cookie.path,
size: cookie.size,
httpOnly: cookie.httpOnly,
secure: cookie.secure,
sameSite: cookie.sameSite === undefined
? "none" /* Network.SameSite.None */
: sameSiteCdpToBiDi(cookie.sameSite),
...(cookie.expires >= 0 ? { expiry: cookie.expires } : undefined),
};
// Extending with CDP-specific properties with `goog:` prefix.
result[`goog:session`] = cookie.session;
result[`goog:priority`] = cookie.priority;
result[`goog:sameParty`] = cookie.sameParty;
result[`goog:sourceScheme`] = cookie.sourceScheme;
result[`goog:sourcePort`] = cookie.sourcePort;
if (cookie.partitionKey !== undefined) {
result[`goog:partitionKey`] = cookie.partitionKey;
}
if (cookie.partitionKeyOpaque !== undefined) {
result[`goog:partitionKeyOpaque`] = cookie.partitionKeyOpaque;
}
return result;
}
/**
* Decodes a byte value to a string.
* @param {Network.BytesValue} value
* @return {string}
*/
export function deserializeByteValue(value) {
if (value.type === 'base64') {
return base64ToString(value.value);
}
return value.value;
}
/**
* Converts from BiDi set network cookie params to CDP Network domain cookie.
* * https://w3c.github.io/webdriver-bidi/#type-network-Cookie
* * https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-CookieParam
*/
export function bidiToCdpCookie(params, partitionKey) {
const deserializedValue = deserializeByteValue(params.cookie.value);
const result = {
name: params.cookie.name,
value: deserializedValue,
domain: params.cookie.domain,
path: params.cookie.path ?? '/',
secure: params.cookie.secure ?? false,
httpOnly: params.cookie.httpOnly ?? false,
...(partitionKey.sourceOrigin !== undefined && {
partitionKey: {
hasCrossSiteAncestor: false,
// CDP's `partitionKey.topLevelSite` is the BiDi's `partition.sourceOrigin`.
topLevelSite: partitionKey.sourceOrigin,
},
}),
...(params.cookie.expiry !== undefined && {
expires: params.cookie.expiry,
}),
...(params.cookie.sameSite !== undefined && {
sameSite: sameSiteBiDiToCdp(params.cookie.sameSite),
}),
};
// Extending with CDP-specific properties with `goog:` prefix.
if (params.cookie[`goog:url`] !== undefined) {
result.url = params.cookie[`goog:url`];
}
if (params.cookie[`goog:priority`] !== undefined) {
result.priority = params.cookie[`goog:priority`];
}
if (params.cookie[`goog:sameParty`] !== undefined) {
result.sameParty = params.cookie[`goog:sameParty`];
}
if (params.cookie[`goog:sourceScheme`] !== undefined) {
result.sourceScheme = params.cookie[`goog:sourceScheme`];
}
if (params.cookie[`goog:sourcePort`] !== undefined) {
result.sourcePort = params.cookie[`goog:sourcePort`];
}
return result;
}
function sameSiteCdpToBiDi(sameSite) {
switch (sameSite) {
case 'Strict':
return "strict" /* Network.SameSite.Strict */;
case 'None':
return "none" /* Network.SameSite.None */;
case 'Lax':
return "lax" /* Network.SameSite.Lax */;
default:
// Defaults to `Lax`:
// https://web.dev/articles/samesite-cookies-explained#samesitelax_by_default
return "lax" /* Network.SameSite.Lax */;
}
}
export function sameSiteBiDiToCdp(sameSite) {
switch (sameSite) {
case "strict" /* Network.SameSite.Strict */:
return 'Strict';
case "lax" /* Network.SameSite.Lax */:
return 'Lax';
case "none" /* Network.SameSite.None */:
return 'None';
}
throw new InvalidArgumentException(`Unknown 'sameSite' value ${sameSite}`);
}
/**
* Returns true if the given protocol is special.
* Special protocols are those that have a default port.
*
* Example inputs: 'http', 'http:'
*
* @see https://url.spec.whatwg.org/#special-scheme
*/
export function isSpecialScheme(protocol) {
return ['ftp', 'file', 'http', 'https', 'ws', 'wss'].includes(protocol.replace(/:$/, ''));
}
function getScheme(url) {
return url.protocol.replace(/:$/, '');
}
/** Matches the given URLPattern against the given URL. */
export function matchUrlPattern(pattern, url) {
// Roughly https://w3c.github.io/webdriver-bidi/#match-url-pattern
// plus some differences based on the URL parsing methods.
const parsedUrl = new URL(url);
if (pattern.protocol !== undefined &&
pattern.protocol !== getScheme(parsedUrl)) {
return false;
}
if (pattern.hostname !== undefined &&
pattern.hostname !== parsedUrl.hostname) {
return false;
}
if (pattern.port !== undefined && pattern.port !== parsedUrl.port) {
return false;
}
if (pattern.pathname !== undefined &&
pattern.pathname !== parsedUrl.pathname) {
return false;
}
if (pattern.search !== undefined && pattern.search !== parsedUrl.search) {
return false;
}
return true;
}
export function bidiBodySizeFromCdpPostDataEntries(entries) {
let size = 0;
for (const entry of entries) {
size += atob(entry.bytes ?? '').length;
}
return size;
}
export function getTiming(timing) {
if (!timing) {
return 0;
}
if (timing < 0) {
return 0;
}
return timing;
}
//# sourceMappingURL=NetworkUtils.js.map

File diff suppressed because one or more lines are too long