Next.js website for Rocky Mountain Vending company featuring: - Product catalog with Stripe integration - Service areas and parts pages - Admin dashboard with Clerk authentication - SEO optimized pages with JSON-LD structured data Co-authored-by: Cursor <cursoragent@cursor.com>
867 lines
No EOL
35 KiB
Text
867 lines
No EOL
35 KiB
Text
/*
|
|
* 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 { DefaultMap } from '../../../utils/DefaultMap.js';
|
|
import { Deferred } from '../../../utils/Deferred.js';
|
|
import { LogType } from '../../../utils/log.js';
|
|
import { bidiBodySizeFromCdpPostDataEntries, bidiNetworkHeadersFromCdpNetworkHeaders, cdpAuthChallengeResponseFromBidiAuthContinueWithAuthAction, cdpFetchHeadersFromBidiNetworkHeaders, cdpToBiDiCookie, computeHeadersSize, getTiming, networkHeaderFromCookieHeaders, stringToBase64, } 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.paused?.request.url ??
|
|
this.#requestOverrides?.url ??
|
|
this.#response.info?.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;
|
|
}
|
|
/** CdpTarget can be changed when frame is moving out of process. */
|
|
updateCdpTarget(cdpTarget) {
|
|
if (cdpTarget !== this.#cdpTarget) {
|
|
this.#logger?.(LogType.debugInfo, `Request ${this.id} was moved from ${this.#cdpTarget.id} to ${cdpTarget.id}`);
|
|
this.#cdpTarget = cdpTarget;
|
|
}
|
|
}
|
|
get cdpClient() {
|
|
return this.#cdpTarget.cdpClient;
|
|
}
|
|
isRedirecting() {
|
|
return Boolean(this.#request.info);
|
|
}
|
|
#isDataUrl() {
|
|
return this.url.startsWith('data:');
|
|
}
|
|
#isNonInterceptable() {
|
|
return (
|
|
// We can't intercept data urls from CDP
|
|
this.#isDataUrl() ||
|
|
// Cached requests never hit the network
|
|
this.#servedFromCache);
|
|
}
|
|
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;
|
|
}
|
|
#getBodySizeFromHeaders(headers) {
|
|
if (headers === undefined) {
|
|
return undefined;
|
|
}
|
|
if (headers['Content-Length'] !== undefined) {
|
|
const bodySize = Number.parseInt(headers['Content-Length']);
|
|
if (Number.isInteger(bodySize)) {
|
|
return bodySize;
|
|
}
|
|
this.#logger?.(LogType.debugError, "Unexpected non-integer 'Content-Length' header");
|
|
}
|
|
// TODO: process `Transfer-Encoding: chunked` case properly.
|
|
return undefined;
|
|
}
|
|
get bodySize() {
|
|
if (typeof this.#requestOverrides?.bodySize === 'number') {
|
|
return this.#requestOverrides.bodySize;
|
|
}
|
|
if (this.#request.info?.request.postDataEntries !== undefined) {
|
|
return bidiBodySizeFromCdpPostDataEntries(this.#request.info?.request.postDataEntries);
|
|
}
|
|
// Try to guess the body size based on the `Content-Length` header.
|
|
return (this.#getBodySizeFromHeaders(this.#request.info?.request.headers) ??
|
|
this.#getBodySizeFromHeaders(this.#request.extraInfo?.headers) ??
|
|
0);
|
|
}
|
|
get #context() {
|
|
const result = this.#response.paused?.frameId ??
|
|
this.#request.info?.frameId ??
|
|
this.#request.paused?.frameId ??
|
|
this.#request.auth?.frameId;
|
|
if (result !== undefined) {
|
|
return result;
|
|
}
|
|
// Heuristic for associating a preflight request with context via it's initiator
|
|
// request. Useful for preflight requests.
|
|
// https://github.com/GoogleChromeLabs/chromium-bidi/issues/3570
|
|
if (this.#request?.info?.initiator.type === 'preflight' &&
|
|
this.#request?.info?.initiator.requestId !== undefined) {
|
|
const maybeInitiator = this.#networkStorage.getRequestById(this.#request?.info?.initiator.requestId);
|
|
if (maybeInitiator !== undefined) {
|
|
return maybeInitiator.#request.info?.frameId ?? null;
|
|
}
|
|
}
|
|
return 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) {
|
|
const headerMap = new DefaultMap(() => []);
|
|
for (const header of this.#requestOverrides.headers) {
|
|
headerMap.get(header.name).push(header.value.value);
|
|
}
|
|
for (const [name, value] of headerMap.entries()) {
|
|
headers.push({
|
|
name,
|
|
value: {
|
|
type: 'string',
|
|
value: value.join('\n').trimEnd(),
|
|
},
|
|
});
|
|
}
|
|
}
|
|
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() {
|
|
// The timing in the CDP events are provided relative to the event's baseline.
|
|
// However, the baseline can be different for different events, and the events have to
|
|
// be normalized throughout resource events. Normalize events timestamps by the
|
|
// request.
|
|
// TODO: Verify this is correct.
|
|
const responseTimeOffset = getTiming(getTiming(this.#response.info?.timing?.requestTime) -
|
|
getTiming(this.#request.info?.timestamp));
|
|
return {
|
|
// TODO: Verify this is correct
|
|
timeOrigin: Math.round(getTiming(this.#request.info?.wallTime) * 1000),
|
|
// Timing baseline.
|
|
// TODO: Verify this is correct.
|
|
requestTime: 0,
|
|
// TODO: set if redirect detected.
|
|
redirectStart: 0,
|
|
// TODO: set if redirect detected.
|
|
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?.workerFetchStart, responseTimeOffset),
|
|
// fetchStart: 0,
|
|
dnsStart: getTiming(this.#response.info?.timing?.dnsStart, responseTimeOffset),
|
|
dnsEnd: getTiming(this.#response.info?.timing?.dnsEnd, responseTimeOffset),
|
|
connectStart: getTiming(this.#response.info?.timing?.connectStart, responseTimeOffset),
|
|
connectEnd: getTiming(this.#response.info?.timing?.connectEnd, responseTimeOffset),
|
|
tlsStart: getTiming(this.#response.info?.timing?.sslStart, responseTimeOffset),
|
|
requestStart: getTiming(this.#response.info?.timing?.sendStart, responseTimeOffset),
|
|
// https://source.chromium.org/chromium/chromium/src/+/main:net/base/load_timing_info.h;l=196
|
|
responseStart: getTiming(this.#response.info?.timing?.receiveHeadersStart, responseTimeOffset),
|
|
responseEnd: getTiming(this.#response.info?.timing?.receiveHeadersEnd, responseTimeOffset),
|
|
};
|
|
}
|
|
#phaseChanged() {
|
|
this.waitNextPhase.resolve();
|
|
this.waitNextPhase = new Deferred();
|
|
}
|
|
#interceptsInPhase(phase) {
|
|
if (this.#isNonInterceptable() ||
|
|
!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 = this.#isNonInterceptable();
|
|
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.disposeRequest(this.id);
|
|
}
|
|
}
|
|
onRequestWillBeSentEvent(event) {
|
|
this.#request.info = event;
|
|
this.#networkStorage.collectIfNeeded(this, "request" /* Network.DataType.Request */);
|
|
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.#networkStorage.collectIfNeeded(this, "response" /* Network.DataType.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;
|
|
if (this.#context) {
|
|
this.#eventManager.registerEvent(Object.assign(event, {
|
|
type: 'event',
|
|
}), this.#context);
|
|
}
|
|
else {
|
|
this.#eventManager.registerGlobalEvent(Object.assign(event, {
|
|
type: 'event',
|
|
}));
|
|
}
|
|
}
|
|
#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;
|
|
}
|
|
// TODO: Also this.#response.paused?.responseHeaders have to be merged here.
|
|
const cdpHeaders = this.#response.info?.headers ?? {};
|
|
const cdpRawHeaders = this.#response.extraInfo?.headers ?? {};
|
|
for (const [key, value] of Object.entries(cdpRawHeaders)) {
|
|
cdpHeaders[key] = value;
|
|
}
|
|
const headers = bidiNetworkHeadersFromCdpNetworkHeaders(cdpHeaders);
|
|
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.bytesReceived,
|
|
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,
|
|
};
|
|
}
|
|
get bytesReceived() {
|
|
return this.#response.info?.encodedDataLength || 0;
|
|
}
|
|
#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: this.#getDestination(),
|
|
// TODO: populate
|
|
initiatorType: this.#getInitiatorType(),
|
|
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,
|
|
'goog:resourceInitiator': this.#request.info?.initiator,
|
|
};
|
|
}
|
|
/**
|
|
* Heuristic trying to guess the destination.
|
|
* Specification: https://fetch.spec.whatwg.org/#concept-request-destination.
|
|
* Specified values: "audio", "audioworklet", "document", "embed", "font", "frame",
|
|
* "iframe", "image", "json", "manifest", "object", "paintworklet", "report", "script",
|
|
* "serviceworker", "sharedworker", "style", "track", "video", "webidentity", "worker",
|
|
* "xslt".
|
|
*/
|
|
#getDestination() {
|
|
switch (this.#request.info?.type) {
|
|
case 'Script':
|
|
return 'script';
|
|
case 'Stylesheet':
|
|
return 'style';
|
|
case 'Image':
|
|
return 'image';
|
|
case 'Document':
|
|
// If request to document is initiated by parser, assume it is expected to
|
|
// arrive in an iframe. Otherwise, consider it is a navigation and the request
|
|
// result will end up in the document.
|
|
return this.#request.info?.initiator.type === 'parser'
|
|
? 'iframe'
|
|
: 'document';
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
/**
|
|
* Heuristic trying to guess the initiator type.
|
|
* Specification: https://fetch.spec.whatwg.org/#request-initiator-type.
|
|
* Specified values: "audio", "beacon", "body", "css", "early-hints", "embed", "fetch",
|
|
* "font", "frame", "iframe", "image", "img", "input", "link", "object", "ping",
|
|
* "script", "track", "video", "xmlhttprequest", "other".
|
|
*/
|
|
#getInitiatorType() {
|
|
if (this.#request.info?.initiator.type === 'parser') {
|
|
switch (this.#request.info?.type) {
|
|
case 'Document':
|
|
// The request to document is initiated by the parser. Assuming it's an iframe.
|
|
return 'iframe';
|
|
case 'Font':
|
|
// If the document's url is not the parser's url, assume the resource is loaded
|
|
// from css. Otherwise, it's a `font` element.
|
|
return this.#request.info?.initiator?.url ===
|
|
this.#request.info?.documentURL
|
|
? 'font'
|
|
: 'css';
|
|
case 'Image':
|
|
// If the document's url is not the parser's url, assume the resource is loaded
|
|
// from css. Otherwise, it's a `img` element.
|
|
return this.#request.info?.initiator?.url ===
|
|
this.#request.info?.documentURL
|
|
? 'img'
|
|
: 'css';
|
|
case 'Script':
|
|
return 'script';
|
|
case 'Stylesheet':
|
|
return 'link';
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
if (this.#request?.info?.type === 'Fetch') {
|
|
return 'fetch';
|
|
}
|
|
return null;
|
|
}
|
|
#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.#getInitiator(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 #getInitiator(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 = stringToBase64(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 |