| // 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 enableDeviceBoundSessions(enable = true): Promise<Promise<Protocol.ProtocolResponseWithError>> { |
| return await this.#networkAgent.invoke_enableDeviceBoundSessions({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', |
| DeviceBoundSessionsAdded = 'DeviceBoundSessionsAdded', |
| DeviceBoundSessionEventOccurred = 'DeviceBoundSessionEventOccurred', |
| /* 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; |
| [Events.DeviceBoundSessionsAdded]: Protocol.Network.DeviceBoundSession[]; |
| [Events.DeviceBoundSessionEventOccurred]: Protocol.Network.DeviceBoundSessionEventOccurredEvent; |
| } |
| |
| /** |
| * 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, |
| renderBlockingBehavior, |
| }: 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); |
| if (renderBlockingBehavior) { |
| networkRequest.setRenderBlockingBehavior(renderBlockingBehavior); |
| } |
| 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, |
| deviceBoundSessionUsages, |
| 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: ExtraRequestInfo = { |
| blockedRequestCookies, |
| includedRequestCookies, |
| requestHeaders: this.headersMapToHeadersArray(headers), |
| deviceBoundSessionUsages, |
| 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 { |
| 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); |
| } |
| |
| directTCPSocketCreated(event: Protocol.Network.DirectTCPSocketCreatedEvent): void { |
| const requestURL = this.concatHostPort(event.remoteAddr, event.remotePort); |
| const networkRequest = NetworkRequest.createForSocket( |
| event.identifier, requestURL as Platform.DevToolsPath.UrlString, event.initiator); |
| networkRequest.hasNetworkData = true; |
| networkRequest.setRemoteAddress(event.remoteAddr, event.remotePort); |
| networkRequest.protocol = i18n.i18n.lockedString('tcp'); |
| |
| networkRequest.statusText = i18nString(UIStrings.directSocketStatusOpening); |
| networkRequest.directSocketInfo = { |
| type: DirectSocketType.TCP, |
| status: DirectSocketStatus.OPENING, |
| createOptions: { |
| remoteAddr: event.remoteAddr, |
| remotePort: event.remotePort, |
| noDelay: event.options.noDelay, |
| keepAliveDelay: event.options.keepAliveDelay, |
| sendBufferSize: event.options.sendBufferSize, |
| receiveBufferSize: event.options.receiveBufferSize, |
| dnsQueryType: event.options.dnsQueryType, |
| } |
| }; |
| networkRequest.setResourceType(Common.ResourceType.resourceTypes.DirectSocket); |
| networkRequest.setIssueTime(event.timestamp, event.timestamp); |
| |
| requestToManagerMap.set(networkRequest, this.#manager); |
| this.startNetworkRequest(networkRequest, null); |
| } |
| |
| directTCPSocketOpened(event: Protocol.Network.DirectTCPSocketOpenedEvent): void { |
| const networkRequest = this.#requestsById.get(event.identifier); |
| if (!networkRequest?.directSocketInfo) { |
| return; |
| } |
| networkRequest.responseReceivedTime = event.timestamp; |
| networkRequest.directSocketInfo.status = DirectSocketStatus.OPEN; |
| networkRequest.statusText = i18nString(UIStrings.directSocketStatusOpen); |
| networkRequest.directSocketInfo.openInfo = { |
| remoteAddr: event.remoteAddr, |
| remotePort: event.remotePort, |
| localAddr: event.localAddr, |
| localPort: event.localPort, |
| }; |
| networkRequest.setRemoteAddress(event.remoteAddr, event.remotePort); |
| const requestURL = this.concatHostPort(event.remoteAddr, event.remotePort); |
| networkRequest.setUrl(requestURL as Platform.DevToolsPath.UrlString); |
| this.updateNetworkRequest(networkRequest); |
| } |
| |
| directTCPSocketAborted(event: Protocol.Network.DirectTCPSocketAbortedEvent): void { |
| const networkRequest = this.#requestsById.get(event.identifier); |
| if (!networkRequest?.directSocketInfo) { |
| return; |
| } |
| networkRequest.failed = true; |
| networkRequest.directSocketInfo.status = DirectSocketStatus.ABORTED; |
| networkRequest.statusText = i18nString(UIStrings.directSocketStatusAborted); |
| networkRequest.directSocketInfo.errorMessage = event.errorMessage; |
| this.finishNetworkRequest(networkRequest, event.timestamp, 0); |
| } |
| |
| directTCPSocketClosed(event: Protocol.Network.DirectTCPSocketClosedEvent): void { |
| const networkRequest = this.#requestsById.get(event.identifier); |
| if (!networkRequest?.directSocketInfo) { |
| return; |
| } |
| networkRequest.statusText = i18nString(UIStrings.directSocketStatusClosed); |
| networkRequest.directSocketInfo.status = DirectSocketStatus.CLOSED; |
| this.finishNetworkRequest(networkRequest, event.timestamp, 0); |
| } |
| |
| directTCPSocketChunkSent(event: Protocol.Network.DirectTCPSocketChunkSentEvent): void { |
| const networkRequest = this.#requestsById.get(event.identifier); |
| if (!networkRequest) { |
| return; |
| } |
| |
| networkRequest.addDirectSocketChunk({ |
| data: event.data, |
| type: DirectSocketChunkType.SEND, |
| timestamp: event.timestamp, |
| }); |
| networkRequest.responseReceivedTime = event.timestamp; |
| |
| this.updateNetworkRequest(networkRequest); |
| } |
| |
| directTCPSocketChunkReceived(event: Protocol.Network.DirectTCPSocketChunkReceivedEvent): void { |
| const networkRequest = this.#requestsById.get(event.identifier); |
| if (!networkRequest) { |
| return; |
| } |
| |
| networkRequest.addDirectSocketChunk({ |
| data: event.data, |
| type: DirectSocketChunkType.RECEIVE, |
| timestamp: event.timestamp, |
| }); |
| networkRequest.responseReceivedTime = event.timestamp; |
| |
| this.updateNetworkRequest(networkRequest); |
| } |
| |
| directUDPSocketCreated(event: Protocol.Network.DirectUDPSocketCreatedEvent): void { |
| let requestURL = ''; |
| let type: DirectSocketType; |
| if (event.options.remoteAddr && event.options.remotePort) { |
| requestURL = this.concatHostPort(event.options.remoteAddr, event.options.remotePort); |
| type = DirectSocketType.UDP_CONNECTED; |
| } else if (event.options.localAddr) { |
| requestURL = this.concatHostPort(event.options.localAddr, event.options.localPort); |
| type = DirectSocketType.UDP_BOUND; |
| } else { |
| // Must be present in a valid command if remoteAddr |
| // is not specified. |
| return; |
| } |
| const networkRequest = NetworkRequest.createForSocket( |
| event.identifier, requestURL as Platform.DevToolsPath.UrlString, event.initiator); |
| networkRequest.hasNetworkData = true; |
| if (event.options.remoteAddr && event.options.remotePort) { |
| networkRequest.setRemoteAddress(event.options.remoteAddr, event.options.remotePort); |
| } |
| networkRequest.protocol = i18n.i18n.lockedString('udp'); |
| |
| networkRequest.statusText = i18nString(UIStrings.directSocketStatusOpening); |
| networkRequest.directSocketInfo = { |
| type, |
| status: DirectSocketStatus.OPENING, |
| createOptions: { |
| remoteAddr: event.options.remoteAddr, |
| remotePort: event.options.remotePort, |
| localAddr: event.options.localAddr, |
| localPort: event.options.localPort, |
| sendBufferSize: event.options.sendBufferSize, |
| receiveBufferSize: event.options.receiveBufferSize, |
| dnsQueryType: event.options.dnsQueryType, |
| multicastLoopback: event.options.multicastLoopback, |
| multicastTimeToLive: event.options.multicastTimeToLive, |
| multicastAllowAddressSharing: event.options.multicastAllowAddressSharing, |
| }, |
| joinedMulticastGroups: new Set(), |
| }; |
| networkRequest.setResourceType(Common.ResourceType.resourceTypes.DirectSocket); |
| networkRequest.setIssueTime(event.timestamp, event.timestamp); |
| |
| requestToManagerMap.set(networkRequest, this.#manager); |
| this.startNetworkRequest(networkRequest, null); |
| } |
| |
| directUDPSocketOpened(event: Protocol.Network.DirectUDPSocketOpenedEvent): void { |
| const networkRequest = this.#requestsById.get(event.identifier); |
| if (!networkRequest?.directSocketInfo) { |
| return; |
| } |
| let requestURL: string; |
| if (networkRequest.directSocketInfo.type === DirectSocketType.UDP_CONNECTED) { |
| if (!event.remoteAddr || !event.remotePort) { |
| // Connected socket must have remoteAdd and remotePort. |
| return; |
| } |
| networkRequest.setRemoteAddress(event.remoteAddr, event.remotePort); |
| requestURL = this.concatHostPort(event.remoteAddr, event.remotePort); |
| } else { |
| requestURL = this.concatHostPort(event.localAddr, event.localPort); |
| } |
| |
| networkRequest.setUrl(requestURL as Platform.DevToolsPath.UrlString); |
| networkRequest.responseReceivedTime = event.timestamp; |
| networkRequest.directSocketInfo.status = DirectSocketStatus.OPEN; |
| networkRequest.statusText = i18nString(UIStrings.directSocketStatusOpen); |
| networkRequest.directSocketInfo.openInfo = { |
| remoteAddr: event.remoteAddr, |
| remotePort: event.remotePort, |
| localAddr: event.localAddr, |
| localPort: event.localPort, |
| }; |
| |
| this.updateNetworkRequest(networkRequest); |
| } |
| |
| directUDPSocketAborted(event: Protocol.Network.DirectUDPSocketAbortedEvent): void { |
| const networkRequest = this.#requestsById.get(event.identifier); |
| if (!networkRequest?.directSocketInfo) { |
| return; |
| } |
| networkRequest.failed = true; |
| networkRequest.directSocketInfo.status = DirectSocketStatus.ABORTED; |
| networkRequest.statusText = i18nString(UIStrings.directSocketStatusAborted); |
| networkRequest.directSocketInfo.errorMessage = event.errorMessage; |
| this.finishNetworkRequest(networkRequest, event.timestamp, 0); |
| } |
| |
| directUDPSocketClosed(event: Protocol.Network.DirectUDPSocketClosedEvent): void { |
| const networkRequest = this.#requestsById.get(event.identifier); |
| if (!networkRequest?.directSocketInfo) { |
| return; |
| } |
| networkRequest.statusText = i18nString(UIStrings.directSocketStatusClosed); |
| networkRequest.directSocketInfo.status = DirectSocketStatus.CLOSED; |
| this.finishNetworkRequest(networkRequest, event.timestamp, 0); |
| } |
| |
| directUDPSocketChunkSent(event: Protocol.Network.DirectUDPSocketChunkSentEvent): void { |
| const networkRequest = this.#requestsById.get(event.identifier); |
| if (!networkRequest) { |
| return; |
| } |
| |
| networkRequest.addDirectSocketChunk({ |
| data: event.message.data, |
| type: DirectSocketChunkType.SEND, |
| timestamp: event.timestamp, |
| remoteAddress: event.message.remoteAddr, |
| remotePort: event.message.remotePort |
| }); |
| networkRequest.responseReceivedTime = event.timestamp; |
| |
| this.updateNetworkRequest(networkRequest); |
| } |
| |
| directUDPSocketChunkReceived(event: Protocol.Network.DirectUDPSocketChunkReceivedEvent): void { |
| const networkRequest = this.#requestsById.get(event.identifier); |
| if (!networkRequest) { |
| return; |
| } |
| |
| networkRequest.addDirectSocketChunk({ |
| data: event.message.data, |
| type: DirectSocketChunkType.RECEIVE, |
| timestamp: event.timestamp, |
| remoteAddress: event.message.remoteAddr, |
| remotePort: event.message.remotePort |
| }); |
| networkRequest.responseReceivedTime = event.timestamp; |
| |
| this.updateNetworkRequest(networkRequest); |
| } |
| |
| directUDPSocketJoinedMulticastGroup(event: Protocol.Network.DirectUDPSocketJoinedMulticastGroupEvent): void { |
| const networkRequest = this.#requestsById.get(event.identifier); |
| if (!networkRequest?.directSocketInfo) { |
| return; |
| } |
| if (!networkRequest.directSocketInfo.joinedMulticastGroups) { |
| networkRequest.directSocketInfo.joinedMulticastGroups = new Set(); |
| } |
| if (!networkRequest.directSocketInfo.joinedMulticastGroups.has(event.IPAddress)) { |
| networkRequest.directSocketInfo.joinedMulticastGroups.add(event.IPAddress); |
| this.updateNetworkRequest(networkRequest); |
| } |
| } |
| |
| directUDPSocketLeftMulticastGroup(event: Protocol.Network.DirectUDPSocketLeftMulticastGroupEvent): void { |
| const networkRequest = this.#requestsById.get(event.identifier); |
| if (!networkRequest?.directSocketInfo?.joinedMulticastGroups) { |
| return; |
| } |
| if (networkRequest.directSocketInfo.joinedMulticastGroups.delete(event.IPAddress)) { |
| this.updateNetworkRequest(networkRequest); |
| } |
| } |
| |
| 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); |
| } |
| |
| 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); |
| } |
| |
| deviceBoundSessionsAdded(_params: Protocol.Network.DeviceBoundSessionsAddedEvent): void { |
| this.#manager.dispatchEventToListeners(Events.DeviceBoundSessionsAdded, _params.sessions); |
| } |
| |
| deviceBoundSessionEventOccurred(_params: Protocol.Network.DeviceBoundSessionEventOccurredEvent): void { |
| this.#manager.dispatchEventToListeners(Events.DeviceBoundSessionEventOccurred, _params); |
| } |
| |
| policyUpdated(): void { |
| } |
| |
| /** |
| * @deprecated |
| * This method is only kept for usage in a web test. |
| */ |
| protected 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; |
| } |
| |
| private concatHostPort(host: string, port?: number): string { |
| if (!port || port === 0) { |
| return host; |
| } |
| return `${host}:${port}`; |
| } |
| } |
| |
| export type RequestConditionsSetting = { |
| url: string, |
| enabled: boolean, |
| }|{ |
| urlPattern: URLPatternConstructorString, |
| conditions: ThrottlingConditionKey, |
| enabled: boolean, |
| }; |
| |
| export type URLPatternConstructorString = Platform.Brand.Brand<string, 'URLPatternConstructorString'>; |
| |
| export const enum RequestURLPatternValidity { |
| VALID = 'valid', |
| FAILED_TO_PARSE = 'failed-to-parse', |
| HAS_REGEXP_GROUPS = 'has-regexp-groups', |
| } |
| |
| export class RequestURLPattern { |
| private constructor(readonly constructorString: URLPatternConstructorString, readonly pattern: URLPattern) { |
| if (pattern.hasRegExpGroups) { |
| throw new Error('RegExp groups are not allowed'); |
| } |
| } |
| |
| static isValidPattern(pattern: string): RequestURLPatternValidity { |
| try { |
| const urlPattern = new URLPattern(pattern); |
| return urlPattern.hasRegExpGroups ? RequestURLPatternValidity.HAS_REGEXP_GROUPS : RequestURLPatternValidity.VALID; |
| } catch { |
| return RequestURLPatternValidity.FAILED_TO_PARSE; |
| } |
| } |
| |
| static create(constructorString: URLPatternConstructorString): RequestURLPattern|null { |
| try { |
| const urlPattern = new URLPattern(constructorString); |
| return urlPattern.hasRegExpGroups ? null : new RequestURLPattern(constructorString, urlPattern); |
| } catch { |
| return null; |
| } |
| } |
| |
| static upgradeFromWildcard(pattern: string): RequestURLPattern|null { |
| const tryCreate = (constructorString: string): RequestURLPattern|null => { |
| const result = this.create(constructorString as URLPatternConstructorString); |
| if (result?.pattern.protocol === 'localhost' && result?.pattern.hostname === '') { |
| // localhost:1234 parses as a valid pattern, do the right thing here instead |
| return tryCreate(`*://${constructorString}`); |
| } |
| return result; |
| }; |
| |
| return tryCreate(pattern) // try as is |
| ?? |
| // Try to upgrade patterns created from the network panel, which either blocks the full url (sans |
| // protocol) or just the domain name. In both cases the wildcard patterns had implicit wildcards at the end. |
| // We explicitly add that here, which will match both domain names without path (implicitly setting pathname |
| // to '*') and urls with path (appending * to the pathname). |
| tryCreate(`*://${pattern}*`); |
| } |
| } |
| |
| export class RequestCondition extends Common.ObjectWrapper.ObjectWrapper<RequestCondition.EventTypes> { |
| #pattern: RequestURLPattern|{wildcardURL: string, upgradedPattern?: RequestURLPattern}; |
| #enabled: boolean; |
| #conditions: ThrottlingConditions; |
| #ruleIds = new Set<string>(); |
| |
| static createFromSetting(setting: RequestConditionsSetting): RequestCondition { |
| if ('urlPattern' in setting) { |
| const pattern = RequestURLPattern.create(setting.urlPattern) ?? { |
| wildcardURL: setting.urlPattern, |
| upgradedPattern: RequestURLPattern.upgradeFromWildcard(setting.urlPattern) ?? undefined, |
| }; |
| |
| const conditions = getPredefinedOrBlockingCondition(setting.conditions) ?? |
| customUserNetworkConditionsSetting().get().find(condition => condition.key === setting.conditions) ?? |
| NoThrottlingConditions; |
| |
| return new this(pattern, setting.enabled, conditions); |
| } |
| |
| const pattern = { |
| wildcardURL: setting.url, |
| upgradedPattern: RequestURLPattern.upgradeFromWildcard(setting.url) ?? undefined |
| }; |
| return new this(pattern, setting.enabled, BlockingConditions); |
| } |
| |
| static create(pattern: RequestURLPattern, conditions: ThrottlingConditions): RequestCondition { |
| return new this(pattern, /* enabled=*/ true, conditions); |
| } |
| |
| private constructor( |
| pattern: RequestURLPattern|{wildcardURL: string, upgradedPattern?: RequestURLPattern}, enabled: boolean, |
| conditions: ThrottlingConditions) { |
| super(); |
| this.#pattern = pattern; |
| this.#enabled = enabled; |
| this.#conditions = conditions; |
| } |
| |
| get isBlocking(): boolean { |
| return this.conditions === BlockingConditions; |
| } |
| |
| get ruleIds(): Set<string> { |
| return this.#ruleIds; |
| } |
| |
| get constructorString(): string|undefined { |
| return this.#pattern instanceof RequestURLPattern ? this.#pattern.constructorString : |
| this.#pattern.upgradedPattern?.constructorString; |
| } |
| |
| get wildcardURL(): string|undefined { |
| return 'wildcardURL' in this.#pattern ? this.#pattern.wildcardURL : undefined; |
| } |
| |
| get constructorStringOrWildcardURL(): string { |
| return this.#pattern instanceof RequestURLPattern ? |
| this.#pattern.constructorString : |
| (this.#pattern.upgradedPattern?.constructorString ?? this.#pattern.wildcardURL); |
| } |
| |
| set pattern(pattern: RequestURLPattern|string) { |
| if (typeof pattern === 'string') { |
| // TODO(pfaffe) Remove once the feature flag is no longer required |
| if (Root.Runtime.hostConfig.devToolsIndividualRequestThrottling?.enabled) { |
| throw new Error('Should not use wildcard urls'); |
| } |
| this.#pattern = { |
| wildcardURL: pattern, |
| upgradedPattern: RequestURLPattern.upgradeFromWildcard(pattern) ?? undefined |
| }; |
| } else { |
| this.#pattern = pattern; |
| } |
| this.dispatchEventToListeners(RequestCondition.Events.REQUEST_CONDITION_CHANGED); |
| } |
| |
| get enabled(): boolean { |
| return this.#enabled; |
| } |
| |
| set enabled(enabled: boolean) { |
| this.#enabled = enabled; |
| this.dispatchEventToListeners(RequestCondition.Events.REQUEST_CONDITION_CHANGED); |
| } |
| |
| get conditions(): ThrottlingConditions { |
| return this.#conditions; |
| } |
| |
| set conditions(conditions: ThrottlingConditions) { |
| this.#conditions = conditions; |
| this.#ruleIds = new Set(); |
| this.dispatchEventToListeners(RequestCondition.Events.REQUEST_CONDITION_CHANGED); |
| } |
| |
| toSetting(): RequestConditionsSetting { |
| const enabled = this.enabled; |
| if (this.#pattern instanceof RequestURLPattern) { |
| return {enabled, urlPattern: this.#pattern.constructorString, conditions: this.#conditions.key}; |
| } |
| if (this.#conditions !== BlockingConditions && this.#pattern.upgradedPattern) { |
| return {enabled, urlPattern: this.#pattern.upgradedPattern.constructorString, conditions: this.#conditions.key}; |
| } |
| return {enabled, url: this.#pattern.wildcardURL}; |
| } |
| |
| get originalOrUpgradedURLPattern(): URLPattern|undefined { |
| return this.#pattern instanceof RequestURLPattern ? this.#pattern.pattern : this.#pattern.upgradedPattern?.pattern; |
| } |
| } |
| |
| export namespace RequestCondition { |
| export const enum Events { |
| REQUEST_CONDITION_CHANGED = 'request-condition-changed', |
| } |
| |
| export interface EventTypes { |
| [Events.REQUEST_CONDITION_CHANGED]: void; |
| } |
| } |
| |
| export class RequestConditions extends Common.ObjectWrapper.ObjectWrapper<RequestConditions.EventTypes> { |
| readonly #setting = |
| Common.Settings.Settings.instance().createSetting<RequestConditionsSetting[]>('network-blocked-patterns', []); |
| readonly #conditionsEnabledSetting = |
| Common.Settings.Settings.instance().moduleSetting<boolean>('request-blocking-enabled'); |
| readonly #conditions: RequestCondition[] = []; |
| readonly #requestConditionsById = new Map<string, { |
| conditions: Conditions, |
| urlPattern?: string, |
| }>(); |
| #conditionsAppliedForTestPromise: Promise<unknown> = Promise.resolve(); |
| |
| constructor() { |
| super(); |
| for (const condition of this.#setting.get()) { |
| try { |
| this.#conditions.push(RequestCondition.createFromSetting(condition)); |
| } catch (e) { |
| console.error('Error loading throttling settings: ', e); |
| } |
| } |
| for (const condition of this.#conditions) { |
| condition.addEventListener(RequestCondition.Events.REQUEST_CONDITION_CHANGED, this.#conditionsChanged, this); |
| } |
| this.#conditionsEnabledSetting.addChangeListener( |
| () => this.dispatchEventToListeners(RequestConditions.Events.REQUEST_CONDITIONS_CHANGED)); |
| } |
| |
| get count(): number { |
| return this.#conditions.length; |
| } |
| |
| get conditionsEnabled(): boolean { |
| return this.#conditionsEnabledSetting.get(); |
| } |
| |
| set conditionsEnabled(enabled: boolean) { |
| if (this.#conditionsEnabledSetting.get() === enabled) { |
| return; |
| } |
| this.#conditionsEnabledSetting.set(enabled); |
| } |
| |
| findCondition(pattern: string): RequestCondition|undefined { |
| if (Root.Runtime.hostConfig.devToolsIndividualRequestThrottling?.enabled) { |
| return this.#conditions.find(condition => condition.constructorString === pattern); |
| } |
| return this.#conditions.find(condition => condition.wildcardURL === pattern); |
| } |
| |
| has(url: string): boolean { |
| return Boolean(this.findCondition(url)); |
| } |
| |
| add(...conditions: RequestCondition[]): void { |
| this.#conditions.push(...conditions); |
| for (const condition of conditions) { |
| condition.addEventListener(RequestCondition.Events.REQUEST_CONDITION_CHANGED, this.#conditionsChanged, this); |
| } |
| this.#conditionsChanged(); |
| } |
| |
| decreasePriority(condition: RequestCondition): void { |
| const index = this.#conditions.indexOf(condition); |
| if (index < 0 || index >= this.#conditions.length - 1) { |
| return; |
| } |
| |
| Platform.ArrayUtilities.swap(this.#conditions, index, index + 1); |
| this.#conditionsChanged(); |
| } |
| |
| increasePriority(condition: RequestCondition): void { |
| const index = this.#conditions.indexOf(condition); |
| if (index <= 0) { |
| return; |
| } |
| |
| Platform.ArrayUtilities.swap(this.#conditions, index - 1, index); |
| this.#conditionsChanged(); |
| } |
| |
| delete(condition: RequestCondition): void { |
| const index = this.#conditions.indexOf(condition); |
| if (index < 0) { |
| return; |
| } |
| condition.removeEventListener(RequestCondition.Events.REQUEST_CONDITION_CHANGED, this.#conditionsChanged, this); |
| this.#conditions.splice(index, 1); |
| this.#conditionsChanged(); |
| } |
| |
| clear(): void { |
| this.#conditions.splice(0); |
| this.#conditionsChanged(); |
| for (const condition of this.#conditions) { |
| condition.removeEventListener(RequestCondition.Events.REQUEST_CONDITION_CHANGED, this.#conditionsChanged, this); |
| } |
| } |
| |
| #conditionsChanged(): void { |
| this.#setting.set(this.#conditions.map(condition => condition.toSetting())); |
| this.dispatchEventToListeners(RequestConditions.Events.REQUEST_CONDITIONS_CHANGED); |
| } |
| |
| get conditions(): IteratorObject<RequestCondition> { |
| return this.#conditions.values(); |
| } |
| |
| applyConditions(offline: boolean, globalConditions: Conditions|null, ...agents: ProtocolProxyApi.NetworkApi[]): |
| boolean { |
| function isNonBlockingCondition(condition: ThrottlingConditions): condition is Conditions { |
| return !('block' in condition); |
| } |
| if (Root.Runtime.hostConfig.devToolsIndividualRequestThrottling?.enabled) { |
| const urlPatterns: Protocol.Network.BlockPattern[] = []; |
| // We store all this info out-of-band to prevent races with changing conditions while the promise is still pending |
| const matchedNetworkConditions: Array<{conditions: Conditions, ruleIds?: Set<string>, urlPattern?: string}> = []; |
| if (this.conditionsEnabled) { |
| for (const condition of this.#conditions) { |
| const urlPattern = condition.constructorString; |
| const conditions = condition.conditions; |
| if (!condition.enabled || !urlPattern || conditions === NoThrottlingConditions) { |
| continue; |
| } |
| const block = !isNonBlockingCondition(conditions); |
| urlPatterns.push({urlPattern, block}); |
| if (!block) { |
| const {ruleIds} = condition; |
| matchedNetworkConditions.push({ruleIds, urlPattern, conditions}); |
| } |
| } |
| } |
| |
| if (globalConditions) { |
| matchedNetworkConditions.push({conditions: globalConditions}); |
| } |
| |
| const promises: Array<Promise<unknown>> = []; |
| |
| for (const agent of agents) { |
| promises.push(agent.invoke_setBlockedURLs({urlPatterns})); |
| promises.push(agent |
| .invoke_emulateNetworkConditionsByRule({ |
| offline, |
| matchedNetworkConditions: matchedNetworkConditions.map( |
| ({urlPattern, conditions}) => ({ |
| urlPattern: urlPattern ?? '', |
| latency: conditions.latency, |
| downloadThroughput: conditions.download < 0 ? 0 : conditions.download, |
| uploadThroughput: conditions.upload < 0 ? 0 : conditions.upload, |
| packetLoss: (conditions.packetLoss ?? 0) < 0 ? 0 : conditions.packetLoss, |
| packetQueueLength: conditions.packetQueueLength, |
| packetReordering: conditions.packetReordering, |
| connectionType: NetworkManager.connectionType(conditions), |
| })) |
| }) |
| .then(response => { |
| if (!response.getError()) { |
| for (let i = 0; i < response.ruleIds.length; ++i) { |
| const ruleId = response.ruleIds[i]; |
| const {ruleIds, conditions, urlPattern} = matchedNetworkConditions[i]; |
| if (ruleIds) { |
| this.#requestConditionsById.set(ruleId, {urlPattern, conditions}); |
| matchedNetworkConditions[i].ruleIds?.add(ruleId); |
| } |
| } |
| } |
| })); |
| promises.push(agent.invoke_overrideNetworkState({ |
| offline, |
| latency: globalConditions?.latency ?? 0, |
| downloadThroughput: globalConditions?.download ?? -1, |
| uploadThroughput: globalConditions?.upload ?? -1, |
| connectionType: globalConditions ? NetworkManager.connectionType(globalConditions) : |
| Protocol.Network.ConnectionType.None, |
| })); |
| } |
| |
| this.#conditionsAppliedForTestPromise = this.#conditionsAppliedForTestPromise.then(() => Promise.all(promises)); |
| return urlPatterns.length > 0; |
| } |
| |
| const urls = this.conditionsEnabled ? |
| this.#conditions.filter(condition => condition.enabled && condition.wildcardURL) |
| .map(condition => condition.wildcardURL as string) : |
| []; |
| |
| for (const agent of agents) { |
| void agent.invoke_setBlockedURLs({urls}); |
| } |
| return urls.length > 0; |
| } |
| |
| conditionsAppliedForTest(): Promise<unknown> { |
| return this.#conditionsAppliedForTestPromise; |
| } |
| |
| conditionsForId(appliedNetworkConditionsId: string): AppliedNetworkConditions|undefined { |
| const requestConditions = this.#requestConditionsById.get(appliedNetworkConditionsId); |
| if (!requestConditions) { |
| return undefined; |
| } |
| const {conditions, urlPattern} = requestConditions; |
| return new AppliedNetworkConditions(conditions, appliedNetworkConditionsId, urlPattern); |
| } |
| } |
| |
| export namespace RequestConditions { |
| export const enum Events { |
| REQUEST_CONDITIONS_CHANGED = 'request-conditions-changed', |
| } |
| export interface EventTypes { |
| [Events.REQUEST_CONDITIONS_CHANGED]: void; |
| } |
| } |
| |
| export class AppliedNetworkConditions { |
| constructor( |
| readonly conditions: Conditions, readonly appliedNetworkConditionsId: string, readonly urlPattern?: string) { |
| } |
| } |
| |
| export class MultitargetNetworkManager extends Common.ObjectWrapper.ObjectWrapper<MultitargetNetworkManager.EventTypes> |
| implements SDKModelObserver<NetworkManager> { |
| readonly #targetManager: TargetManager; |
| #userAgentOverride = ''; |
| #userAgentMetadataOverride: Protocol.Emulation.UserAgentMetadata|null = null; |
| #customAcceptedEncodings: Protocol.Network.ContentEncoding[]|null = null; |
| readonly #networkAgents = new Set<ProtocolProxyApi.NetworkApi>(); |
| readonly #fetchAgents = new Set<ProtocolProxyApi.FetchApi>(); |
| readonly inflightMainResourceRequests = new Map<string, NetworkRequest>(); |
| #networkConditions: Conditions = NoThrottlingConditions; |
| #updatingInterceptionPatternsPromise: Promise<void>|null = null; |
| readonly #requestConditions = new RequestConditions(); |
| readonly #urlsForRequestInterceptor: |
| Platform.MapUtilities.Multimap<(arg0: InterceptedRequest) => Promise<void>, InterceptionPattern> = |
| new Platform.MapUtilities.Multimap(); |
| #extraHeaders?: Protocol.Network.Headers; |
| #customUserAgent?: string; |
| #isBlocking = false; |
| |
| constructor(targetManager: TargetManager) { |
| super(); |
| this.#targetManager = targetManager; |
| |
| // TODO(allada) Remove these and merge it with request interception. |
| const blockedPatternChanged: () => void = () => { |
| this.updateBlockedPatterns(); |
| this.dispatchEventToListeners(MultitargetNetworkManager.Events.BLOCKED_PATTERNS_CHANGED); |
| }; |
| this.#requestConditions.addEventListener( |
| RequestConditions.Events.REQUEST_CONDITIONS_CHANGED, blockedPatternChanged); |
| this.updateBlockedPatterns(); |
| |
| this.#targetManager.observeModels(NetworkManager, this); |
| } |
| |
| static instance(opts: { |
| forceNew: boolean|null, |
| targetManager?: TargetManager, |
| } = {forceNew: null}): MultitargetNetworkManager { |
| const {forceNew, targetManager} = opts; |
| if (!Root.DevToolsContext.globalInstance().has(MultitargetNetworkManager) || forceNew) { |
| Root.DevToolsContext.globalInstance().set( |
| MultitargetNetworkManager, new MultitargetNetworkManager(targetManager ?? TargetManager.instance())); |
| } |
| |
| return Root.DevToolsContext.globalInstance().get(MultitargetNetworkManager); |
| } |
| |
| static dispose(): void { |
| Root.DevToolsContext.globalInstance().delete(MultitargetNetworkManager); |
| } |
| |
| 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 = Root.Runtime.getChromeVersion(); |
| if (chromeVersion.length > 0) { |
| // "1.2.3.4" becomes "1.0.100.0" |
| const additionalAppVersion = chromeVersion.split('.', 1)[0] + '.0.100.0'; |
| return Platform.StringUtilities.sprintf(uaString, chromeVersion, additionalAppVersion); |
| } |
| return uaString; |
| } |
| |
| static patchUserAgentMetadataWithChromeVersion(userAgentMetadata: Protocol.Emulation.UserAgentMetadata): void { |
| // Patches Chrome/ChrOS version from user #agent metadata ("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. |
| if (!userAgentMetadata.brands) { |
| return; |
| } |
| const chromeVersion = Root.Runtime.getChromeVersion(); |
| if (chromeVersion.length === 0) { |
| return; |
| } |
| |
| const majorVersion = chromeVersion.split('.', 1)[0]; |
| for (const brand of userAgentMetadata.brands) { |
| if (brand.version.includes('%s')) { |
| brand.version = Platform.StringUtilities.sprintf(brand.version, majorVersion); |
| } |
| } |
| |
| if (userAgentMetadata.fullVersion) { |
| if (userAgentMetadata.fullVersion.includes('%s')) { |
| userAgentMetadata.fullVersion = Platform.StringUtilities.sprintf(userAgentMetadata.fullVersion, chromeVersion); |
| } |
| } |
| } |
| |
| modelAdded(networkManager: NetworkManager): void { |
| const networkAgent = networkManager.target().networkAgent(); |
| const fetchAgent = networkManager.target().fetchAgent(); |
| if (this.#extraHeaders) { |
| void networkAgent.invoke_setExtraHTTPHeaders({headers: this.#extraHeaders}); |
| } |
| if (this.currentUserAgent()) { |
| void networkAgent.invoke_setUserAgentOverride( |
| {userAgent: this.currentUserAgent(), userAgentMetadata: this.#userAgentMetadataOverride || undefined}); |
| } |
| this.#requestConditions.applyConditions( |
| this.isOffline(), this.isThrottling() ? this.#networkConditions : null, networkAgent); |
| if (this.isIntercepting()) { |
| void fetchAgent.invoke_enable({patterns: this.#urlsForRequestInterceptor.valuesArray()}); |
| } |
| if (this.#customAcceptedEncodings === null) { |
| void networkAgent.invoke_clearAcceptedEncodingsOverride(); |
| } else { |
| void networkAgent.invoke_setAcceptedEncodings({encodings: this.#customAcceptedEncodings}); |
| } |
| this.#networkAgents.add(networkAgent); |
| this.#fetchAgents.add(fetchAgent); |
| if (this.isThrottling() && !Root.Runtime.hostConfig.devToolsIndividualRequestThrottling?.enabled) { |
| this.updateNetworkConditions(networkAgent); |
| } |
| } |
| |
| modelRemoved(networkManager: NetworkManager): void { |
| for (const entry of this.inflightMainResourceRequests) { |
| const manager = NetworkManager.forRequest((entry[1])); |
| if (manager !== networkManager) { |
| continue; |
| } |
| this.inflightMainResourceRequests.delete((entry[0])); |
| } |
| this.#networkAgents.delete(networkManager.target().networkAgent()); |
| this.#fetchAgents.delete(networkManager.target().fetchAgent()); |
| } |
| |
| isThrottling(): boolean { |
| return this.#networkConditions.download >= 0 || this.#networkConditions.upload >= 0 || |
| this.#networkConditions.latency > 0; |
| } |
| |
| isOffline(): boolean { |
| return !this.#networkConditions.download && !this.#networkConditions.upload; |
| } |
| |
| setNetworkConditions(conditions: Conditions): void { |
| this.#networkConditions = conditions; |
| if (Root.Runtime.hostConfig.devToolsIndividualRequestThrottling?.enabled) { |
| this.#requestConditions.applyConditions( |
| this.isOffline(), this.isThrottling() ? this.#networkConditions : null, ...this.#networkAgents); |
| } else { |
| for (const agent of this.#networkAgents) { |
| this.updateNetworkConditions(agent); |
| } |
| } |
| this.dispatchEventToListeners(MultitargetNetworkManager.Events.CONDITIONS_CHANGED); |
| } |
| |
| networkConditions(): Conditions { |
| return this.#networkConditions; |
| } |
| |
| private updateNetworkConditions(networkAgent: ProtocolProxyApi.NetworkApi): void { |
| const conditions = this.#networkConditions; |
| if (!this.isThrottling()) { |
| void networkAgent.invoke_emulateNetworkConditions({ |
| offline: false, |
| latency: 0, |
| downloadThroughput: 0, |
| uploadThroughput: 0, |
| }); |
| } else { |
| void networkAgent.invoke_emulateNetworkConditions({ |
| offline: this.isOffline(), |
| latency: conditions.latency, |
| downloadThroughput: conditions.download < 0 ? 0 : conditions.download, |
| uploadThroughput: conditions.upload < 0 ? 0 : conditions.upload, |
| packetLoss: (conditions.packetLoss ?? 0) < 0 ? 0 : conditions.packetLoss, |
| packetQueueLength: conditions.packetQueueLength, |
| packetReordering: conditions.packetReordering, |
| connectionType: NetworkManager.connectionType(conditions), |
| }); |
| } |
| } |
| |
| setExtraHTTPHeaders(headers: Protocol.Network.Headers): void { |
| this.#extraHeaders = headers; |
| for (const agent of this.#networkAgents) { |
| void agent.invoke_setExtraHTTPHeaders({headers: this.#extraHeaders}); |
| } |
| } |
| |
| currentUserAgent(): string { |
| return this.#customUserAgent ? this.#customUserAgent : this.#userAgentOverride; |
| } |
| |
| private updateUserAgentOverride(): void { |
| const userAgent = this.currentUserAgent(); |
| for (const agent of this.#networkAgents) { |
| void agent.invoke_setUserAgentOverride( |
| {userAgent, userAgentMetadata: this.#userAgentMetadataOverride || undefined}); |
| } |
| } |
| |
| setUserAgentOverride(userAgent: string, userAgentMetadataOverride: Protocol.Emulation.UserAgentMetadata|null): void { |
| const uaChanged = (this.#userAgentOverride !== userAgent); |
| this.#userAgentOverride = userAgent; |
| if (!this.#customUserAgent) { |
| this.#userAgentMetadataOverride = userAgentMetadataOverride; |
| this.updateUserAgentOverride(); |
| } else { |
| this.#userAgentMetadataOverride = null; |
| } |
| |
| if (uaChanged) { |
| this.dispatchEventToListeners(MultitargetNetworkManager.Events.USER_AGENT_CHANGED); |
| } |
| } |
| |
| setCustomUserAgentOverride( |
| userAgent: string, userAgentMetadataOverride: Protocol.Emulation.UserAgentMetadata|null = null): void { |
| this.#customUserAgent = userAgent; |
| this.#userAgentMetadataOverride = userAgentMetadataOverride; |
| this.updateUserAgentOverride(); |
| } |
| |
| setCustomAcceptedEncodingsOverride(acceptedEncodings: Protocol.Network.ContentEncoding[]): void { |
| this.#customAcceptedEncodings = acceptedEncodings; |
| this.updateAcceptedEncodingsOverride(); |
| this.dispatchEventToListeners(MultitargetNetworkManager.Events.ACCEPTED_ENCODINGS_CHANGED); |
| } |
| |
| clearCustomAcceptedEncodingsOverride(): void { |
| this.#customAcceptedEncodings = null; |
| this.updateAcceptedEncodingsOverride(); |
| this.dispatchEventToListeners(MultitargetNetworkManager.Events.ACCEPTED_ENCODINGS_CHANGED); |
| } |
| |
| isAcceptedEncodingOverrideSet(): boolean { |
| return this.#customAcceptedEncodings !== null; |
| } |
| |
| private updateAcceptedEncodingsOverride(): void { |
| const customAcceptedEncodings = this.#customAcceptedEncodings; |
| for (const agent of this.#networkAgents) { |
| if (customAcceptedEncodings === null) { |
| void agent.invoke_clearAcceptedEncodingsOverride(); |
| } else { |
| void agent.invoke_setAcceptedEncodings({encodings: customAcceptedEncodings}); |
| } |
| } |
| } |
| |
| get requestConditions(): RequestConditions { |
| return this.#requestConditions; |
| } |
| |
| isBlocking(): boolean { |
| return this.#isBlocking && this.requestConditions.conditionsEnabled; |
| } |
| |
| /** |
| * @deprecated Kept for layout tests |
| * TODO(pfaffe) remove |
| */ |
| private setBlockingEnabled(enabled: boolean): void { |
| this.requestConditions.conditionsEnabled = enabled; |
| } |
| |
| /** |
| * @deprecated Kept for layout tests |
| * TODO(pfaffe) remove |
| */ |
| private setBlockedPatterns(patterns: Array<{url: string, enabled: boolean}>): void { |
| this.requestConditions.clear(); |
| this.requestConditions.add(...patterns.map(pattern => RequestCondition.createFromSetting(pattern))); |
| } |
| |
| private updateBlockedPatterns(): void { |
| this.#isBlocking = this.#requestConditions.applyConditions( |
| this.isOffline(), this.isThrottling() ? this.#networkConditions : null, ...this.#networkAgents); |
| } |
| |
| isIntercepting(): boolean { |
| return Boolean(this.#urlsForRequestInterceptor.size); |
| } |
| |
| setInterceptionHandlerForPatterns( |
| patterns: InterceptionPattern[], requestInterceptor: (arg0: InterceptedRequest) => Promise<void>): Promise<void> { |
| // Note: requestInterceptors may receive interception #requests for patterns they did not subscribe to. |
| this.#urlsForRequestInterceptor.deleteAll(requestInterceptor); |
| for (const newPattern of patterns) { |
| this.#urlsForRequestInterceptor.set(requestInterceptor, newPattern); |
| } |
| return this.updateInterceptionPatternsOnNextTick(); |
| } |
| |
| private updateInterceptionPatternsOnNextTick(): Promise<void> { |
| // This is used so we can register and unregister patterns in loops without sending lots of protocol messages. |
| if (!this.#updatingInterceptionPatternsPromise) { |
| this.#updatingInterceptionPatternsPromise = Promise.resolve().then(this.updateInterceptionPatterns.bind(this)); |
| } |
| return this.#updatingInterceptionPatternsPromise; |
| } |
| |
| private async updateInterceptionPatterns(): Promise<void> { |
| if (!Common.Settings.Settings.instance().moduleSetting('cache-disabled').get()) { |
| Common.Settings.Settings.instance().moduleSetting('cache-disabled').set(true); |
| } |
| this.#updatingInterceptionPatternsPromise = null; |
| const promises = ([] as Array<Promise<unknown>>); |
| for (const agent of this.#fetchAgents) { |
| promises.push(agent.invoke_enable({patterns: this.#urlsForRequestInterceptor.valuesArray()})); |
| } |
| this.dispatchEventToListeners(MultitargetNetworkManager.Events.INTERCEPTORS_CHANGED); |
| await Promise.all(promises); |
| } |
| |
| async requestIntercepted(interceptedRequest: InterceptedRequest): Promise<void> { |
| for (const requestInterceptor of this.#urlsForRequestInterceptor.keysArray()) { |
| await requestInterceptor(interceptedRequest); |
| if (interceptedRequest.hasResponded() && interceptedRequest.networkRequest) { |
| this.dispatchEventToListeners( |
| MultitargetNetworkManager.Events.REQUEST_INTERCEPTED, interceptedRequest.networkRequest.requestId()); |
| return; |
| } |
| } |
| if (!interceptedRequest.hasResponded()) { |
| interceptedRequest.continueRequestWithoutChange(); |
| } |
| } |
| |
| clearBrowserCache(): void { |
| for (const agent of this.#networkAgents) { |
| void agent.invoke_clearBrowserCache(); |
| } |
| } |
| |
| clearBrowserCookies(): void { |
| for (const agent of this.#networkAgents) { |
| void agent.invoke_clearBrowserCookies(); |
| } |
| } |
| |
| async getCertificate(origin: string): Promise<string[]> { |
| const target = this.#targetManager.primaryPageTarget(); |
| if (!target) { |
| return []; |
| } |
| const certificate = await target.networkAgent().invoke_getCertificate({origin}); |
| if (!certificate) { |
| return []; |
| } |
| return certificate.tableNames; |
| } |
| |
| appliedRequestConditions(requestInternal: NetworkRequest): AppliedNetworkConditions|undefined { |
| if (!requestInternal.appliedNetworkConditionsId) { |
| return undefined; |
| } |
| return this.requestConditions.conditionsForId(requestInternal.appliedNetworkConditionsId); |
| } |
| } |
| |
| export namespace MultitargetNetworkManager { |
| export const enum Events { |
| BLOCKED_PATTERNS_CHANGED = 'BlockedPatternsChanged', |
| CONDITIONS_CHANGED = 'ConditionsChanged', |
| USER_AGENT_CHANGED = 'UserAgentChanged', |
| INTERCEPTORS_CHANGED = 'InterceptorsChanged', |
| ACCEPTED_ENCODINGS_CHANGED = 'AcceptedEncodingsChanged', |
| REQUEST_INTERCEPTED = 'RequestIntercepted', |
| REQUEST_FULFILLED = 'RequestFulfilled', |
| } |
| |
| export interface EventTypes { |
| [Events.BLOCKED_PATTERNS_CHANGED]: void; |
| [Events.CONDITIONS_CHANGED]: void; |
| [Events.USER_AGENT_CHANGED]: void; |
| [Events.INTERCEPTORS_CHANGED]: void; |
| [Events.ACCEPTED_ENCODINGS_CHANGED]: void; |
| [Events.REQUEST_INTERCEPTED]: string; |
| [Events.REQUEST_FULFILLED]: Platform.DevToolsPath.UrlString; |
| } |
| } |
| |
| export class InterceptedRequest { |
| readonly #fetchAgent: ProtocolProxyApi.FetchApi; |
| #hasResponded = false; |
| request: Protocol.Network.Request; |
| resourceType: Protocol.Network.ResourceType; |
| responseStatusCode: number|undefined; |
| responseHeaders: Protocol.Fetch.HeaderEntry[]|undefined; |
| requestId: Protocol.Fetch.RequestId; |
| networkRequest: NetworkRequest|null; |
| |
| constructor( |
| fetchAgent: ProtocolProxyApi.FetchApi, |
| request: Protocol.Network.Request, |
| resourceType: Protocol.Network.ResourceType, |
| requestId: Protocol.Fetch.RequestId, |
| networkRequest: NetworkRequest|null, |
| responseStatusCode?: number, |
| responseHeaders?: Protocol.Fetch.HeaderEntry[], |
| ) { |
| this.#fetchAgent = fetchAgent; |
| this.request = request; |
| this.resourceType = resourceType; |
| this.responseStatusCode = responseStatusCode; |
| this.responseHeaders = responseHeaders; |
| this.requestId = requestId; |
| this.networkRequest = networkRequest; |
| } |
| |
| hasResponded(): boolean { |
| return this.#hasResponded; |
| } |
| |
| static mergeSetCookieHeaders( |
| originalSetCookieHeaders: Protocol.Fetch.HeaderEntry[], |
| setCookieHeadersFromOverrides: Protocol.Fetch.HeaderEntry[]): Protocol.Fetch.HeaderEntry[] { |
| // Generates a map containing the `set-cookie` headers. Valid `set-cookie` |
| // headers are stored by the cookie name. Malformed `set-cookie` headers are |
| // stored by the whole header value. Duplicates are allowed. |
| const generateHeaderMap = (headers: Protocol.Fetch.HeaderEntry[]): Map<string, string[]> => { |
| const result = new Map<string, string[]>(); |
| for (const header of headers) { |
| // The regex matches cookie headers of the form '<header-name>=<header-value>'. |
| // <header-name> is a token as defined in https://www.rfc-editor.org/rfc/rfc9110.html#name-tokens. |
| // The shape of <header-value> is not being validated at all here. |
| const match = header.value.match(/^([a-zA-Z0-9!#$%&'*+.^_`|~-]+=)(.*)$/); |
| if (match) { |
| if (result.has(match[1])) { |
| result.get(match[1])?.push(header.value); |
| } else { |
| result.set(match[1], [header.value]); |
| } |
| } else if (result.has(header.value)) { |
| result.get(header.value)?.push(header.value); |
| } else { |
| result.set(header.value, [header.value]); |
| } |
| } |
| return result; |
| }; |
| |
| const originalHeadersMap = generateHeaderMap(originalSetCookieHeaders); |
| const overridesHeaderMap = generateHeaderMap(setCookieHeadersFromOverrides); |
| |
| // Iterate over original headers. If the same key is found among the |
| // overrides, use those instead. |
| const mergedHeaders: Protocol.Fetch.HeaderEntry[] = []; |
| for (const [key, headerValues] of originalHeadersMap) { |
| if (overridesHeaderMap.has(key)) { |
| for (const headerValue of overridesHeaderMap.get(key) || []) { |
| mergedHeaders.push({name: 'set-cookie', value: headerValue}); |
| } |
| } else { |
| for (const headerValue of headerValues) { |
| mergedHeaders.push({name: 'set-cookie', value: headerValue}); |
| } |
| } |
| } |
| |
| // Finally add all overrides which have not been added yet. |
| for (const [key, headerValues] of overridesHeaderMap) { |
| if (originalHeadersMap.has(key)) { |
| continue; |
| } |
| for (const headerValue of headerValues) { |
| mergedHeaders.push({name: 'set-cookie', value: headerValue}); |
| } |
| } |
| return mergedHeaders; |
| } |
| |
| async continueRequestWithContent( |
| contentBlob: Blob, encoded: boolean, responseHeaders: Protocol.Fetch.HeaderEntry[], |
| isBodyOverridden: boolean): Promise<void> { |
| this.#hasResponded = true; |
| const body = encoded ? await contentBlob.text() : await Common.Base64.encode(contentBlob).catch(err => { |
| console.error(err); |
| return ''; |
| }); |
| const responseCode = isBodyOverridden ? 200 : (this.responseStatusCode || 200); |
| |
| if (this.networkRequest) { |
| const originalSetCookieHeaders = |
| this.networkRequest?.originalResponseHeaders.filter(header => header.name === 'set-cookie') || []; |
| const setCookieHeadersFromOverrides = responseHeaders.filter(header => header.name === 'set-cookie'); |
| this.networkRequest.setCookieHeaders = |
| InterceptedRequest.mergeSetCookieHeaders(originalSetCookieHeaders, setCookieHeadersFromOverrides); |
| this.networkRequest.hasOverriddenContent = isBodyOverridden; |
| } |
| |
| void this.#fetchAgent.invoke_fulfillRequest({requestId: this.requestId, responseCode, body, responseHeaders}); |
| MultitargetNetworkManager.instance().dispatchEventToListeners( |
| MultitargetNetworkManager.Events.REQUEST_FULFILLED, this.request.url as Platform.DevToolsPath.UrlString); |
| } |
| |
| continueRequestWithoutChange(): void { |
| console.assert(!this.#hasResponded); |
| this.#hasResponded = true; |
| void this.#fetchAgent.invoke_continueRequest({requestId: this.requestId}); |
| } |
| |
| async responseBody(): Promise<TextUtils.ContentData.ContentDataOrError> { |
| const response = await this.#fetchAgent.invoke_getResponseBody({requestId: this.requestId}); |
| const error = response.getError(); |
| if (error) { |
| return {error}; |
| } |
| |
| const {mimeType, charset} = this.getMimeTypeAndCharset(); |
| return new TextUtils.ContentData.ContentData( |
| response.body, response.base64Encoded, mimeType ?? 'application/octet-stream', charset ?? undefined); |
| } |
| |
| isRedirect(): boolean { |
| return this.responseStatusCode !== undefined && this.responseStatusCode >= 300 && this.responseStatusCode < 400; |
| } |
| |
| /** |
| * Tries to determine the MIME type and charset for this intercepted request. |
| * Looks at the intercepted response headers first (for Content-Type header), then |
| * checks the `NetworkRequest` if we have one. |
| */ |
| getMimeTypeAndCharset(): {mimeType: string|null, charset: string|null} { |
| for (const header of this.responseHeaders ?? []) { |
| if (header.name.toLowerCase() === 'content-type') { |
| return Platform.MimeType.parseContentType(header.value); |
| } |
| } |
| |
| const mimeType = this.networkRequest?.mimeType ?? null; |
| const charset = this.networkRequest?.charset() ?? null; |
| return {mimeType, charset}; |
| } |
| } |
| |
| /** |
| * Helper class to match #requests created from requestWillBeSent with |
| * requestWillBeSentExtraInfo and responseReceivedExtraInfo when they have the |
| * same requestId due to redirects. |
| */ |
| class ExtraInfoBuilder { |
| readonly #requests: NetworkRequest[] = []; |
| #responseExtraInfoFlag: Array<boolean|null> = []; |
| #requestExtraInfos: Array<ExtraRequestInfo|null> = []; |
| #responseExtraInfos: Array<ExtraResponseInfo|null> = []; |
| #responseEarlyHintsHeaders: NameValue[] = []; |
| #finished = false; |
| |
| addRequest(req: NetworkRequest): void { |
| this.#requests.push(req); |
| this.sync(this.#requests.length - 1); |
| } |
| |
| addHasExtraInfo(hasExtraInfo: boolean): void { |
| this.#responseExtraInfoFlag.push(hasExtraInfo); |
| // This comes in response, so it can't come before request or after next |
| // request in the redirect chain. |
| console.assert(this.#requests.length === this.#responseExtraInfoFlag.length, 'request/response count mismatch'); |
| if (!hasExtraInfo) { |
| // We may potentially have gotten extra infos from the next redirect |
| // request already. Account for that by inserting null for missing |
| // extra infos at current position. |
| this.#requestExtraInfos.splice(this.#requests.length - 1, 0, null); |
| this.#responseExtraInfos.splice(this.#requests.length - 1, 0, null); |
| } |
| this.sync(this.#requests.length - 1); |
| } |
| |
| addRequestExtraInfo(info: ExtraRequestInfo): void { |
| this.#requestExtraInfos.push(info); |
| this.sync(this.#requestExtraInfos.length - 1); |
| } |
| |
| addResponseExtraInfo(info: ExtraResponseInfo): void { |
| this.#responseExtraInfos.push(info); |
| this.sync(this.#responseExtraInfos.length - 1); |
| } |
| |
| setEarlyHintsHeaders(earlyHintsHeaders: NameValue[]): void { |
| this.#responseEarlyHintsHeaders = earlyHintsHeaders; |
| this.updateFinalRequest(); |
| } |
| |
| finished(): void { |
| this.#finished = true; |
| // We may have missed responseReceived event in case of failure. |
| // That said, the ExtraInfo events still may be here, so mark them |
| // as present. Event if they are not, this is harmless. |
| // TODO(caseq): consider if we need to report hasExtraInfo in the |
| // loadingFailed event. |
| if (this.#responseExtraInfoFlag.length < this.#requests.length) { |
| this.#responseExtraInfoFlag.push(true); |
| this.sync(this.#responseExtraInfoFlag.length - 1); |
| } |
| console.assert( |
| this.#requests.length === this.#responseExtraInfoFlag.length, |
| 'request/response count mismatch when request finished'); |
| this.updateFinalRequest(); |
| } |
| |
| isFinished(): boolean { |
| return this.#finished; |
| } |
| |
| private sync(index: number): void { |
| const req = this.#requests[index]; |
| if (!req) { |
| return; |
| } |
| |
| // No response yet, so we don't know if extra info would |
| // be there, bail out for now. |
| if (index >= this.#responseExtraInfoFlag.length) { |
| return; |
| } |
| if (!this.#responseExtraInfoFlag[index]) { |
| return; |
| } |
| |
| const requestExtraInfo = this.#requestExtraInfos[index]; |
| if (requestExtraInfo) { |
| req.addExtraRequestInfo(requestExtraInfo); |
| this.#requestExtraInfos[index] = null; |
| } |
| |
| const responseExtraInfo = this.#responseExtraInfos[index]; |
| if (responseExtraInfo) { |
| req.addExtraResponseInfo(responseExtraInfo); |
| this.#responseExtraInfos[index] = null; |
| } |
| } |
| |
| finalRequest(): NetworkRequest|null { |
| if (!this.#finished) { |
| return null; |
| } |
| return this.#requests[this.#requests.length - 1] || null; |
| } |
| |
| private updateFinalRequest(): void { |
| if (!this.#finished) { |
| return; |
| } |
| const finalRequest = this.finalRequest(); |
| finalRequest?.setEarlyHintsHeaders(this.#responseEarlyHintsHeaders); |
| } |
| } |
| |
| SDKModel.register(NetworkManager, {capabilities: Capability.NETWORK, autostart: true}); |
| |
| export function networkConditionsEqual(first: ThrottlingConditions, second: ThrottlingConditions): boolean { |
| if ('block' in first || 'block' in second) { |
| if ('block' in first && 'block' in second) { |
| const firstTitle = (typeof first.title === 'function' ? first.title() : first.title); |
| const secondTitle = (typeof second.title === 'function' ? second.title() : second.title); |
| return firstTitle === secondTitle && first.block === second.block; |
| } |
| return false; |
| } |
| // Caution: titles might be different function instances, which produce |
| // the same value. |
| // We prefer to use the i18nTitleKey to prevent against locale changes or |
| // UIString changes that might change the value vs what the user has stored |
| // locally. |
| const firstTitle = first.i18nTitleKey || (typeof first.title === 'function' ? first.title() : first.title); |
| const secondTitle = second.i18nTitleKey || (typeof second.title === 'function' ? second.title() : second.title); |
| |
| return second.download === first.download && second.upload === first.upload && second.latency === first.latency && |
| first.packetLoss === second.packetLoss && first.packetQueueLength === second.packetQueueLength && |
| first.packetReordering === second.packetReordering && secondTitle === firstTitle; |
| } |
| |
| /** |
| * IMPORTANT: this key is used as the value that is persisted so we remember |
| * the user's throttling settings |
| * |
| * This means that it is very important that; |
| * 1. Each Conditions that is defined must have a unique key. |
| * 2. The keys & values DO NOT CHANGE for a particular condition, else we might break |
| * DevTools when restoring a user's persisted setting. |
| * |
| * If you do want to change them, you need to handle that in a migration, but |
| * please talk to jacktfranklin@ first. |
| */ |
| export const enum PredefinedThrottlingConditionKey { |
| BLOCKING = 'BLOCKING', |
| NO_THROTTLING = 'NO_THROTTLING', |
| OFFLINE = 'OFFLINE', |
| SPEED_3G = 'SPEED_3G', |
| SPEED_SLOW_4G = 'SPEED_SLOW_4G', |
| SPEED_FAST_4G = 'SPEED_FAST_4G', |
| } |
| |
| export type UserDefinedThrottlingConditionKey = `USER_CUSTOM_SETTING_${number}`; |
| export type ThrottlingConditionKey = PredefinedThrottlingConditionKey|UserDefinedThrottlingConditionKey; |
| |
| export const THROTTLING_CONDITIONS_LOOKUP: ReadonlyMap<PredefinedThrottlingConditionKey, Conditions> = new Map([ |
| [PredefinedThrottlingConditionKey.NO_THROTTLING, NoThrottlingConditions], |
| [PredefinedThrottlingConditionKey.OFFLINE, OfflineConditions], |
| [PredefinedThrottlingConditionKey.SPEED_3G, Slow3GConditions], |
| [PredefinedThrottlingConditionKey.SPEED_SLOW_4G, Slow4GConditions], |
| [PredefinedThrottlingConditionKey.SPEED_FAST_4G, Fast4GConditions] |
| ]); |
| |
| function keyIsPredefined(key: ThrottlingConditionKey): key is PredefinedThrottlingConditionKey { |
| return !key.startsWith('USER_CUSTOM_SETTING_'); |
| } |
| export function keyIsCustomUser(key: ThrottlingConditionKey): key is UserDefinedThrottlingConditionKey { |
| return key.startsWith('USER_CUSTOM_SETTING_'); |
| } |
| |
| export function getPredefinedCondition(key: ThrottlingConditionKey): Conditions|null { |
| if (!keyIsPredefined(key)) { |
| return null; |
| } |
| return THROTTLING_CONDITIONS_LOOKUP.get(key) ?? null; |
| } |
| |
| export function getPredefinedOrBlockingCondition(key: ThrottlingConditionKey): ThrottlingConditions|null { |
| return key === PredefinedThrottlingConditionKey.BLOCKING ? BlockingConditions : getPredefinedCondition(key); |
| } |
| |
| export type ThrottlingConditions = Conditions|{ |
| readonly key: ThrottlingConditionKey, |
| block: true, |
| title: string | (() => string), |
| }; |
| export interface Conditions { |
| readonly key: ThrottlingConditionKey; |
| download: number; |
| upload: number; |
| latency: number; |
| packetLoss?: number; |
| packetQueueLength?: number; |
| packetReordering?: boolean; |
| // TODO(crbug.com/422682525): make this just a function because we use lazy string everywhere. |
| title: string|(() => string); |
| // Instances may be serialized to local storage, so localized titles |
| // should not be irrecoverably baked, just in case the string changes |
| // (or the user switches locales). |
| // TODO(crbug.com/422682525): get rid of this, there is no need to store on |
| // the condition now we do not rely on it to reload a setting from disk. |
| i18nTitleKey?: string; |
| /** |
| * RTT values are multiplied by adjustment factors to make DevTools' emulation more accurate. |
| * This value represents the RTT value *before* the adjustment factor is applied. |
| * @see https://docs.google.com/document/d/10lfVdS1iDWCRKQXPfbxEn4Or99D64mvNlugP1AQuFlE/edit for historical context. |
| */ |
| targetLatency?: number; |
| } |
| |
| export interface Message { |
| message: string; |
| requestId: string; |
| warning: boolean; |
| } |
| |
| export interface InterceptionPattern { |
| urlPattern: string; |
| requestStage: Protocol.Fetch.RequestStage; |
| } |
| |
| export type RequestInterceptor = (request: InterceptedRequest) => Promise<void>; |
| |
| export interface RequestUpdateDroppedEventData { |
| url: Platform.DevToolsPath.UrlString; |
| frameId: Protocol.Page.FrameId|null; |
| loaderId: Protocol.Network.LoaderId; |
| resourceType: Protocol.Network.ResourceType; |
| mimeType: string; |
| lastModified: Date|null; |
| } |
| |
| /** |
| * For the given Round Trip Time (in MilliSeconds), return the best throttling conditions. |
| */ |
| export function getRecommendedNetworkPreset(rtt: number): Conditions|null { |
| const RTT_COMPARISON_THRESHOLD = 200; |
| const RTT_MINIMUM = 60; |
| |
| if (!Number.isFinite(rtt)) { |
| return null; |
| } |
| |
| if (rtt < RTT_MINIMUM) { |
| return null; |
| } |
| |
| // We pick from the set of presets in the panel but do not want to allow |
| // the "No Throttling" option to be picked. |
| const presets = THROTTLING_CONDITIONS_LOOKUP.values() |
| .filter(condition => { |
| return condition !== NoThrottlingConditions; |
| }) |
| .toArray(); |
| |
| let closestPreset: Conditions|null = null; |
| let smallestDiff = Infinity; |
| for (const preset of presets) { |
| const {targetLatency} = preset; |
| if (!targetLatency) { |
| continue; |
| } |
| |
| const diff = Math.abs(targetLatency - rtt); |
| if (diff > RTT_COMPARISON_THRESHOLD) { |
| continue; |
| } |
| |
| if (smallestDiff < diff) { |
| continue; |
| } |
| |
| closestPreset = preset; |
| smallestDiff = diff; |
| } |
| |
| return closestPreset; |
| } |