UNPKG

chrome-devtools-frontend

Version:
1,333 lines (1,141 loc) • 70.5 kB
// Copyright 2021 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* * Copyright (C) 2012 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the #name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import * 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 {Attributes, type Cookie} from './Cookie.js'; import {CookieParser} from './CookieParser.js'; import {NetworkManager, Events as NetworkManagerEvents} from './NetworkManager.js'; import {Type} from './Target.js'; import {ServerTiming} from './ServerTiming.js'; // clang-format off const UIStrings = { /** *@description Text in Network Request */ binary: '(binary)', /** *@description Tooltip to explain why a cookie was blocked */ secureOnly: 'This cookie was blocked because it had the "`Secure`" attribute and the connection was not secure.', /** *@description Tooltip to explain why a cookie was blocked */ notOnPath: 'This cookie was blocked because its path was not an exact match for or a superdirectory of the request url\'s path.', /** *@description Tooltip to explain why a cookie was blocked */ domainMismatch: 'This cookie was blocked because neither did the request URL\'s domain exactly match the cookie\'s domain, nor was the request URL\'s domain a subdomain of the cookie\'s Domain attribute value.', /** *@description Tooltip to explain why a cookie was blocked */ sameSiteStrict: 'This cookie was blocked because it had the "`SameSite=Strict`" attribute and the request was made from a different site. This includes top-level navigation requests initiated by other sites.', /** *@description Tooltip to explain why a cookie was blocked */ sameSiteLax: 'This cookie was blocked because it had the "`SameSite=Lax`" attribute and the request was made from a different site and was not initiated by a top-level navigation.', /** *@description Tooltip to explain why a cookie was blocked */ sameSiteUnspecifiedTreatedAsLax: 'This cookie didn\'t specify a "`SameSite`" attribute when it was stored and was defaulted to "SameSite=Lax," and was blocked because the request was made from a different site and was not initiated by a top-level navigation. The cookie had to have been set with "`SameSite=None`" to enable cross-site usage.', /** *@description Tooltip to explain why a cookie was blocked */ sameSiteNoneInsecure: 'This cookie was blocked because it had the "`SameSite=None`" attribute but was not marked "Secure". Cookies without SameSite restrictions must be marked "Secure" and sent over a secure connection.', /** *@description Tooltip to explain why a cookie was blocked */ userPreferences: 'This cookie was blocked due to user preferences.', /** *@description Tooltip to explain why a cookie was blocked */ unknownError: 'An unknown error was encountered when trying to send this cookie.', /** *@description Tooltip to explain why a cookie was blocked due to Schemeful Same-Site */ schemefulSameSiteStrict: 'This cookie was blocked because it had the "`SameSite=Strict`" attribute but the request was cross-site. This includes top-level navigation requests initiated by other sites. This request is considered cross-site because the URL has a different scheme than the current site.', /** *@description Tooltip to explain why a cookie was blocked due to Schemeful Same-Site */ schemefulSameSiteLax: 'This cookie was blocked because it had the "`SameSite=Lax`" attribute but the request was cross-site and was not initiated by a top-level navigation. This request is considered cross-site because the URL has a different scheme than the current site.', /** *@description Tooltip to explain why a cookie was blocked due to Schemeful Same-Site */ schemefulSameSiteUnspecifiedTreatedAsLax: 'This cookie didn\'t specify a "`SameSite`" attribute when it was stored, was defaulted to "`SameSite=Lax"`, and was blocked because the request was cross-site and was not initiated by a top-level navigation. This request is considered cross-site because the URL has a different scheme than the current site.', /** *@description Tooltip to explain why a cookie was blocked due to SameParty */ samePartyFromCrossPartyContext: 'This cookie was blocked because it had the "`SameParty`" attribute but the request was cross-party. The request was considered cross-party because the domain of the resource\'s URL and the domains of the resource\'s enclosing frames/documents are neither owners nor members in the same First-Party Set.', /** *@description Tooltip to explain why a cookie was blocked due to exceeding the maximum size */ nameValuePairExceedsMaxSize: 'This cookie was blocked because it was too large. The combined size of the name and value must be less than or equal to 4096 characters.', /** *@description Tooltip to explain why an attempt to set a cookie via `Set-Cookie` HTTP header on a request's response was blocked. */ thisSetcookieWasBlockedDueToUser: 'This attempt to set a cookie via a `Set-Cookie` header was blocked due to user preferences.', /** *@description Tooltip to explain why an attempt to set a cookie via `Set-Cookie` HTTP header on a request's response was blocked. */ thisSetcookieHadInvalidSyntax: 'This `Set-Cookie` header had invalid syntax.', /** *@description Tooltip to explain why a cookie was blocked */ theSchemeOfThisConnectionIsNot: 'The scheme of this connection is not allowed to store cookies.', /** *@description Tooltip to explain why a cookie was blocked */ anUnknownErrorWasEncounteredWhenTrying: 'An unknown error was encountered when trying to store this cookie.', /** *@description Tooltip to explain why a cookie was blocked due to Schemeful Same-Site *@example {SameSite=Strict} PH1 */ thisSetcookieWasBlockedBecauseItHadTheSamesiteStrictLax: 'This attempt to set a cookie via a `Set-Cookie` header was blocked because it had the "{PH1}" attribute but came from a cross-site response which was not the response to a top-level navigation. This response is considered cross-site because the URL has a different scheme than the current site.', /** *@description Tooltip to explain why a cookie was blocked due to Schemeful Same-Site */ thisSetcookieDidntSpecifyASamesite: 'This `Set-Cookie` header didn\'t specify a "`SameSite`" attribute, was defaulted to "`SameSite=Lax"`, and was blocked because it came from a cross-site response which was not the response to a top-level navigation. This response is considered cross-site because the URL has a different scheme than the current site.', /** *@description Tooltip to explain why a cookie was blocked due to SameParty */ thisSetcookieWasBlockedBecauseItHadTheSameparty: 'This attempt to set a cookie via a `Set-Cookie` header was blocked because it had the "`SameParty`" attribute but the request was cross-party. The request was considered cross-party because the domain of the resource\'s URL and the domains of the resource\'s enclosing frames/documents are neither owners nor members in the same First-Party Set.', /** *@description Tooltip to explain why a cookie was blocked due to SameParty */ thisSetcookieWasBlockedBecauseItHadTheSamepartyAttribute: 'This attempt to set a cookie via a `Set-Cookie` header was blocked because it had the "`SameParty`" attribute but also had other conflicting attributes. Chrome requires cookies that use the "`SameParty`" attribute to also have the "Secure" attribute, and to not be restricted to "`SameSite=Strict`".', /** *@description Tooltip to explain why an attempt to set a cookie via a `Set-Cookie` HTTP header on a request's response was blocked. */ blockedReasonSecureOnly: 'This attempt to set a cookie via a `Set-Cookie` header was blocked because it had the "Secure" attribute but was not received over a secure connection.', /** *@description Tooltip to explain why an attempt to set a cookie via a `Set-Cookie` HTTP header on a request's response was blocked. *@example {SameSite=Strict} PH1 */ blockedReasonSameSiteStrictLax: 'This attempt to set a cookie via a `Set-Cookie` header was blocked because it had the "{PH1}" attribute but came from a cross-site response which was not the response to a top-level navigation.', /** *@description Tooltip to explain why an attempt to set a cookie via a `Set-Cookie` HTTP header on a request's response was blocked. */ blockedReasonSameSiteUnspecifiedTreatedAsLax: 'This `Set-Cookie` header didn\'t specify a "`SameSite`" attribute and was defaulted to "`SameSite=Lax,`" and was blocked because it came from a cross-site response which was not the response to a top-level navigation. The `Set-Cookie` had to have been set with "`SameSite=None`" to enable cross-site usage.', /** *@description Tooltip to explain why an attempt to set a cookie via a `Set-Cookie` HTTP header on a request's response was blocked. */ blockedReasonSameSiteNoneInsecure: 'This attempt to set a cookie via a `Set-Cookie` header was blocked because it had the "`SameSite=None`" attribute but did not have the "Secure" attribute, which is required in order to use "`SameSite=None`".', /** *@description Tooltip to explain why an attempt to set a cookie via a `Set-Cookie` HTTP header on a request's response was blocked. */ blockedReasonOverwriteSecure: 'This attempt to set a cookie via a `Set-Cookie` header was blocked because it was not sent over a secure connection and would have overwritten a cookie with the Secure attribute.', /** *@description Tooltip to explain why an attempt to set a cookie via a `Set-Cookie` HTTP header on a request's response was blocked. */ blockedReasonInvalidDomain: 'This attempt to set a cookie via a `Set-Cookie` header was blocked because its Domain attribute was invalid with regards to the current host url.', /** *@description Tooltip to explain why an attempt to set a cookie via a `Set-Cookie` HTTP header on a request's response was blocked. */ blockedReasonInvalidPrefix: 'This attempt to set a cookie via a `Set-Cookie` header was blocked because it used the "`__Secure-`" or "`__Host-`" prefix in its name and broke the additional rules applied to cookies with these prefixes as defined in `https://tools.ietf.org/html/draft-west-cookie-prefixes-05`.', /** *@description Tooltip to explain why a cookie was blocked when the size of the #name plus the size of the value exceeds the max size. */ thisSetcookieWasBlockedBecauseTheNameValuePairExceedsMaxSize: 'This attempt to set a cookie via a `Set-Cookie` header was blocked because the cookie was too large. The combined size of the name and value must be less than or equal to 4096 characters.', /** *@description Text in Network Manager *@example {https://example.com} PH1 */ setcookieHeaderIsIgnoredIn: 'Set-Cookie header is ignored in response from url: {PH1}. The combined size of the name and value must be less than or equal to 4096 characters.', }; // clang-format on const str_ = i18n.i18n.registerUIStrings('core/sdk/NetworkRequest.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum, @typescript-eslint/naming-convention export enum MIME_TYPE { HTML = 'text/html', XML = 'text/xml', PLAIN = 'text/plain', XHTML = 'application/xhtml+xml', SVG = 'image/svg+xml', CSS = 'text/css', XSL = 'text/xsl', VTT = 'text/vtt', PDF = 'application/pdf', EVENTSTREAM = 'text/event-stream', } export class NetworkRequest extends Common.ObjectWrapper.ObjectWrapper<EventTypes> implements TextUtils.ContentProvider.ContentProvider { #requestIdInternal: string; #backendRequestIdInternal?: Protocol.Network.RequestId; readonly #documentURLInternal: Platform.DevToolsPath.UrlString; readonly #frameIdInternal: Protocol.Page.FrameId|null; readonly #loaderIdInternal: Protocol.Network.LoaderId|null; readonly #initiatorInternal: Protocol.Network.Initiator|null|undefined; readonly #hasUserGesture: boolean|undefined; #redirectSourceInternal: NetworkRequest|null; #preflightRequestInternal: NetworkRequest|null; #preflightInitiatorRequestInternal: NetworkRequest|null; #isRedirectInternal: boolean; #redirectDestinationInternal: NetworkRequest|null; #issueTimeInternal: number; #startTimeInternal: number; #endTimeInternal: number; #blockedReasonInternal: Protocol.Network.BlockedReason|undefined; #corsErrorStatusInternal: Protocol.Network.CorsErrorStatus|undefined; statusCode: number; statusText: string; requestMethod: string; requestTime: number; protocol: string; alternateProtocolUsage: Protocol.Network.AlternateProtocolUsage|undefined; mixedContentType: Protocol.Security.MixedContentType; #initialPriorityInternal: Protocol.Network.ResourcePriority|null; #currentPriority: Protocol.Network.ResourcePriority|null; #signedExchangeInfoInternal: Protocol.Network.SignedExchangeInfo|null; #webBundleInfoInternal: WebBundleInfo|null; #webBundleInnerRequestInfoInternal: WebBundleInnerRequestInfo|null; #resourceTypeInternal: Common.ResourceType.ResourceType; #contentDataInternal: Promise<ContentData>|null; readonly #framesInternal: WebSocketFrame[]; readonly #eventSourceMessagesInternal: EventSourceMessage[]; #responseHeaderValues: { [x: string]: string|undefined, }; #responseHeadersTextInternal: string; #originalResponseHeaders: Protocol.Fetch.HeaderEntry[]; #sortedOriginalResponseHeaders?: NameValue[]; // This field is only used when intercepting and overriding requests, because // in that case 'this.responseHeaders' does not contain 'set-cookie' headers. #setCookieHeaders: Protocol.Fetch.HeaderEntry[]; #requestHeadersInternal: NameValue[]; #requestHeaderValues: { [x: string]: string|undefined, }; #remoteAddressInternal: string; #remoteAddressSpaceInternal: Protocol.Network.IPAddressSpace; #referrerPolicyInternal: Protocol.Network.RequestReferrerPolicy|null; #securityStateInternal: Protocol.Security.SecurityState; #securityDetailsInternal: Protocol.Network.SecurityDetails|null; connectionId: string; connectionReused: boolean; hasNetworkData: boolean; #formParametersPromise: Promise<NameValue[]|null>|null; #requestFormDataPromise: Promise<string|null>|null; #hasExtraRequestInfoInternal: boolean; #hasExtraResponseInfoInternal: boolean; #blockedRequestCookiesInternal: BlockedCookieWithReason[]; #includedRequestCookiesInternal: Cookie[]; #blockedResponseCookiesInternal: BlockedSetCookieWithReason[]; #responseCookiesPartitionKey: string|null; #responseCookiesPartitionKeyOpaque: boolean|null; #siteHasCookieInOtherPartition: boolean; localizedFailDescription: string|null; #urlInternal!: Platform.DevToolsPath.UrlString; #responseReceivedTimeInternal!: number; #transferSizeInternal!: number; #finishedInternal!: boolean; #failedInternal!: boolean; #canceledInternal!: boolean; #preservedInternal!: boolean; #mimeTypeInternal!: MIME_TYPE; #parsedURLInternal!: Common.ParsedURL.ParsedURL; #nameInternal!: string|undefined; #pathInternal!: string|undefined; #clientSecurityStateInternal!: Protocol.Network.ClientSecurityState|undefined; #trustTokenParamsInternal!: Protocol.Network.TrustTokenParams|undefined; #trustTokenOperationDoneEventInternal!: Protocol.Network.TrustTokenOperationDoneEvent|undefined; #responseCacheStorageCacheName?: string; #serviceWorkerResponseSourceInternal?: Protocol.Network.ServiceWorkerResponseSource; #wallIssueTime?: number; #responseRetrievalTime?: Date; #resourceSizeInternal?: number; #fromMemoryCache?: boolean; #fromDiskCache?: boolean; #fromPrefetchCacheInternal?: boolean; #fetchedViaServiceWorkerInternal?: boolean; #timingInternal?: Protocol.Network.ResourceTiming; #requestHeadersTextInternal?: string; #responseHeadersInternal?: NameValue[]; #sortedResponseHeadersInternal?: NameValue[]; #responseCookiesInternal?: Cookie[]; #serverTimingsInternal?: ServerTiming[]|null; #queryStringInternal?: string|null; #parsedQueryParameters?: NameValue[]; #contentDataProvider?: (() => Promise<ContentData>); #isSameSiteInternal: boolean|null; #wasIntercepted: boolean; #associatedData = new Map<string, object>(); private constructor( requestId: string, backendRequestId: Protocol.Network.RequestId|undefined, url: Platform.DevToolsPath.UrlString, documentURL: Platform.DevToolsPath.UrlString, frameId: Protocol.Page.FrameId|null, loaderId: Protocol.Network.LoaderId|null, initiator: Protocol.Network.Initiator|null, hasUserGesture?: boolean) { super(); this.#requestIdInternal = requestId; this.#backendRequestIdInternal = backendRequestId; this.setUrl(url); this.#documentURLInternal = documentURL; this.#frameIdInternal = frameId; this.#loaderIdInternal = loaderId; this.#initiatorInternal = initiator; this.#hasUserGesture = hasUserGesture; this.#redirectSourceInternal = null; this.#preflightRequestInternal = null; this.#preflightInitiatorRequestInternal = null; this.#isRedirectInternal = false; this.#redirectDestinationInternal = null; this.#issueTimeInternal = -1; this.#startTimeInternal = -1; this.#endTimeInternal = -1; this.#blockedReasonInternal = undefined; this.#corsErrorStatusInternal = undefined; this.statusCode = 0; this.statusText = ''; this.requestMethod = ''; this.requestTime = 0; this.protocol = ''; this.alternateProtocolUsage = undefined; this.mixedContentType = Protocol.Security.MixedContentType.None; this.#initialPriorityInternal = null; this.#currentPriority = null; this.#signedExchangeInfoInternal = null; this.#webBundleInfoInternal = null; this.#webBundleInnerRequestInfoInternal = null; this.#resourceTypeInternal = Common.ResourceType.resourceTypes.Other; this.#contentDataInternal = null; this.#framesInternal = []; this.#eventSourceMessagesInternal = []; this.#responseHeaderValues = {}; this.#responseHeadersTextInternal = ''; this.#originalResponseHeaders = []; this.#setCookieHeaders = []; this.#requestHeadersInternal = []; this.#requestHeaderValues = {}; this.#remoteAddressInternal = ''; this.#remoteAddressSpaceInternal = Protocol.Network.IPAddressSpace.Unknown; this.#referrerPolicyInternal = null; this.#securityStateInternal = Protocol.Security.SecurityState.Unknown; this.#securityDetailsInternal = null; this.connectionId = '0'; this.connectionReused = false; this.hasNetworkData = false; this.#formParametersPromise = null; this.#requestFormDataPromise = (Promise.resolve(null) as Promise<string|null>| null); this.#hasExtraRequestInfoInternal = false; this.#hasExtraResponseInfoInternal = false; this.#blockedRequestCookiesInternal = []; this.#includedRequestCookiesInternal = []; this.#blockedResponseCookiesInternal = []; this.#siteHasCookieInOtherPartition = false; this.#responseCookiesPartitionKey = null; this.#responseCookiesPartitionKeyOpaque = null; this.localizedFailDescription = null; this.#isSameSiteInternal = null; this.#wasIntercepted = false; } static create( backendRequestId: Protocol.Network.RequestId, url: Platform.DevToolsPath.UrlString, documentURL: Platform.DevToolsPath.UrlString, frameId: Protocol.Page.FrameId|null, loaderId: Protocol.Network.LoaderId|null, initiator: Protocol.Network.Initiator|null, hasUserGesture?: boolean): NetworkRequest { return new NetworkRequest( backendRequestId, backendRequestId, url, documentURL, frameId, loaderId, initiator, hasUserGesture); } static createForWebSocket( backendRequestId: Protocol.Network.RequestId, requestURL: Platform.DevToolsPath.UrlString, initiator?: Protocol.Network.Initiator): NetworkRequest { return new NetworkRequest( backendRequestId, backendRequestId, requestURL, Platform.DevToolsPath.EmptyUrlString, null, null, initiator || null); } static createWithoutBackendRequest( requestId: string, url: Platform.DevToolsPath.UrlString, documentURL: Platform.DevToolsPath.UrlString, initiator: Protocol.Network.Initiator|null): NetworkRequest { return new NetworkRequest(requestId, undefined, url, documentURL, null, null, initiator); } identityCompare(other: NetworkRequest): number { const thisId = this.requestId(); const thatId = other.requestId(); if (thisId > thatId) { return 1; } if (thisId < thatId) { return -1; } return 0; } requestId(): string { return this.#requestIdInternal; } backendRequestId(): Protocol.Network.RequestId|undefined { return this.#backendRequestIdInternal; } url(): Platform.DevToolsPath.UrlString { return this.#urlInternal; } isBlobRequest(): boolean { return this.#urlInternal.startsWith('blob:'); } setUrl(x: Platform.DevToolsPath.UrlString): void { if (this.#urlInternal === x) { return; } this.#urlInternal = x; this.#parsedURLInternal = new Common.ParsedURL.ParsedURL(x); this.#queryStringInternal = undefined; this.#parsedQueryParameters = undefined; this.#nameInternal = undefined; this.#pathInternal = undefined; } get documentURL(): Platform.DevToolsPath.UrlString { return this.#documentURLInternal; } get parsedURL(): Common.ParsedURL.ParsedURL { return this.#parsedURLInternal; } get frameId(): Protocol.Page.FrameId|null { return this.#frameIdInternal; } get loaderId(): Protocol.Network.LoaderId|null { return this.#loaderIdInternal; } setRemoteAddress(ip: string, port: number): void { this.#remoteAddressInternal = ip + ':' + port; this.dispatchEventToListeners(Events.RemoteAddressChanged, this); } remoteAddress(): string { return this.#remoteAddressInternal; } remoteAddressSpace(): Protocol.Network.IPAddressSpace { return this.#remoteAddressSpaceInternal; } /** * The cache #name of the CacheStorage from where the response is served via * the ServiceWorker. */ getResponseCacheStorageCacheName(): string|undefined { return this.#responseCacheStorageCacheName; } setResponseCacheStorageCacheName(x: string): void { this.#responseCacheStorageCacheName = x; } serviceWorkerResponseSource(): Protocol.Network.ServiceWorkerResponseSource|undefined { return this.#serviceWorkerResponseSourceInternal; } setServiceWorkerResponseSource(serviceWorkerResponseSource: Protocol.Network.ServiceWorkerResponseSource): void { this.#serviceWorkerResponseSourceInternal = serviceWorkerResponseSource; } setReferrerPolicy(referrerPolicy: Protocol.Network.RequestReferrerPolicy): void { this.#referrerPolicyInternal = referrerPolicy; } referrerPolicy(): Protocol.Network.RequestReferrerPolicy|null { return this.#referrerPolicyInternal; } securityState(): Protocol.Security.SecurityState { return this.#securityStateInternal; } setSecurityState(securityState: Protocol.Security.SecurityState): void { this.#securityStateInternal = securityState; } securityDetails(): Protocol.Network.SecurityDetails|null { return this.#securityDetailsInternal; } securityOrigin(): string { return this.#parsedURLInternal.securityOrigin(); } setSecurityDetails(securityDetails: Protocol.Network.SecurityDetails): void { this.#securityDetailsInternal = securityDetails; } get startTime(): number { return this.#startTimeInternal || -1; } setIssueTime(monotonicTime: number, wallTime: number): void { this.#issueTimeInternal = monotonicTime; this.#wallIssueTime = wallTime; this.#startTimeInternal = monotonicTime; } issueTime(): number { return this.#issueTimeInternal; } pseudoWallTime(monotonicTime: number): number { return this.#wallIssueTime ? this.#wallIssueTime - this.#issueTimeInternal + monotonicTime : monotonicTime; } get responseReceivedTime(): number { return this.#responseReceivedTimeInternal || -1; } set responseReceivedTime(x: number) { this.#responseReceivedTimeInternal = x; } /** * The time at which the returned response was generated. For cached * responses, this is the last time the cache entry was validated. */ getResponseRetrievalTime(): Date|undefined { return this.#responseRetrievalTime; } setResponseRetrievalTime(x: Date): void { this.#responseRetrievalTime = x; } get endTime(): number { return this.#endTimeInternal || -1; } set endTime(x: number) { if (this.timing && this.timing.requestTime) { // Check against accurate responseReceivedTime. this.#endTimeInternal = Math.max(x, this.responseReceivedTime); } else { // Prefer endTime since it might be from the network stack. this.#endTimeInternal = x; if (this.#responseReceivedTimeInternal > x) { this.#responseReceivedTimeInternal = x; } } this.dispatchEventToListeners(Events.TimingChanged, this); } get duration(): number { if (this.#endTimeInternal === -1 || this.#startTimeInternal === -1) { return -1; } return this.#endTimeInternal - this.#startTimeInternal; } get latency(): number { if (this.#responseReceivedTimeInternal === -1 || this.#startTimeInternal === -1) { return -1; } return this.#responseReceivedTimeInternal - this.#startTimeInternal; } get resourceSize(): number { return this.#resourceSizeInternal || 0; } set resourceSize(x: number) { this.#resourceSizeInternal = x; } get transferSize(): number { return this.#transferSizeInternal || 0; } increaseTransferSize(x: number): void { this.#transferSizeInternal = (this.#transferSizeInternal || 0) + x; } setTransferSize(x: number): void { this.#transferSizeInternal = x; } get finished(): boolean { return this.#finishedInternal; } set finished(x: boolean) { if (this.#finishedInternal === x) { return; } this.#finishedInternal = x; if (x) { this.dispatchEventToListeners(Events.FinishedLoading, this); } } get failed(): boolean { return this.#failedInternal; } set failed(x: boolean) { this.#failedInternal = x; } get canceled(): boolean { return this.#canceledInternal; } set canceled(x: boolean) { this.#canceledInternal = x; } get preserved(): boolean { return this.#preservedInternal; } set preserved(x: boolean) { this.#preservedInternal = x; } blockedReason(): Protocol.Network.BlockedReason|undefined { return this.#blockedReasonInternal; } setBlockedReason(reason: Protocol.Network.BlockedReason): void { this.#blockedReasonInternal = reason; } corsErrorStatus(): Protocol.Network.CorsErrorStatus|undefined { return this.#corsErrorStatusInternal; } setCorsErrorStatus(corsErrorStatus: Protocol.Network.CorsErrorStatus): void { this.#corsErrorStatusInternal = corsErrorStatus; } wasBlocked(): boolean { return Boolean(this.#blockedReasonInternal); } cached(): boolean { return (Boolean(this.#fromMemoryCache) || Boolean(this.#fromDiskCache)) && !this.#transferSizeInternal; } cachedInMemory(): boolean { return Boolean(this.#fromMemoryCache) && !this.#transferSizeInternal; } fromPrefetchCache(): boolean { return Boolean(this.#fromPrefetchCacheInternal); } setFromMemoryCache(): void { this.#fromMemoryCache = true; this.#timingInternal = undefined; } get fromDiskCache(): boolean|undefined { return this.#fromDiskCache; } setFromDiskCache(): void { this.#fromDiskCache = true; } setFromPrefetchCache(): void { this.#fromPrefetchCacheInternal = true; } /** * Returns true if the request was intercepted by a service worker and it * provided its own response. */ get fetchedViaServiceWorker(): boolean { return Boolean(this.#fetchedViaServiceWorkerInternal); } set fetchedViaServiceWorker(x: boolean) { this.#fetchedViaServiceWorkerInternal = x; } /** * Returns true if the request was sent by a service worker. */ initiatedByServiceWorker(): boolean { const networkManager = NetworkManager.forRequest(this); if (!networkManager) { return false; } return networkManager.target().type() === Type.ServiceWorker; } get timing(): Protocol.Network.ResourceTiming|undefined { return this.#timingInternal; } set timing(timingInfo: Protocol.Network.ResourceTiming|undefined) { if (!timingInfo || this.#fromMemoryCache) { return; } // Take startTime and responseReceivedTime from timing data for better accuracy. // Timing's requestTime is a baseline in seconds, rest of the numbers there are ticks in millis. this.#startTimeInternal = timingInfo.requestTime; const headersReceivedTime = timingInfo.requestTime + timingInfo.receiveHeadersEnd / 1000.0; if ((this.#responseReceivedTimeInternal || -1) < 0 || this.#responseReceivedTimeInternal > headersReceivedTime) { this.#responseReceivedTimeInternal = headersReceivedTime; } if (this.#startTimeInternal > this.#responseReceivedTimeInternal) { this.#responseReceivedTimeInternal = this.#startTimeInternal; } this.#timingInternal = timingInfo; this.dispatchEventToListeners(Events.TimingChanged, this); } private setConnectTimingFromExtraInfo(connectTiming: Protocol.Network.ConnectTiming): void { this.#startTimeInternal = connectTiming.requestTime; this.dispatchEventToListeners(Events.TimingChanged, this); } get mimeType(): MIME_TYPE { return this.#mimeTypeInternal; } set mimeType(x: MIME_TYPE) { this.#mimeTypeInternal = x; } get displayName(): string { return this.#parsedURLInternal.displayName; } name(): string { if (this.#nameInternal) { return this.#nameInternal; } this.parseNameAndPathFromURL(); return this.#nameInternal as string; } path(): string { if (this.#pathInternal) { return this.#pathInternal; } this.parseNameAndPathFromURL(); return this.#pathInternal as string; } private parseNameAndPathFromURL(): void { if (this.#parsedURLInternal.isDataURL()) { this.#nameInternal = this.#parsedURLInternal.dataURLDisplayName(); this.#pathInternal = ''; } else if (this.#parsedURLInternal.isBlobURL()) { this.#nameInternal = this.#parsedURLInternal.url; this.#pathInternal = ''; } else if (this.#parsedURLInternal.isAboutBlank()) { this.#nameInternal = this.#parsedURLInternal.url; this.#pathInternal = ''; } else { this.#pathInternal = this.#parsedURLInternal.host + this.#parsedURLInternal.folderPathComponents; const networkManager = NetworkManager.forRequest(this); const inspectedURL = networkManager ? Common.ParsedURL.ParsedURL.fromString(networkManager.target().inspectedURL()) : null; this.#pathInternal = Platform.StringUtilities.trimURL(this.#pathInternal, inspectedURL ? inspectedURL.host : ''); if (this.#parsedURLInternal.lastPathComponent || this.#parsedURLInternal.queryParams) { this.#nameInternal = this.#parsedURLInternal.lastPathComponent + (this.#parsedURLInternal.queryParams ? '?' + this.#parsedURLInternal.queryParams : ''); } else if (this.#parsedURLInternal.folderPathComponents) { this.#nameInternal = this.#parsedURLInternal.folderPathComponents.substring( this.#parsedURLInternal.folderPathComponents.lastIndexOf('/') + 1) + '/'; this.#pathInternal = this.#pathInternal.substring(0, this.#pathInternal.lastIndexOf('/')); } else { this.#nameInternal = this.#parsedURLInternal.host; this.#pathInternal = ''; } } } get folder(): string { let path: string = this.#parsedURLInternal.path; const indexOfQuery = path.indexOf('?'); if (indexOfQuery !== -1) { path = path.substring(0, indexOfQuery); } const lastSlashIndex = path.lastIndexOf('/'); return lastSlashIndex !== -1 ? path.substring(0, lastSlashIndex) : ''; } get pathname(): string { return this.#parsedURLInternal.path; } resourceType(): Common.ResourceType.ResourceType { return this.#resourceTypeInternal; } setResourceType(resourceType: Common.ResourceType.ResourceType): void { this.#resourceTypeInternal = resourceType; } get domain(): string { return this.#parsedURLInternal.host; } get scheme(): string { return this.#parsedURLInternal.scheme; } redirectSource(): NetworkRequest|null { return this.#redirectSourceInternal; } setRedirectSource(originatingRequest: NetworkRequest|null): void { this.#redirectSourceInternal = originatingRequest; } preflightRequest(): NetworkRequest|null { return this.#preflightRequestInternal; } setPreflightRequest(preflightRequest: NetworkRequest|null): void { this.#preflightRequestInternal = preflightRequest; } preflightInitiatorRequest(): NetworkRequest|null { return this.#preflightInitiatorRequestInternal; } setPreflightInitiatorRequest(preflightInitiatorRequest: NetworkRequest|null): void { this.#preflightInitiatorRequestInternal = preflightInitiatorRequest; } isPreflightRequest(): boolean { return this.#initiatorInternal !== null && this.#initiatorInternal !== undefined && this.#initiatorInternal.type === Protocol.Network.InitiatorType.Preflight; } redirectDestination(): NetworkRequest|null { return this.#redirectDestinationInternal; } setRedirectDestination(redirectDestination: NetworkRequest|null): void { this.#redirectDestinationInternal = redirectDestination; } requestHeaders(): NameValue[] { return this.#requestHeadersInternal; } setRequestHeaders(headers: NameValue[]): void { this.#requestHeadersInternal = headers; this.dispatchEventToListeners(Events.RequestHeadersChanged); } requestHeadersText(): string|undefined { return this.#requestHeadersTextInternal; } setRequestHeadersText(text: string): void { this.#requestHeadersTextInternal = text; this.dispatchEventToListeners(Events.RequestHeadersChanged); } requestHeaderValue(headerName: string): string|undefined { if (this.#requestHeaderValues[headerName]) { return this.#requestHeaderValues[headerName]; } this.#requestHeaderValues[headerName] = this.computeHeaderValue(this.requestHeaders(), headerName); return this.#requestHeaderValues[headerName]; } requestFormData(): Promise<string|null> { if (!this.#requestFormDataPromise) { this.#requestFormDataPromise = NetworkManager.requestPostData(this); } return this.#requestFormDataPromise; } setRequestFormData(hasData: boolean, data: string|null): void { this.#requestFormDataPromise = (hasData && data === null) ? null : Promise.resolve(data); this.#formParametersPromise = null; } private filteredProtocolName(): string { const protocol = this.protocol.toLowerCase(); if (protocol === 'h2') { return 'http/2.0'; } return protocol.replace(/^http\/2(\.0)?\+/, 'http/2.0+'); } requestHttpVersion(): string { const headersText = this.requestHeadersText(); if (!headersText) { const version = this.requestHeaderValue('version') || this.requestHeaderValue(':version'); if (version) { return version; } return this.filteredProtocolName(); } const firstLine = headersText.split(/\r\n/)[0]; const match = firstLine.match(/(HTTP\/\d+\.\d+)$/); return match ? match[1] : 'HTTP/0.9'; } get responseHeaders(): NameValue[] { return this.#responseHeadersInternal || []; } set responseHeaders(x: NameValue[]) { this.#responseHeadersInternal = x; this.#sortedResponseHeadersInternal = undefined; this.#serverTimingsInternal = undefined; this.#responseCookiesInternal = undefined; this.#responseHeaderValues = {}; this.dispatchEventToListeners(Events.ResponseHeadersChanged); } get originalResponseHeaders(): Protocol.Fetch.HeaderEntry[] { return this.#originalResponseHeaders; } set originalResponseHeaders(headers: Protocol.Fetch.HeaderEntry[]) { this.#originalResponseHeaders = headers; this.#sortedOriginalResponseHeaders = undefined; } get setCookieHeaders(): Protocol.Fetch.HeaderEntry[] { return this.#setCookieHeaders; } set setCookieHeaders(headers: Protocol.Fetch.HeaderEntry[]) { this.#setCookieHeaders = headers; } get responseHeadersText(): string { return this.#responseHeadersTextInternal; } set responseHeadersText(x: string) { this.#responseHeadersTextInternal = x; this.dispatchEventToListeners(Events.ResponseHeadersChanged); } get sortedResponseHeaders(): NameValue[] { if (this.#sortedResponseHeadersInternal !== undefined) { return this.#sortedResponseHeadersInternal; } this.#sortedResponseHeadersInternal = this.responseHeaders.slice(); return this.#sortedResponseHeadersInternal.sort(function(a, b) { return Platform.StringUtilities.compare(a.name.toLowerCase(), b.name.toLowerCase()) || Platform.StringUtilities.compare(a.value, b.value); }); } get sortedOriginalResponseHeaders(): NameValue[] { if (this.#sortedOriginalResponseHeaders !== undefined) { return this.#sortedOriginalResponseHeaders; } this.#sortedOriginalResponseHeaders = this.originalResponseHeaders.slice(); return this.#sortedOriginalResponseHeaders.sort(function(a, b) { return Platform.StringUtilities.compare(a.name.toLowerCase(), b.name.toLowerCase()) || Platform.StringUtilities.compare(a.value, b.value); }); } hasOverriddenHeaders(): boolean { if (!this.#originalResponseHeaders.length) { return false; } const sortedResponseHeaders = this.sortedResponseHeaders; const sortedOriginalResponseHeaders = this.sortedOriginalResponseHeaders; if (sortedOriginalResponseHeaders.length !== sortedResponseHeaders.length) { return true; } for (let i = 0; i < sortedResponseHeaders.length; i++) { if (sortedResponseHeaders[i].name.toLowerCase() !== sortedOriginalResponseHeaders[i].name.toLowerCase()) { return true; } if (sortedResponseHeaders[i].value !== sortedOriginalResponseHeaders[i].value) { return true; } } return false; } responseHeaderValue(headerName: string): string|undefined { if (headerName in this.#responseHeaderValues) { return this.#responseHeaderValues[headerName]; } this.#responseHeaderValues[headerName] = this.computeHeaderValue(this.responseHeaders, headerName); return this.#responseHeaderValues[headerName]; } wasIntercepted(): boolean { return this.#wasIntercepted; } setWasIntercepted(wasIntercepted: boolean): void { this.#wasIntercepted = wasIntercepted; } get responseCookies(): Cookie[] { if (!this.#responseCookiesInternal) { this.#responseCookiesInternal = CookieParser.parseSetCookie(this.responseHeaderValue('Set-Cookie'), this.domain) || []; if (this.#responseCookiesPartitionKey) { for (const cookie of this.#responseCookiesInternal) { cookie.setPartitionKey(this.#responseCookiesPartitionKey); } } else if (this.#responseCookiesPartitionKeyOpaque) { for (const cookie of this.#responseCookiesInternal) { cookie.setPartitionKeyOpaque(); } } } return this.#responseCookiesInternal; } responseLastModified(): string|undefined { return this.responseHeaderValue('last-modified'); } allCookiesIncludingBlockedOnes(): Cookie[] { return [ ...this.includedRequestCookies(), ...this.responseCookies, ...this.blockedRequestCookies().map(blockedRequestCookie => blockedRequestCookie.cookie), ...this.blockedResponseCookies().map(blockedResponseCookie => blockedResponseCookie.cookie), ].filter(v => Boolean(v)) as Cookie[]; } get serverTimings(): ServerTiming[]|null { if (typeof this.#serverTimingsInternal === 'undefined') { this.#serverTimingsInternal = ServerTiming.parseHeaders(this.responseHeaders); } return this.#serverTimingsInternal; } queryString(): string|null { if (this.#queryStringInternal !== undefined) { return this.#queryStringInternal; } let queryString: string|null = null; const url = this.url(); const questionMarkPosition = url.indexOf('?'); if (questionMarkPosition !== -1) { queryString = url.substring(questionMarkPosition + 1); const hashSignPosition = queryString.indexOf('#'); if (hashSignPosition !== -1) { queryString = queryString.substring(0, hashSignPosition); } } this.#queryStringInternal = queryString; return this.#queryStringInternal; } get queryParameters(): NameValue[]|null { if (this.#parsedQueryParameters) { return this.#parsedQueryParameters; } const queryString = this.queryString(); if (!queryString) { return null; } this.#parsedQueryParameters = this.parseParameters(queryString); return this.#parsedQueryParameters; } private async parseFormParameters(): Promise<NameValue[]|null> { const requestContentType = this.requestContentType(); if (!requestContentType) { return null; } // Handling application/#x-www-form-urlencoded request bodies. if (requestContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i)) { const formData = await this.requestFormData(); if (!formData) { return null; } return this.parseParameters(formData); } // Handling multipart/form-data request bodies. const multipartDetails = requestContentType.match(/^multipart\/form-data\s*;\s*boundary\s*=\s*(\S+)\s*$/); if (!multipartDetails) { return null; } const boundary = multipartDetails[1]; if (!boundary) { return null; } const formData = await this.requestFormData(); if (!formData) { return null; } return this.parseMultipartFormDataParameters(formData, boundary); } formParameters(): Promise<NameValue[]|null> { if (!this.#formParametersPromise) { this.#formParametersPromise = this.parseFormParameters(); } return this.#formParametersPromise; } responseHttpVersion(): string { const headersText = this.#responseHeadersTextInternal; if (!headersText) { const version = this.responseHeaderValue('version') || this.responseHeaderValue(':version'); if (version) { return version; } return this.filteredProtocolName(); } const firstLine = headersText.split(/\r\n/)[0]; const match = firstLine.match(/^(HTTP\/\d+\.\d+)/); return match ? match[1] : 'HTTP/0.9'; } private parseParameters(queryString: string): NameValue[] { function parseNameValue(pair: string): { name: string, value: string, } { const position = pair.indexOf('='); if (position === -1) { return {name: pair, value: ''}; } return {name: pair.substring(0, position), value: pair.substring(position + 1)}; } return queryString.split('&').map(parseNameValue); } /** * Parses multipart/form-data; boundary=boundaryString request bodies - * --boundaryString * Content-Disposition: form-data; #name="field-#name"; filename="r.gif" * Content-Type: application/octet-stream * * optionalValue * --boundaryString * Content-Disposition: form-data; #name="field-#name-2" * * optionalValue2 * --boundaryString-- */ private parseMultipartFormDataParameters(data: string, boundary: string): NameValue[] { const sanitizedBoundary = Platform.StringUtilities.escapeForRegExp(boundary); const keyValuePattern = new RegExp( // Header with an optional file #name. '^\\r\\ncontent-disposition\\s*:\\s*form-data\\s*;\\s*name="([^"]*)"(?:\\s*;\\s*filename="([^"]*)")?' + // Optional secondary header with the content type. '(?:\\r\\ncontent-type\\s*:\\s*([^\\r\\n]*))?' + // Padding. '\\r\\n\\r\\n' + // Value '(.*)' + // Padding. '\\r\\n$', 'is'); const fields = data.split(new RegExp(`--${sanitizedBoundary}(?:--\s*$)?`, 'g')); return fields.reduce(parseMultipartField, []); function parseMultipartField(result: NameValue[], field: string): NameValue[] { const [match, name, filename, contentType, value] = field.match(keyValuePattern) || []; if (!match) { return result; } const processedValue = (filename || contentType) ? i18nString(UIStrings.binary) : value; result.push({name, value: processedValue}); return result; } } private computeHeaderValue(headers: NameValue[], headerName: string): string|undefined { headerName = headerName.toLowerCase(); const values = []; for (let i = 0; i < headers.length; ++i) { if (headers[i].name.toLowerCase() === headerName) { values.push(headers[i].value); } } if (!values.length) { return undefined; } // Set-Cookie #values should be separated by '\n', not comma, otherwise cookies could not be parsed. if (headerName === 'set-cookie') { return values.join('\n'); } return values.join(', '); } contentData(): Promise<ContentData> { if (this.#contentDataInternal) { return this.#contentDataInternal; } if (this.#contentDataProvider) { this.#contentDataInternal = this.#contentDataProvider(); } else { this.#contentDataInternal = NetworkManager.requestContentData(this); } return this.#contentDataInternal; } setContentDataProvider(dataProvider: () => Promise<ContentData>): void { console.assert(!this.#contentDataInternal, 'contentData can only be set once.'); this.#contentDataProvider = dataProvider; } contentURL(): Platform.DevToolsPath.UrlString { return this.#urlInternal; } contentType(): Common.ResourceType.ResourceType { return this.#resourceTypeInternal; } async requestContent(): Promise<TextUtils.ContentProvider.DeferredContent> { const {content, error, encoded} = await this.contentData(); return { content, error, isEncoded: encoded, } as TextUtils.ContentProvider.DeferredContent; } async searchInContent(query: string, caseSensitive: boolean, isRegex: boolean): Promise<TextUtils.ContentProvider.SearchMatch[]> { if (!this.#contentDataProvider) { return NetworkManager.searchInRequest(this, query, caseSensitive, isRegex); } const contentData = await this.contentData(); let content: string|(string | null) = contentData.content; if (!content) { return []; } if (contentData.encoded) { content = window.atob(content); } return TextUtils.TextUtils.performSearchInContent(content, query, caseSensitive, isRegex); } isHttpFamily(): boolean { return Boolean(this.url().match(/^https?:/i)); } requestContentType(): string|undefined { return this.requestHeaderValue('Content-Type'); } hasErrorStatusCode(): boolean { return this.statusCode >= 400; } setInitialPriority(priority: Protocol.Network.ResourcePriority): void { this.#initialPriorityInternal = priority; } initialPriority(): Protocol.Network.ResourcePriority|null { return this.#initialPriorityInternal; } setPriority(priority: Protocol.Network.ResourcePriority): void { this.#currentPriority = priority; } priority(): Protocol.Network.ResourcePriority|null { return this.#currentPriority || this.#initialPriorityInternal || null; } setSignedExchang