UNPKG

chrome-devtools-frontend

Version:
1,245 lines (1,111 loc) • 110 kB
// Copyright 2011 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import type * as ProtocolProxyApi from '../../generated/protocol-proxy-api.js'; import * as Protocol from '../../generated/protocol.js'; import * as TextUtils from '../../models/text_utils/text_utils.js'; import * as Common from '../common/common.js'; import * as i18n from '../i18n/i18n.js'; import * as Platform from '../platform/platform.js'; import * as Root from '../root/root.js'; import {Cookie} from './Cookie.js'; import { type BlockedCookieWithReason, DirectSocketChunkType, DirectSocketStatus, DirectSocketType, Events as NetworkRequestEvents, type ExtraRequestInfo, type ExtraResponseInfo, type IncludedCookieWithReason, type NameValue, NetworkRequest, } from './NetworkRequest.js'; import {SDKModel} from './SDKModel.js'; import {Capability, type Target} from './Target.js'; import {type SDKModelObserver, TargetManager} from './TargetManager.js'; const UIStrings = { /** * @description Explanation why no content is shown for WebSocket connection. */ noContentForWebSocket: 'Content for WebSockets is currently not supported', /** * @description Explanation why no content is shown for redirect response. */ noContentForRedirect: 'No content available because this request was redirected', /** * @description Explanation why no content is shown for preflight request. */ noContentForPreflight: 'No content available for preflight request', /** * @description Text to indicate that network throttling is disabled */ noThrottling: 'No throttling', /** * @description Text to indicate the network connectivity is offline */ offline: 'Offline', /** * @description Text in Network Manager representing the "3G" throttling preset. */ slowG: '3G', // Named `slowG` for legacy reasons and because this value // is serialized locally on the user's machine: if we // change it we break their stored throttling settings. // (See crrev.com/c/2947255) /** * @description Text in Network Manager representing the "Slow 4G" throttling preset */ fastG: 'Slow 4G', // Named `fastG` for legacy reasons and because this value // is serialized locally on the user's machine: if we // change it we break their stored throttling settings. // (See crrev.com/c/2947255) /** * @description Text in Network Manager representing the "Fast 4G" throttling preset */ fast4G: 'Fast 4G', /** * @description Text in Network Manager representing the "Blocking" throttling preset */ block: 'Block', /** * @description Text in Network Manager * @example {https://example.com} PH1 */ requestWasBlockedByDevtoolsS: 'Request was blocked by DevTools: "{PH1}"', /** * @description Message in Network Manager * @example {XHR} PH1 * @example {GET} PH2 * @example {https://example.com} PH3 */ sFailedLoadingSS: '{PH1} failed loading: {PH2} "{PH3}".', /** * @description Message in Network Manager * @example {XHR} PH1 * @example {GET} PH2 * @example {https://example.com} PH3 */ sFinishedLoadingSS: '{PH1} finished loading: {PH2} "{PH3}".', /** * @description One of direct socket connection statuses */ directSocketStatusOpening: 'Opening', /** * @description One of direct socket connection statuses */ directSocketStatusOpen: 'Open', /** * @description One of direct socket connection statuses */ directSocketStatusClosed: 'Closed', /** * @description One of direct socket connection statuses */ directSocketStatusAborted: 'Aborted', } as const; const str_ = i18n.i18n.registerUIStrings('core/sdk/NetworkManager.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); const i18nLazyString = i18n.i18n.getLazilyComputedLocalizedString.bind(undefined, str_); const requestToManagerMap = new WeakMap<NetworkRequest, NetworkManager>(); const CONNECTION_TYPES = new Map([ ['2g', Protocol.Network.ConnectionType.Cellular2g], ['3g', Protocol.Network.ConnectionType.Cellular3g], ['4g', Protocol.Network.ConnectionType.Cellular4g], ['bluetooth', Protocol.Network.ConnectionType.Bluetooth], ['wifi', Protocol.Network.ConnectionType.Wifi], ['wimax', Protocol.Network.ConnectionType.Wimax], ]); /** * We store two settings to disk to persist network throttling. * 1. The custom conditions that the user has defined. * 2. The active `key` that applies the correct current preset. * The reason the setting creation functions are defined here is because they are referred * to in multiple places, and this ensures we don't have accidental typos which * mean extra settings get mistakenly created. */ export function customUserNetworkConditionsSetting(): Common.Settings.Setting<Conditions[]> { return Common.Settings.Settings.instance().moduleSetting<Conditions[]>('custom-network-conditions'); } export function activeNetworkThrottlingKeySetting(): Common.Settings.Setting<ThrottlingConditionKey> { return Common.Settings.Settings.instance().createSetting( 'active-network-condition-key', PredefinedThrottlingConditionKey.NO_THROTTLING); } export class NetworkManager extends SDKModel<EventTypes> { readonly dispatcher: NetworkDispatcher; readonly fetchDispatcher: FetchDispatcher; readonly #networkAgent: ProtocolProxyApi.NetworkApi; readonly #bypassServiceWorkerSetting: Common.Settings.Setting<boolean>; readonly activeNetworkThrottlingKey: Common.Settings.Setting<ThrottlingConditionKey> = activeNetworkThrottlingKeySetting(); constructor(target: Target) { super(target); this.dispatcher = new NetworkDispatcher(this); this.fetchDispatcher = new FetchDispatcher(target.fetchAgent(), this); this.#networkAgent = target.networkAgent(); target.registerNetworkDispatcher(this.dispatcher); target.registerFetchDispatcher(this.fetchDispatcher); if (Common.Settings.Settings.instance().moduleSetting('cache-disabled').get()) { void this.#networkAgent.invoke_setCacheDisabled({cacheDisabled: true}); } if (Root.Runtime.hostConfig.devToolsPrivacyUI?.enabled && Root.Runtime.hostConfig.thirdPartyCookieControls?.managedBlockThirdPartyCookies !== true && (Common.Settings.Settings.instance().createSetting('cookie-control-override-enabled', undefined).get() || Common.Settings.Settings.instance().createSetting('grace-period-mitigation-disabled', undefined).get() || Common.Settings.Settings.instance().createSetting('heuristic-mitigation-disabled', undefined).get())) { this.cookieControlFlagsSettingChanged(); } void this.#networkAgent.invoke_enable({ maxPostDataSize: MAX_EAGER_POST_REQUEST_BODY_LENGTH, enableDurableMessages: Root.Runtime.hostConfig.devToolsEnableDurableMessages?.enabled, maxTotalBufferSize: MAX_RESPONSE_BODY_TOTAL_BUFFER_LENGTH, reportDirectSocketTraffic: true, }); void this.#networkAgent.invoke_setAttachDebugStack({enabled: true}); this.#bypassServiceWorkerSetting = Common.Settings.Settings.instance().createSetting('bypass-service-worker', false); if (this.#bypassServiceWorkerSetting.get()) { this.bypassServiceWorkerChanged(); } this.#bypassServiceWorkerSetting.addChangeListener(this.bypassServiceWorkerChanged, this); Common.Settings.Settings.instance() .moduleSetting('cache-disabled') .addChangeListener(this.cacheDisabledSettingChanged, this); Common.Settings.Settings.instance() .createSetting('cookie-control-override-enabled', undefined) .addChangeListener(this.cookieControlFlagsSettingChanged, this); Common.Settings.Settings.instance() .createSetting('grace-period-mitigation-disabled', undefined) .addChangeListener(this.cookieControlFlagsSettingChanged, this); Common.Settings.Settings.instance() .createSetting('heuristic-mitigation-disabled', undefined) .addChangeListener(this.cookieControlFlagsSettingChanged, this); } static forRequest(request: NetworkRequest): NetworkManager|null { return requestToManagerMap.get(request) || null; } static canReplayRequest(request: NetworkRequest): boolean { return Boolean(requestToManagerMap.get(request)) && Boolean(request.backendRequestId()) && !request.isRedirect() && request.resourceType() === Common.ResourceType.resourceTypes.XHR; } static replayRequest(request: NetworkRequest): void { const manager = requestToManagerMap.get(request); const requestId = request.backendRequestId(); if (!manager || !requestId || request.isRedirect()) { return; } void manager.#networkAgent.invoke_replayXHR({requestId}); } static async searchInRequest(request: NetworkRequest, query: string, caseSensitive: boolean, isRegex: boolean): Promise<TextUtils.ContentProvider.SearchMatch[]> { const manager = NetworkManager.forRequest(request); const requestId = request.backendRequestId(); if (!manager || !requestId || request.isRedirect()) { return []; } const response = await manager.#networkAgent.invoke_searchInResponseBody({requestId, query, caseSensitive, isRegex}); return TextUtils.TextUtils.performSearchInSearchMatches(response.result || [], query, caseSensitive, isRegex); } static async requestContentData(request: NetworkRequest): Promise<TextUtils.ContentData.ContentDataOrError> { if (request.resourceType() === Common.ResourceType.resourceTypes.WebSocket) { return {error: i18nString(UIStrings.noContentForWebSocket)}; } if (!request.finished) { await request.once(NetworkRequestEvents.FINISHED_LOADING); } if (request.isRedirect()) { return {error: i18nString(UIStrings.noContentForRedirect)}; } if (request.isPreflightRequest()) { return {error: i18nString(UIStrings.noContentForPreflight)}; } const manager = NetworkManager.forRequest(request); if (!manager) { return {error: 'No network manager for request'}; } const requestId = request.backendRequestId(); if (!requestId) { return {error: 'No backend request id for request'}; } const response = await manager.#networkAgent.invoke_getResponseBody({requestId}); const error = response.getError(); if (error) { return {error}; } return new TextUtils.ContentData.ContentData( response.body, response.base64Encoded, request.mimeType, request.charset() ?? undefined); } /** * Returns the already received bytes for an in-flight request. After calling this method * "dataReceived" events will contain additional data. */ static async streamResponseBody(request: NetworkRequest): Promise<TextUtils.ContentData.ContentDataOrError> { if (request.finished) { return {error: 'Streaming the response body is only available for in-flight requests.'}; } const manager = NetworkManager.forRequest(request); if (!manager) { return {error: 'No network manager for request'}; } const requestId = request.backendRequestId(); if (!requestId) { return {error: 'No backend request id for request'}; } const response = await manager.#networkAgent.invoke_streamResourceContent({requestId}); const error = response.getError(); if (error) { return {error}; } // Wait for at least the `responseReceived event so we have accurate mimetype and charset. await request.waitForResponseReceived(); return new TextUtils.ContentData.ContentData( response.bufferedData, /* isBase64=*/ true, request.mimeType, request.charset() ?? undefined); } static async requestPostData(request: NetworkRequest): Promise<string|null> { const manager = NetworkManager.forRequest(request); if (!manager) { console.error('No network manager for request'); return null; } const requestId = request.backendRequestId(); if (!requestId) { console.error('No backend request id for request'); return null; } try { const {postData} = await manager.#networkAgent.invoke_getRequestPostData({requestId}); return postData; } catch (e) { return e.message; } } static connectionType(conditions: Conditions): Protocol.Network.ConnectionType { if (!conditions.download && !conditions.upload) { return Protocol.Network.ConnectionType.None; } try { const title = typeof conditions.title === 'function' ? conditions.title().toLowerCase() : conditions.title.toLowerCase(); for (const [name, protocolType] of CONNECTION_TYPES) { if (title.includes(name)) { return protocolType; } } } catch { // If the i18nKey for this condition has changed, calling conditions.title() will break, so in that case we reset to NONE return Protocol.Network.ConnectionType.None; } return Protocol.Network.ConnectionType.Other; } static lowercaseHeaders(headers: Protocol.Network.Headers): Protocol.Network.Headers { const newHeaders: Protocol.Network.Headers = {}; for (const headerName in headers) { newHeaders[headerName.toLowerCase()] = headers[headerName]; } return newHeaders; } requestForURL(url: Platform.DevToolsPath.UrlString): NetworkRequest|null { return this.dispatcher.requestForURL(url); } requestForId(id: string): NetworkRequest|null { return this.dispatcher.requestForId(id); } requestForLoaderId(loaderId: Protocol.Network.LoaderId): NetworkRequest|null { return this.dispatcher.requestForLoaderId(loaderId); } private cacheDisabledSettingChanged({data: enabled}: Common.EventTarget.EventTargetEvent<boolean>): void { void this.#networkAgent.invoke_setCacheDisabled({cacheDisabled: enabled}); } private cookieControlFlagsSettingChanged(): void { const overridesEnabled = Boolean(Common.Settings.Settings.instance().createSetting('cookie-control-override-enabled', undefined).get()); const gracePeriodEnabled = overridesEnabled ? Boolean( Common.Settings.Settings.instance().createSetting('grace-period-mitigation-disabled', undefined).get()) : false; const heuristicEnabled = overridesEnabled ? Boolean(Common.Settings.Settings.instance().createSetting('heuristic-mitigation-disabled', undefined).get()) : false; void this.#networkAgent.invoke_setCookieControls({ enableThirdPartyCookieRestriction: overridesEnabled, disableThirdPartyCookieMetadata: gracePeriodEnabled, disableThirdPartyCookieHeuristics: heuristicEnabled, }); } override dispose(): void { Common.Settings.Settings.instance() .moduleSetting('cache-disabled') .removeChangeListener(this.cacheDisabledSettingChanged, this); } private bypassServiceWorkerChanged(): void { void this.#networkAgent.invoke_setBypassServiceWorker({bypass: this.#bypassServiceWorkerSetting.get()}); } async getSecurityIsolationStatus(frameId: Protocol.Page.FrameId|null): Promise<Protocol.Network.SecurityIsolationStatus|null> { const result = await this.#networkAgent.invoke_getSecurityIsolationStatus({frameId: frameId ?? undefined}); if (result.getError()) { return null; } return result.status; } async enableReportingApi(enable = true): Promise<Promise<Protocol.ProtocolResponseWithError>> { return await this.#networkAgent.invoke_enableReportingApi({enable}); } async loadNetworkResource( frameId: Protocol.Page.FrameId|null, url: Platform.DevToolsPath.UrlString, options: Protocol.Network.LoadNetworkResourceOptions): Promise<Protocol.Network.LoadNetworkResourcePageResult> { const result = await this.#networkAgent.invoke_loadNetworkResource({frameId: frameId ?? undefined, url, options}); if (result.getError()) { throw new Error(result.getError()); } return result.resource; } clearRequests(): void { this.dispatcher.clearRequests(); } } export enum Events { /* eslint-disable @typescript-eslint/naming-convention -- Used by web_tests. */ RequestStarted = 'RequestStarted', RequestUpdated = 'RequestUpdated', RequestFinished = 'RequestFinished', RequestUpdateDropped = 'RequestUpdateDropped', ResponseReceived = 'ResponseReceived', MessageGenerated = 'MessageGenerated', RequestRedirected = 'RequestRedirected', LoadingFinished = 'LoadingFinished', ReportingApiReportAdded = 'ReportingApiReportAdded', ReportingApiReportUpdated = 'ReportingApiReportUpdated', ReportingApiEndpointsChangedForOrigin = 'ReportingApiEndpointsChangedForOrigin', /* eslint-enable @typescript-eslint/naming-convention */ } export interface RequestStartedEvent { request: NetworkRequest; originalRequest: Protocol.Network.Request|null; } export interface ResponseReceivedEvent { request: NetworkRequest; response: Protocol.Network.Response; } export interface MessageGeneratedEvent { message: Common.UIString.LocalizedString; requestId: string; warning: boolean; } export interface EventTypes { [Events.RequestStarted]: RequestStartedEvent; [Events.RequestUpdated]: NetworkRequest; [Events.RequestFinished]: NetworkRequest; [Events.RequestUpdateDropped]: RequestUpdateDroppedEventData; [Events.ResponseReceived]: ResponseReceivedEvent; [Events.MessageGenerated]: MessageGeneratedEvent; [Events.RequestRedirected]: NetworkRequest; [Events.LoadingFinished]: NetworkRequest; [Events.ReportingApiReportAdded]: Protocol.Network.ReportingApiReport; [Events.ReportingApiReportUpdated]: Protocol.Network.ReportingApiReport; [Events.ReportingApiEndpointsChangedForOrigin]: Protocol.Network.ReportingApiEndpointsChangedForOriginEvent; } /** * Define some built-in DevTools throttling presets. * Note that for the download, upload and RTT values we multiply them by adjustment factors to make DevTools' emulation more accurate. * @see https://docs.google.com/document/d/10lfVdS1iDWCRKQXPfbxEn4Or99D64mvNlugP1AQuFlE/edit for historical context. * @see https://crbug.com/342406608#comment10 for context around the addition of 4G presets in June 2024. */ export const BlockingConditions: ThrottlingConditions = { key: PredefinedThrottlingConditionKey.BLOCKING, block: true, title: i18nLazyString(UIStrings.block), }; export const NoThrottlingConditions: Conditions = { key: PredefinedThrottlingConditionKey.NO_THROTTLING, title: i18nLazyString(UIStrings.noThrottling), i18nTitleKey: UIStrings.noThrottling, download: -1, upload: -1, latency: 0, }; export const OfflineConditions: Conditions = { key: PredefinedThrottlingConditionKey.OFFLINE, title: i18nLazyString(UIStrings.offline), i18nTitleKey: UIStrings.offline, download: 0, upload: 0, latency: 0, }; const slow3GTargetLatency = 400; export const Slow3GConditions: Conditions = { key: PredefinedThrottlingConditionKey.SPEED_3G, title: i18nLazyString(UIStrings.slowG), i18nTitleKey: UIStrings.slowG, // ~500Kbps down download: 500 * 1000 / 8 * .8, // ~500Kbps up upload: 500 * 1000 / 8 * .8, // 400ms RTT latency: slow3GTargetLatency * 5, targetLatency: slow3GTargetLatency, }; // Note for readers: this used to be called "Fast 3G" but it was renamed in May // 2024 to align with LH (crbug.com/342406608). const slow4GTargetLatency = 150; export const Slow4GConditions: Conditions = { key: PredefinedThrottlingConditionKey.SPEED_SLOW_4G, title: i18nLazyString(UIStrings.fastG), i18nTitleKey: UIStrings.fastG, // ~1.6 Mbps down download: 1.6 * 1000 * 1000 / 8 * .9, // ~0.75 Mbps up upload: 750 * 1000 / 8 * .9, // 150ms RTT latency: slow4GTargetLatency * 3.75, targetLatency: slow4GTargetLatency, }; const fast4GTargetLatency = 60; export const Fast4GConditions: Conditions = { key: PredefinedThrottlingConditionKey.SPEED_FAST_4G, title: i18nLazyString(UIStrings.fast4G), i18nTitleKey: UIStrings.fast4G, // 9 Mbps down download: 9 * 1000 * 1000 / 8 * .9, // 1.5 Mbps up upload: 1.5 * 1000 * 1000 / 8 * .9, // 60ms RTT latency: fast4GTargetLatency * 2.75, targetLatency: fast4GTargetLatency, }; const MAX_EAGER_POST_REQUEST_BODY_LENGTH = 64 * 1024; // bytes const MAX_RESPONSE_BODY_TOTAL_BUFFER_LENGTH = 250 * 1024 * 1024; // bytes export class FetchDispatcher implements ProtocolProxyApi.FetchDispatcher { readonly #fetchAgent: ProtocolProxyApi.FetchApi; readonly #manager: NetworkManager; constructor(agent: ProtocolProxyApi.FetchApi, manager: NetworkManager) { this.#fetchAgent = agent; this.#manager = manager; } requestPaused({requestId, request, resourceType, responseStatusCode, responseHeaders, networkId}: Protocol.Fetch.RequestPausedEvent): void { const networkRequest = networkId ? this.#manager.requestForId(networkId) : null; // If there was no 'Network.responseReceivedExtraInfo' event (e.g. for 'file:/' URLSs), // populate 'originalResponseHeaders' with the headers from the 'Fetch.requestPaused' event. if (networkRequest?.originalResponseHeaders.length === 0 && responseHeaders) { networkRequest.originalResponseHeaders = responseHeaders; } void MultitargetNetworkManager.instance().requestIntercepted(new InterceptedRequest( this.#fetchAgent, request, resourceType, requestId, networkRequest, responseStatusCode, responseHeaders)); } authRequired({}: Protocol.Fetch.AuthRequiredEvent): void { } } export class NetworkDispatcher implements ProtocolProxyApi.NetworkDispatcher { readonly #manager: NetworkManager; readonly #requestsById = new Map<string, NetworkRequest>(); readonly #requestsByURL = new Map<Platform.DevToolsPath.UrlString, NetworkRequest>(); readonly #requestsByLoaderId = new Map<Protocol.Network.LoaderId, NetworkRequest>(); readonly #requestIdToExtraInfoBuilder = new Map<string, ExtraInfoBuilder>(); /** * In case of an early abort or a cache hit, the Trust Token done event is * reported before the request itself is created in `requestWillBeSent`. * This causes the event to be lost as no `NetworkRequest` instance has been * created yet. * This map caches the events temporarily and populates the NetworkRequest * once it is created in `requestWillBeSent`. */ readonly #requestIdToTrustTokenEvent = new Map<string, Protocol.Network.TrustTokenOperationDoneEvent>(); constructor(manager: NetworkManager) { this.#manager = manager; MultitargetNetworkManager.instance().addEventListener( MultitargetNetworkManager.Events.REQUEST_INTERCEPTED, this.#markAsIntercepted.bind(this)); } #markAsIntercepted(event: Common.EventTarget.EventTargetEvent<string>): void { const request = this.requestForId(event.data); if (request) { request.setWasIntercepted(true); } } private headersMapToHeadersArray(headersMap: Protocol.Network.Headers): NameValue[] { const result = []; for (const name in headersMap) { const values = headersMap[name].split('\n'); for (let i = 0; i < values.length; ++i) { result.push({name, value: values[i]}); } } return result; } private updateNetworkRequestWithRequest(networkRequest: NetworkRequest, request: Protocol.Network.Request): void { networkRequest.requestMethod = request.method; networkRequest.setRequestHeaders(this.headersMapToHeadersArray(request.headers)); networkRequest.setRequestFormData(Boolean(request.hasPostData), request.postData || null); networkRequest.setInitialPriority(request.initialPriority); networkRequest.mixedContentType = request.mixedContentType || Protocol.Security.MixedContentType.None; networkRequest.setReferrerPolicy(request.referrerPolicy); networkRequest.setIsSameSite(request.isSameSite || false); networkRequest.setIsAdRelated(request.isAdRelated || false); } private updateNetworkRequestWithResponse(networkRequest: NetworkRequest, response: Protocol.Network.Response): void { if (response.url && networkRequest.url() !== response.url) { networkRequest.setUrl(response.url as Platform.DevToolsPath.UrlString); } networkRequest.mimeType = response.mimeType; networkRequest.setCharset(response.charset); if (!networkRequest.statusCode || networkRequest.wasIntercepted()) { networkRequest.statusCode = response.status; } if (!networkRequest.statusText || networkRequest.wasIntercepted()) { networkRequest.statusText = response.statusText; } if (!networkRequest.hasExtraResponseInfo() || networkRequest.wasIntercepted()) { networkRequest.responseHeaders = this.headersMapToHeadersArray(response.headers); } if (response.encodedDataLength >= 0) { networkRequest.setTransferSize(response.encodedDataLength); } if (response.requestHeaders && !networkRequest.hasExtraRequestInfo()) { // TODO(http://crbug.com/1004979): Stop using response.requestHeaders and // response.requestHeadersText once shared workers // emit Network.*ExtraInfo events for their network #requests. networkRequest.setRequestHeaders(this.headersMapToHeadersArray(response.requestHeaders)); networkRequest.setRequestHeadersText(response.requestHeadersText || ''); } networkRequest.connectionReused = response.connectionReused; networkRequest.connectionId = String(response.connectionId); if (response.remoteIPAddress) { networkRequest.setRemoteAddress(response.remoteIPAddress, response.remotePort || -1); } if (response.fromServiceWorker) { networkRequest.fetchedViaServiceWorker = true; } if (response.fromDiskCache) { networkRequest.setFromDiskCache(); } if (response.fromPrefetchCache) { networkRequest.setFromPrefetchCache(); } if (response.fromEarlyHints) { networkRequest.setFromEarlyHints(); } if (response.cacheStorageCacheName) { networkRequest.setResponseCacheStorageCacheName(response.cacheStorageCacheName); } if (response.serviceWorkerRouterInfo) { networkRequest.serviceWorkerRouterInfo = response.serviceWorkerRouterInfo; } if (response.responseTime) { networkRequest.setResponseRetrievalTime(new Date(response.responseTime)); } networkRequest.timing = response.timing; networkRequest.protocol = response.protocol || ''; networkRequest.alternateProtocolUsage = response.alternateProtocolUsage; if (response.serviceWorkerResponseSource) { networkRequest.setServiceWorkerResponseSource(response.serviceWorkerResponseSource); } networkRequest.setSecurityState(response.securityState); if (response.securityDetails) { networkRequest.setSecurityDetails(response.securityDetails); } const newResourceType = Common.ResourceType.ResourceType.fromMimeTypeOverride(networkRequest.mimeType); if (newResourceType) { networkRequest.setResourceType(newResourceType); } if (networkRequest.responseReceivedPromiseResolve) { // Anyone interested in waiting for response headers being available? networkRequest.responseReceivedPromiseResolve(); } else { // If not, make sure no one will wait on it in the future. networkRequest.responseReceivedPromise = Promise.resolve(); } } requestForId(id: string): NetworkRequest|null { return this.#requestsById.get(id) || null; } requestForURL(url: Platform.DevToolsPath.UrlString): NetworkRequest|null { return this.#requestsByURL.get(url) || null; } requestForLoaderId(loaderId: Protocol.Network.LoaderId): NetworkRequest|null { return this.#requestsByLoaderId.get(loaderId) || null; } resourceChangedPriority({requestId, newPriority}: Protocol.Network.ResourceChangedPriorityEvent): void { const networkRequest = this.#requestsById.get(requestId); if (networkRequest) { networkRequest.setPriority(newPriority); } } signedExchangeReceived({requestId, info}: Protocol.Network.SignedExchangeReceivedEvent): void { // While loading a signed exchange, a signedExchangeReceived event is sent // between two requestWillBeSent events. // 1. The first requestWillBeSent is sent while starting the navigation (or // prefetching). // 2. This signedExchangeReceived event is sent when the browser detects the // signed exchange. // 3. The second requestWillBeSent is sent with the generated redirect // response and a new redirected request which URL is the inner request // URL of the signed exchange. let networkRequest = this.#requestsById.get(requestId); // |requestId| is available only for navigation #requests. If the request was // sent from a renderer process for prefetching, it is not available. In the // case, need to fallback to look for the URL. // TODO(crbug/841076): Sends the request ID of prefetching to the browser // process and DevTools to find the matching request. if (!networkRequest) { networkRequest = this.#requestsByURL.get(info.outerResponse.url as Platform.DevToolsPath.UrlString); if (!networkRequest) { return; } // Or clause is never hit, but is here because we can't use non-null assertions. const backendRequestId = networkRequest.backendRequestId() || requestId; requestId = backendRequestId; } networkRequest.setSignedExchangeInfo(info); networkRequest.setResourceType(Common.ResourceType.resourceTypes.SignedExchange); this.updateNetworkRequestWithResponse(networkRequest, info.outerResponse); this.updateNetworkRequest(networkRequest); this.getExtraInfoBuilder(requestId).addHasExtraInfo(info.hasExtraInfo); this.#manager.dispatchEventToListeners( Events.ResponseReceived, {request: networkRequest, response: info.outerResponse}); } requestWillBeSent({ requestId, loaderId, documentURL, request, timestamp, wallTime, initiator, redirectHasExtraInfo, redirectResponse, type, frameId, hasUserGesture, }: Protocol.Network.RequestWillBeSentEvent): void { let networkRequest = this.#requestsById.get(requestId); if (networkRequest) { // FIXME: move this check to the backend. if (!redirectResponse) { return; } // If signedExchangeReceived event has already been sent for the request, // ignores the internally generated |redirectResponse|. The // |outerResponse| of SignedExchangeInfo was set to |networkRequest| in // signedExchangeReceived(). if (!networkRequest.signedExchangeInfo()) { this.responseReceived({ requestId, loaderId, timestamp, type: type || Protocol.Network.ResourceType.Other, response: redirectResponse, hasExtraInfo: redirectHasExtraInfo, frameId, }); } networkRequest = this.appendRedirect(requestId, timestamp, request.url as Platform.DevToolsPath.UrlString); this.#manager.dispatchEventToListeners(Events.RequestRedirected, networkRequest); } else { networkRequest = NetworkRequest.create( requestId, request.url as Platform.DevToolsPath.UrlString, documentURL as Platform.DevToolsPath.UrlString, frameId ?? null, loaderId, initiator, hasUserGesture); requestToManagerMap.set(networkRequest, this.#manager); } networkRequest.hasNetworkData = true; this.updateNetworkRequestWithRequest(networkRequest, request); networkRequest.setIssueTime(timestamp, wallTime); networkRequest.setResourceType( type ? Common.ResourceType.resourceTypes[type] : Common.ResourceType.resourceTypes.Other); if (request.trustTokenParams) { networkRequest.setTrustTokenParams(request.trustTokenParams); } const maybeTrustTokenEvent = this.#requestIdToTrustTokenEvent.get(requestId); if (maybeTrustTokenEvent) { networkRequest.setTrustTokenOperationDoneEvent(maybeTrustTokenEvent); this.#requestIdToTrustTokenEvent.delete(requestId); } this.getExtraInfoBuilder(requestId).addRequest(networkRequest); this.startNetworkRequest(networkRequest, request); } requestServedFromCache({requestId}: Protocol.Network.RequestServedFromCacheEvent): void { const networkRequest = this.#requestsById.get(requestId); if (!networkRequest) { return; } networkRequest.setFromMemoryCache(); } responseReceived({requestId, loaderId, timestamp, type, response, hasExtraInfo, frameId}: Protocol.Network.ResponseReceivedEvent): void { const networkRequest = this.#requestsById.get(requestId); const lowercaseHeaders = NetworkManager.lowercaseHeaders(response.headers); if (!networkRequest) { const lastModifiedHeader = lowercaseHeaders['last-modified']; // We missed the requestWillBeSent. const eventData: RequestUpdateDroppedEventData = { url: response.url as Platform.DevToolsPath.UrlString, frameId: frameId ?? null, loaderId, resourceType: type, mimeType: response.mimeType, lastModified: lastModifiedHeader ? new Date(lastModifiedHeader) : null, }; this.#manager.dispatchEventToListeners(Events.RequestUpdateDropped, eventData); return; } networkRequest.responseReceivedTime = timestamp; networkRequest.setResourceType(Common.ResourceType.resourceTypes[type]); this.updateNetworkRequestWithResponse(networkRequest, response); this.updateNetworkRequest(networkRequest); this.getExtraInfoBuilder(requestId).addHasExtraInfo(hasExtraInfo); this.#manager.dispatchEventToListeners(Events.ResponseReceived, {request: networkRequest, response}); } dataReceived(event: Protocol.Network.DataReceivedEvent): void { let networkRequest: NetworkRequest|null|undefined = this.#requestsById.get(event.requestId); if (!networkRequest) { networkRequest = this.maybeAdoptMainResourceRequest(event.requestId); } if (!networkRequest) { return; } networkRequest.addDataReceivedEvent(event); this.updateNetworkRequest(networkRequest); } loadingFinished({requestId, timestamp: finishTime, encodedDataLength}: Protocol.Network.LoadingFinishedEvent): void { let networkRequest: NetworkRequest|null|undefined = this.#requestsById.get(requestId); if (!networkRequest) { networkRequest = this.maybeAdoptMainResourceRequest(requestId); } if (!networkRequest) { return; } this.getExtraInfoBuilder(requestId).finished(); this.finishNetworkRequest(networkRequest, finishTime, encodedDataLength); this.#manager.dispatchEventToListeners(Events.LoadingFinished, networkRequest); } loadingFailed({ requestId, timestamp: time, type: resourceType, errorText: localizedDescription, canceled, blockedReason, corsErrorStatus, }: Protocol.Network.LoadingFailedEvent): void { const networkRequest = this.#requestsById.get(requestId); if (!networkRequest) { return; } networkRequest.failed = true; networkRequest.setResourceType(Common.ResourceType.resourceTypes[resourceType]); networkRequest.canceled = Boolean(canceled); if (blockedReason) { networkRequest.setBlockedReason(blockedReason); if (blockedReason === Protocol.Network.BlockedReason.Inspector) { const message = i18nString(UIStrings.requestWasBlockedByDevtoolsS, {PH1: networkRequest.url()}); this.#manager.dispatchEventToListeners(Events.MessageGenerated, {message, requestId, warning: true}); } } if (corsErrorStatus) { networkRequest.setCorsErrorStatus(corsErrorStatus); } networkRequest.localizedFailDescription = localizedDescription; this.getExtraInfoBuilder(requestId).finished(); this.finishNetworkRequest(networkRequest, time, -1); } webSocketCreated({requestId, url: requestURL, initiator}: Protocol.Network.WebSocketCreatedEvent): void { const networkRequest = NetworkRequest.createForSocket(requestId, requestURL as Platform.DevToolsPath.UrlString, initiator); requestToManagerMap.set(networkRequest, this.#manager); networkRequest.setResourceType(Common.ResourceType.resourceTypes.WebSocket); this.startNetworkRequest(networkRequest, null); } webSocketWillSendHandshakeRequest({requestId, timestamp: time, wallTime, request}: Protocol.Network.WebSocketWillSendHandshakeRequestEvent): void { const networkRequest = this.#requestsById.get(requestId); if (!networkRequest) { return; } networkRequest.requestMethod = 'GET'; networkRequest.setRequestHeaders(this.headersMapToHeadersArray(request.headers)); networkRequest.setIssueTime(time, wallTime); this.updateNetworkRequest(networkRequest); } webSocketHandshakeResponseReceived({requestId, timestamp: time, response}: Protocol.Network.WebSocketHandshakeResponseReceivedEvent): void { const networkRequest = this.#requestsById.get(requestId); if (!networkRequest) { return; } networkRequest.statusCode = response.status; networkRequest.statusText = response.statusText; networkRequest.responseHeaders = this.headersMapToHeadersArray(response.headers); networkRequest.responseHeadersText = response.headersText || ''; if (response.requestHeaders) { networkRequest.setRequestHeaders(this.headersMapToHeadersArray(response.requestHeaders)); } if (response.requestHeadersText) { networkRequest.setRequestHeadersText(response.requestHeadersText); } networkRequest.responseReceivedTime = time; networkRequest.protocol = 'websocket'; this.updateNetworkRequest(networkRequest); } webSocketFrameReceived({requestId, timestamp: time, response}: Protocol.Network.WebSocketFrameReceivedEvent): void { const networkRequest = this.#requestsById.get(requestId); if (!networkRequest) { return; } networkRequest.addProtocolFrame(response, time, false); networkRequest.responseReceivedTime = time; this.updateNetworkRequest(networkRequest); } webSocketFrameSent({requestId, timestamp: time, response}: Protocol.Network.WebSocketFrameSentEvent): void { const networkRequest = this.#requestsById.get(requestId); if (!networkRequest) { return; } networkRequest.addProtocolFrame(response, time, true); networkRequest.responseReceivedTime = time; this.updateNetworkRequest(networkRequest); } webSocketFrameError({requestId, timestamp: time, errorMessage}: Protocol.Network.WebSocketFrameErrorEvent): void { const networkRequest = this.#requestsById.get(requestId); if (!networkRequest) { return; } networkRequest.addProtocolFrameError(errorMessage, time); networkRequest.responseReceivedTime = time; this.updateNetworkRequest(networkRequest); } webSocketClosed({requestId, timestamp: time}: Protocol.Network.WebSocketClosedEvent): void { const networkRequest = this.#requestsById.get(requestId); if (!networkRequest) { return; } this.finishNetworkRequest(networkRequest, time, -1); } eventSourceMessageReceived({requestId, timestamp: time, eventName, eventId, data}: Protocol.Network.EventSourceMessageReceivedEvent): void { const networkRequest = this.#requestsById.get(requestId); if (!networkRequest) { return; } networkRequest.addEventSourceMessage(time, eventName, eventId, data); } requestIntercepted({}: Protocol.Network.RequestInterceptedEvent): void { } requestWillBeSentExtraInfo({ requestId, associatedCookies, headers, clientSecurityState, connectTiming, siteHasCookieInOtherPartition, appliedNetworkConditionsId }: Protocol.Network.RequestWillBeSentExtraInfoEvent): void { const blockedRequestCookies: BlockedCookieWithReason[] = []; const includedRequestCookies: IncludedCookieWithReason[] = []; for (const {blockedReasons, exemptionReason, cookie} of associatedCookies) { if (blockedReasons.length === 0) { includedRequestCookies.push({exemptionReason, cookie: Cookie.fromProtocolCookie(cookie)}); } else { blockedRequestCookies.push({blockedReasons, cookie: Cookie.fromProtocolCookie(cookie)}); } } const extraRequestInfo = { blockedRequestCookies, includedRequestCookies, requestHeaders: this.headersMapToHeadersArray(headers), clientSecurityState, connectTiming, siteHasCookieInOtherPartition, appliedNetworkConditionsId, }; this.getExtraInfoBuilder(requestId).addRequestExtraInfo(extraRequestInfo); const networkRequest = this.#requestsById.get(requestId); if (appliedNetworkConditionsId && networkRequest) { networkRequest.setAppliedNetworkConditions(appliedNetworkConditionsId); this.updateNetworkRequest(networkRequest); } } responseReceivedEarlyHints({ requestId, headers, }: Protocol.Network.ResponseReceivedEarlyHintsEvent): void { this.getExtraInfoBuilder(requestId).setEarlyHintsHeaders(this.headersMapToHeadersArray(headers)); } responseReceivedExtraInfo({ requestId, blockedCookies, headers, headersText, resourceIPAddressSpace, statusCode, cookiePartitionKey, cookiePartitionKeyOpaque, exemptedCookies, }: Protocol.Network.ResponseReceivedExtraInfoEvent): void { const extraResponseInfo: ExtraResponseInfo = { blockedResponseCookies: blockedCookies.map(blockedCookie => ({ blockedReasons: blockedCookie.blockedReasons, cookieLine: blockedCookie.cookieLine, cookie: blockedCookie.cookie ? Cookie.fromProtocolCookie(blockedCookie.cookie) : null, })), responseHeaders: this.headersMapToHeadersArray(headers), responseHeadersText: headersText, resourceIPAddressSpace, statusCode, cookiePartitionKey, cookiePartitionKeyOpaque, exemptedResponseCookies: exemptedCookies?.map(exemptedCookie => ({ cookie: Cookie.fromProtocolCookie(exemptedCookie.cookie), cookieLine: exemptedCookie.cookieLine, exemptionReason: exemptedCookie.exemptionReason, })), }; this.getExtraInfoBuilder(requestId).addResponseExtraInfo(extraResponseInfo); } private getExtraInfoBuilder(requestId: string): ExtraInfoBuilder { let builder: ExtraInfoBuilder; if (!this.#requestIdToExtraInfoBuilder.has(requestId)) { builder = new ExtraInfoBuilder(); this.#requestIdToExtraInfoBuilder.set(requestId, builder); } else { builder = (this.#requestIdToExtraInfoBuilder.get(requestId) as ExtraInfoBuilder); } return builder; } private appendRedirect( requestId: Protocol.Network.RequestId, time: number, redirectURL: Platform.DevToolsPath.UrlString): NetworkRequest { const originalNetworkRequest = this.#requestsById.get(requestId); if (!originalNetworkRequest) { throw new Error(`Could not find original network request for ${requestId}`); } let redirectCount = 0; for (let redirect = originalNetworkRequest.redirectSource(); redirect; redirect = redirect.redirectSource()) { redirectCount++; } originalNetworkRequest.markAsRedirect(redirectCount); this.finishNetworkRequest(originalNetworkRequest, time, -1); const newNetworkRequest = NetworkRequest.create( requestId, redirectURL, originalNetworkRequest.documentURL, originalNetworkRequest.frameId, originalNetworkRequest.loaderId, originalNetworkRequest.initiator(), originalNetworkRequest.hasUserGesture() ?? undefined); requestToManagerMap.set(newNetworkRequest, this.#manager); newNetworkRequest.setRedirectSource(originalNetworkRequest); originalNetworkRequest.setRedirectDestination(newNetworkRequest); return newNetworkRequest; } private maybeAdoptMainResourceRequest(requestId: string): NetworkRequest|null { const request = MultitargetNetworkManager.instance().inflightMainResourceRequests.get(requestId); if (!request) { return null; } const oldDispatcher = (NetworkManager.forRequest(request) as NetworkManager).dispatcher; oldDispatcher.#requestsById.delete(requestId); oldDispatcher.#requestsByURL.delete(request.url()); const loaderId = request.loaderId; if (loaderId) { oldDispatcher.#requestsByLoaderId.delete(loaderId); } const builder = oldDispatcher.#requestIdToExtraInfoBuilder.get(requestId); oldDispatcher.#requestIdToExtraInfoBuilder.delete(requestId); this.#requestsById.set(requestId, request); this.#requestsByURL.set(request.url(), request); if (loaderId) { this.#requestsByLoaderId.set(loaderId, request); } if (builder) { this.#requestIdToExtraInfoBuilder.set(requestId, builder); } requestToManagerMap.set(request, this.#manager); return request; } private startNetworkRequest(networkRequest: NetworkRequest, originalRequest: Protocol.Network.Request|null): void { this.#requestsById.set(networkRequest.requestId(), networkRequest); this.#requestsByURL.set(networkRequest.url(), networkRequest); const loaderId = networkRequest.loaderId; if (loaderId) { this.#requestsByLoaderId.set(loaderId, networkRequest); } // The following relies on the fact that loaderIds and requestIds // are globally unique and that the main request has them equal. If // loaderId is an empty string, it indicates a worker request. For the // request to fetch the main worker script, the request ID is the future // worker target ID and, therefore, it is unique. if (networkRequest.loaderId === networkRequest.requestId() || networkRequest.loaderId === '') { MultitargetNetworkManager.instance().inflightMainResourceRequests.set(networkRequest.requestId(), networkRequest); } this.#manager.dispatchEventToListeners(Events.RequestStarted, {request: networkRequest, originalRequest}); } private updateNetworkRequest(networkRequest: NetworkRequest): void { this.#manager.dispatchEventToListeners(Events.RequestUpdated, networkRequest); } private finishNetworkRequest( networkRequest: NetworkRequest, finishTime: number, encodedDataLength: number, ): void { networkRequest.endTime = finishTime; networkRequest.finished = true; if (encodedDataLength >= 0) { const redirectSource = networkRequest.redirectSource(); if (redirectSource?.signedExchangeInfo()) { networkRequest.setTransferSize(0); redirectSource.setTransferSize(encodedDataLength); this.updateNetworkRequest(redirectSource); } else { networkRequest.setTransferSize(encodedDataLength); } } this.#manager.dispatchEventToListeners(Events.RequestFinished, networkRequest); MultitargetNetworkManager.instance().inflightMainResourceRequests.delete(networkRequest.requestId()); if (Common.Settings.Settings.instance().moduleSetting('monitoring-xhr-enabled').get() && networkRequest.resourceType().category() === Common.ResourceType.resourceCategories.XHR) { let message; const failedToLoad = networkRequest.failed || networkRequest.hasErrorStatusCode(); if (failedToLoad) { message = i18nString( UIStrings.sFailedLoadingSS, {PH1: networkRequest.resourceType().title(), PH2: networkRequest.requestMethod, PH3: networkRequest.url()}); } else { message = i18nString( UIStrings.sFinishedLoadingSS, {PH1: networkRequest.resourceType().title(), PH2: networkRequest.requestMethod, PH3: networkRequest.url()}); } this.#manager.dispatchEventToListeners( Events.MessageGenerated, {message, requestId: networkRequest.requestId(), warning: false}); } } clearRequests(): void { for (const [requestId, request] of this.#requestsById) { if (request.finished) { this.#requestsById.delete(requestId); } } for (const [requestURL, request] of this.#requestsByURL) { if (request.finished) { this.#requestsByURL.delete(requestURL); } } for (const [requestLoaderId, request] of this.#requestsByLoaderId) { if (request.finished) { this.#requestsByLoaderId.delete(requestLoaderId); } } for (const [requestId, builder] of this.#requestIdToExtraInfoBuilder) { if (builder.isFinished()) { this.#requestIdToExtraInfoBuilder.delete(requestId); } } } webTransportCreated({transportId, url: requestURL, timestamp: time, initiator}: Protocol.Network.WebTransportCreatedEvent): void { const networkRequest = NetworkRequest.createForSocket(transportId, requestURL as Platform.DevToolsPath.UrlString, initiator); networkRequest.hasNetworkData = true; requestToManagerMap.set(networkRequest, this.#manager); networkRequest.setResourceType(Common.ResourceType.resourceTypes.WebTransport); networkRequest.setIssueTime(time, 0); // TODO(yoichio): Add appropreate events to address abort cases. this.startNetworkRequest(networkRequest, null); } webTransportConnectionEstablished({transportId, timestamp: time}: Protocol.Network.WebTransportConnectionEstablishedEvent): void {