UNPKG

chrome-devtools-frontend

Version:
1,250 lines (1,104 loc) • 74.4 kB
// Copyright 2021 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* * Copyright (C) 2011 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the #name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import type * as TextUtils from '../../models/text_utils/text_utils.js'; import * as Common from '../common/common.js'; import * as Host from '../host/host.js'; import * as i18n from '../i18n/i18n.js'; import * as Platform from '../platform/platform.js'; import type * as ProtocolProxyApi from '../../generated/protocol-proxy-api.js'; import * as Protocol from '../../generated/protocol.js'; import {Cookie} from './Cookie.js'; import { Events as NetworkRequestEvents, NetworkRequest, type BlockedCookieWithReason, type ContentData, type ExtraRequestInfo, type ExtraResponseInfo, type MIME_TYPE, type NameValue, type WebBundleInfo, type WebBundleInnerRequestInfo, } from './NetworkRequest.js'; import {Capability, type Target} from './Target.js'; import {SDKModel} from './SDKModel.js'; import {TargetManager, type SDKModelObserver} from './TargetManager.js'; import {type Serializer} from '../common/Settings.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 */ slowG: 'Slow 3G', /** *@description Text in Network Manager */ fastG: 'Fast 3G', /** *@description Text in Network Manager *@example {https://example.com} PH1 */ requestWasBlockedByDevtoolsS: 'Request was blocked by DevTools: "{PH1}"', /** *@description Text in Network Manager *@example {https://example.com} PH1 *@example {application} PH2 */ crossoriginReadBlockingCorb: 'Cross-Origin Read Blocking (CORB) blocked cross-origin response {PH1} with MIME type {PH2}. See https://www.chromestatus.com/feature/5629709824032768 for more details.', /** *@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}".', }; 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], ]); export class NetworkManager extends SDKModel<EventTypes> { readonly dispatcher: NetworkDispatcher; readonly fetchDispatcher: FetchDispatcher; readonly #networkAgent: ProtocolProxyApi.NetworkApi; readonly #bypassServiceWorkerSetting: Common.Settings.Setting<boolean>; 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('cacheDisabled').get()) { void this.#networkAgent.invoke_setCacheDisabled({cacheDisabled: true}); } void this.#networkAgent.invoke_enable({maxPostDataSize: MAX_EAGER_POST_REQUEST_BODY_LENGTH}); void this.#networkAgent.invoke_setAttachDebugStack({enabled: true}); this.#bypassServiceWorkerSetting = Common.Settings.Settings.instance().createSetting('bypassServiceWorker', false); if (this.#bypassServiceWorkerSetting.get()) { this.bypassServiceWorkerChanged(); } this.#bypassServiceWorkerSetting.addChangeListener(this.bypassServiceWorkerChanged, this); Common.Settings.Settings.instance() .moduleSetting('cacheDisabled') .addChangeListener(this.cacheDisabledSettingChanged, 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: query, caseSensitive: caseSensitive, isRegex: isRegex}); return response.result || []; } static async requestContentData(request: NetworkRequest): Promise<ContentData> { if (request.resourceType() === Common.ResourceType.resourceTypes.WebSocket) { return {error: i18nString(UIStrings.noContentForWebSocket), content: null, encoded: false}; } if (!request.finished) { await request.once(NetworkRequestEvents.FinishedLoading); } if (request.isRedirect()) { return {error: i18nString(UIStrings.noContentForRedirect), content: null, encoded: false}; } if (request.isPreflightRequest()) { return {error: i18nString(UIStrings.noContentForPreflight), content: null, encoded: false}; } const manager = NetworkManager.forRequest(request); if (!manager) { return {error: 'No network manager for request', content: null, encoded: false}; } const requestId = request.backendRequestId(); if (!requestId) { return {error: 'No backend request id for request', content: null, encoded: false}; } const response = await manager.#networkAgent.invoke_getResponseBody({requestId}); const error = response.getError() || null; return {error: error, content: error ? null : response.body, encoded: response.base64Encoded}; } 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; } 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; } } 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}); } override dispose(): void { Common.Settings.Settings.instance() .moduleSetting('cacheDisabled') .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: boolean = true): Promise<Promise<Protocol.ProtocolResponseWithError>> { return 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(); } } // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum Events { RequestStarted = 'RequestStarted', RequestUpdated = 'RequestUpdated', RequestFinished = 'RequestFinished', RequestUpdateDropped = 'RequestUpdateDropped', ResponseReceived = 'ResponseReceived', MessageGenerated = 'MessageGenerated', RequestRedirected = 'RequestRedirected', LoadingFinished = 'LoadingFinished', ReportingApiReportAdded = 'ReportingApiReportAdded', ReportingApiReportUpdated = 'ReportingApiReportUpdated', ReportingApiEndpointsChangedForOrigin = 'ReportingApiEndpointsChangedForOrigin', } 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 type 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, }; export const NoThrottlingConditions: Conditions = { title: i18nLazyString(UIStrings.noThrottling), i18nTitleKey: UIStrings.noThrottling, download: -1, upload: -1, latency: 0, }; export const OfflineConditions: Conditions = { title: i18nLazyString(UIStrings.offline), i18nTitleKey: UIStrings.offline, download: 0, upload: 0, latency: 0, }; export const Slow3GConditions: Conditions = { title: i18nLazyString(UIStrings.slowG), i18nTitleKey: UIStrings.slowG, download: 500 * 1000 / 8 * .8, upload: 500 * 1000 / 8 * .8, latency: 400 * 5, }; export const Fast3GConditions: Conditions = { title: i18nLazyString(UIStrings.fastG), i18nTitleKey: UIStrings.fastG, download: 1.6 * 1000 * 1000 / 8 * .9, upload: 750 * 1000 / 8 * .9, latency: 150 * 3.75, }; const MAX_EAGER_POST_REQUEST_BODY_LENGTH = 64 * 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; #requestsById: Map<string, NetworkRequest>; #requestsByURL: Map<Platform.DevToolsPath.UrlString, NetworkRequest>; #requestsByLoaderId: Map<Protocol.Network.LoaderId, NetworkRequest>; #requestIdToExtraInfoBuilder: Map<string, ExtraInfoBuilder>; readonly #requestIdToTrustTokenEvent: Map<string, Protocol.Network.TrustTokenOperationDoneEvent>; constructor(manager: NetworkManager) { this.#manager = manager; this.#requestsById = new Map(); this.#requestsByURL = new Map(); this.#requestsByLoaderId = new Map(); this.#requestIdToExtraInfoBuilder = new Map(); /** * 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 temporarliy and populates the NetworKRequest * once it is created in `requestWillBeSent`. */ this.#requestIdToTrustTokenEvent = new Map(); MultitargetNetworkManager.instance().addEventListener( MultitargetNetworkManager.Events.RequestIntercepted, 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: 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); } 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 as MIME_TYPE); 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.cacheStorageCacheName) { networkRequest.setResponseCacheStorageCacheName(response.cacheStorageCacheName); } 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); } } 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; } } networkRequest.setSignedExchangeInfo(info); networkRequest.setResourceType(Common.ResourceType.resourceTypes.SignedExchange); this.updateNetworkRequestWithResponse(networkRequest, info.outerResponse); this.updateNetworkRequest(networkRequest); this.#manager.dispatchEventToListeners( Events.ResponseReceived, {request: networkRequest, response: info.outerResponse}); } requestWillBeSent({ requestId, loaderId, documentURL, request, timestamp, wallTime, initiator, 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: false, 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, 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: 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.#manager.dispatchEventToListeners(Events.ResponseReceived, {request: networkRequest, response}); } dataReceived({requestId, timestamp, dataLength, encodedDataLength}: Protocol.Network.DataReceivedEvent): void { let networkRequest: NetworkRequest|null|undefined = this.#requestsById.get(requestId); if (!networkRequest) { networkRequest = this.maybeAdoptMainResourceRequest(requestId); } if (!networkRequest) { return; } networkRequest.resourceSize += dataLength; if (encodedDataLength !== -1) { networkRequest.increaseTransferSize(encodedDataLength); } networkRequest.endTime = timestamp; this.updateNetworkRequest(networkRequest); } loadingFinished({requestId, timestamp: finishTime, encodedDataLength, shouldReportCorbBlocking}: 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, shouldReportCorbBlocking); 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: message, requestId: 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.createForWebSocket(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}: Protocol.Network.RequestWillBeSentExtraInfoEvent): void { const blockedRequestCookies: BlockedCookieWithReason[] = []; const includedRequestCookies = []; for (const {blockedReasons, cookie} of associatedCookies) { if (blockedReasons.length === 0) { includedRequestCookies.push(Cookie.fromProtocolCookie(cookie)); } else { blockedRequestCookies.push({blockedReasons, cookie: Cookie.fromProtocolCookie(cookie)}); } } const extraRequestInfo = { blockedRequestCookies, includedRequestCookies, requestHeaders: this.headersMapToHeadersArray(headers), clientSecurityState, connectTiming, siteHasCookieInOtherPartition, }; this.getExtraInfoBuilder(requestId).addRequestExtraInfo(extraRequestInfo); } responseReceivedExtraInfo({ requestId, blockedCookies, headers, headersText, resourceIPAddressSpace, statusCode, cookiePartitionKey, cookiePartitionKeyOpaque, }: Protocol.Network.ResponseReceivedExtraInfoEvent): void { const extraResponseInfo: ExtraResponseInfo = { blockedResponseCookies: blockedCookies.map(blockedCookie => { return { blockedReasons: blockedCookie.blockedReasons, cookieLine: blockedCookie.cookieLine, cookie: blockedCookie.cookie ? Cookie.fromProtocolCookie(blockedCookie.cookie) : null, }; }), responseHeaders: this.headersMapToHeadersArray(headers), responseHeadersText: headersText, resourceIPAddressSpace, statusCode, cookiePartitionKey, cookiePartitionKeyOpaque, }; 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 (networkRequest.loaderId === networkRequest.requestId()) { 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, shouldReportCorbBlocking?: boolean): void { networkRequest.endTime = finishTime; networkRequest.finished = true; if (encodedDataLength >= 0) { const redirectSource = networkRequest.redirectSource(); if (redirectSource && 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 (shouldReportCorbBlocking) { const message = i18nString(UIStrings.crossoriginReadBlockingCorb, {PH1: networkRequest.url(), PH2: networkRequest.mimeType}); this.#manager.dispatchEventToListeners( Events.MessageGenerated, {message: message, requestId: networkRequest.requestId(), warning: true}); } if (Common.Settings.Settings.instance().moduleSetting('monitoringXHREnabled').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: 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.createForWebSocket(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 { const networkRequest = this.#requestsById.get(transportId); if (!networkRequest) { return; } // This dummy deltas are needed to show this request as being // downloaded(blue) given typical WebTransport is kept for a while. // TODO(yoichio): Add appropreate events to fix these dummy datas. // DNS lookup? networkRequest.responseReceivedTime = time; networkRequest.endTime = time + 0.001; this.updateNetworkRequest(networkRequest); } webTransportClosed({transportId, timestamp: time}: Protocol.Network.WebTransportClosedEvent): void { const networkRequest = this.#requestsById.get(transportId); if (!networkRequest) { return; } networkRequest.endTime = time; this.finishNetworkRequest(networkRequest, time, 0); } trustTokenOperationDone(event: Protocol.Network.TrustTokenOperationDoneEvent): void { const request = this.#requestsById.get(event.requestId); if (!request) { this.#requestIdToTrustTokenEvent.set(event.requestId, event); return; } request.setTrustTokenOperationDoneEvent(event); } subresourceWebBundleMetadataReceived({requestId, urls}: Protocol.Network.SubresourceWebBundleMetadataReceivedEvent): void { const extraInfoBuilder = this.getExtraInfoBuilder(requestId); extraInfoBuilder.setWebBundleInfo({resourceUrls: urls as Platform.DevToolsPath.UrlString[]}); const finalRequest = extraInfoBuilder.finalRequest(); if (finalRequest) { this.updateNetworkRequest(finalRequest); } } subresourceWebBundleMetadataError({requestId, errorMessage}: Protocol.Network.SubresourceWebBundleMetadataErrorEvent): void { const extraInfoBuilder = this.getExtraInfoBuilder(requestId); extraInfoBuilder.setWebBundleInfo({errorMessage}); const finalRequest = extraInfoBuilder.finalRequest(); if (finalRequest) { this.updateNetworkRequest(finalRequest); } } subresourceWebBundleInnerResponseParsed({innerRequestId, bundleRequestId}: Protocol.Network.SubresourceWebBundleInnerResponseParsedEvent): void { const extraInfoBuilder = this.getExtraInfoBuilder(innerRequestId); extraInfoBuilder.setWebBundleInnerRequestInfo({bundleRequestId}); const finalRequest = extraInfoBuilder.finalRequest(); if (finalRequest) { this.updateNetworkRequest(finalRequest); } } subresourceWebBundleInnerResponseError({innerRequestId, errorMessage}: Protocol.Network.SubresourceWebBundleInnerResponseErrorEvent): void { const extraInfoBuilder = this.getExtraInfoBuilder(innerRequestId); extraInfoBuilder.setWebBundleInnerRequestInfo({errorMessage}); const finalRequest = extraInfoBuilder.finalRequest(); if (finalRequest) { this.updateNetworkRequest(finalRequest); } } reportingApiReportAdded(data: Protocol.Network.ReportingApiReportAddedEvent): void { this.#manager.dispatchEventToListeners(Events.ReportingApiReportAdded, data.report); } reportingApiReportUpdated(data: Protocol.Network.ReportingApiReportUpdatedEvent): void { this.#manager.dispatchEventToListeners(Events.ReportingApiReportUpdated, data.report); } reportingApiEndpointsChangedForOrigin(data: Protocol.Network.ReportingApiEndpointsChangedForOriginEvent): void { this.#manager.dispatchEventToListeners(Events.ReportingApiEndpointsChangedForOrigin, data); } /** * @deprecated * This method is only kept for usage in a web test. */ private createNetworkRequest( requestId: Protocol.Network.RequestId, frameId: Protocol.Page.FrameId, loaderId: Protocol.Network.LoaderId, url: string, documentURL: string, initiator: Protocol.Network.Initiator|null): NetworkRequest { const request = NetworkRequest.create( requestId, url as Platform.DevToolsPath.UrlString, documentURL as Platform.DevToolsPath.UrlString, frameId, loaderId, initiator); requestToManagerMap.set(request, this.#manager); return request; } } let multiTargetNetworkManagerInstance: MultitargetNetworkManager|null; export class MultitargetNetworkManager extends Common.ObjectWrapper.ObjectWrapper<MultitargetNetworkManager.EventTypes> implements SDKModelObserver<NetworkManager> { #userAgentOverrideInternal: string; #userAgentMetadataOverride: Protocol.Emulation.UserAgentMetadata|null; #customAcceptedEncodings: Protocol.Network.ContentEncoding[]|null; readonly #networkAgents: Set<ProtocolProxyApi.NetworkApi>; readonly #fetchAgents: Set<ProtocolProxyApi.FetchApi>; readonly inflightMainResourceRequests: Map<string, NetworkRequest>; #networkConditionsInternal: Conditions; #updatingInterceptionPatternsPromise: Promise<void>|null; readonly #blockingEnabledSetting: Common.Settings.Setting<boolean>; readonly #blockedPatternsSetting: Common.Settings.Setting<BlockedPattern[]>; #effectiveBlockedURLs: string[]; readonly #urlsForRequestInterceptor: Platform.MapUtilities.Multimap<(arg0: InterceptedRequest) => Promise<void>, InterceptionPattern>; #extraHeaders?: Protocol.Network.Headers; #customUserAgent?: string; constructor() { super(); this.#userAgentOverrideInternal = ''; this.#userAgentMetadataOverride = null; this.#customAcceptedEncodings = null; this.#networkAgents = new Set(); this.#fetchAgents = new Set(); this.inflightMainResourceRequests = new Map(); this.#networkConditionsInternal = NoThrottlingConditions; this.#updatingInterceptionPatternsPromise = null; // TODO(allada) Remove these and merge it with request interception. this.#blockingEnabledSetting = Common.Settings.Settings.instance().moduleSetting('requestBlockingEnabled'); this.#blockedPatternsSetting = Common.Settings.Settings.instance().createSetting('networkBlockedPatterns', []); this.#effectiveBlockedURLs = []; this.updateBlockedPatterns(); this.#urlsForRequestInterceptor = new Platform.MapUtilities.Multimap(); TargetManager.instance().observeModels(NetworkManager, this); } static instance(opts: { forceNew: boolean|null, } = {forceNew: null}): MultitargetNetworkManager { const {forceNew} = opts; if (!multiTargetNetworkManagerInstance || forceNew) { multiTargetNetworkManagerInstance = new MultitargetNetworkManager(); } return multiTargetNetworkManagerInstance; } static dispose(): void { multiTargetNetworkManagerInstance = null; } static getChromeVersion(): string { const chromeRegex = /(?:^|\W)(?:Chrome|HeadlessChrome)\/(\S+)/; const chromeMatch = navigator.userAgent.match(chromeRegex); if (chromeMatch && chromeMatch.length > 1) { return chromeMatch[1]; } return ''; } static patchUserAgentWithChromeVersion(uaString: string): string { // Patches Chrome/ChrOS version from user #agent ("1.2.3.4" when user #agent is: "Chrome/1.2.3.4"). // Otherwise, ignore it. This assumes additional appVersions appear after the Chrome version. const chromeVersion = MultitargetNetworkManager.getChromeVersion(); if (chromeVersion.length > 0) { // "1.2.3.4" becomes "1