1337 lines
50 KiB
JavaScript
1337 lines
50 KiB
JavaScript
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
* Copyright (C) 2011 Google Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. 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.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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.
|
|
*/
|
|
|
|
WI.Resource = class Resource extends WI.SourceCode
|
|
{
|
|
constructor(url, {mimeType, type, loaderIdentifier, targetId, requestIdentifier, requestMethod, requestHeaders, requestData, requestSentTimestamp, requestSentWalltime, initiatorCallFrames, initiatorSourceCodeLocation, initiatorNode} = {})
|
|
{
|
|
console.assert(url);
|
|
|
|
super(url);
|
|
|
|
if (type in WI.Resource.Type)
|
|
type = WI.Resource.Type[type];
|
|
else if (type === "Stylesheet") {
|
|
// COMPATIBILITY (iOS 13): Page.ResourceType.Stylesheet was renamed to Page.ResourceType.StyleSheet.
|
|
type = WI.Resource.Type.StyleSheet;
|
|
}
|
|
|
|
this._mimeType = mimeType;
|
|
this._mimeTypeComponents = null;
|
|
this._type = Resource.resolvedType(type, mimeType);
|
|
this._loaderIdentifier = loaderIdentifier || null;
|
|
this._requestIdentifier = requestIdentifier || null;
|
|
this._queryStringParameters = undefined;
|
|
this._requestFormParameters = undefined;
|
|
this._requestMethod = requestMethod || null;
|
|
this._requestData = requestData || null;
|
|
this._requestHeaders = requestHeaders || {};
|
|
this._responseHeaders = {};
|
|
this._requestCookies = null;
|
|
this._responseCookies = null;
|
|
this._serverTimingEntries = null;
|
|
this._parentFrame = null;
|
|
this._initiatorCallFrames = initiatorCallFrames || null;
|
|
this._initiatorSourceCodeLocation = initiatorSourceCodeLocation || null;
|
|
this._initiatorNode = initiatorNode || null;
|
|
this._initiatedResources = [];
|
|
this._requestSentTimestamp = requestSentTimestamp || NaN;
|
|
this._requestSentWalltime = requestSentWalltime || NaN;
|
|
this._responseReceivedTimestamp = NaN;
|
|
this._lastDataReceivedTimestamp = NaN;
|
|
this._finishedOrFailedTimestamp = NaN;
|
|
this._finishThenRequestContentPromise = null;
|
|
this._statusCode = NaN;
|
|
this._statusText = null;
|
|
this._cached = false;
|
|
this._canceled = false;
|
|
this._finished = false;
|
|
this._failed = false;
|
|
this._failureReasonText = null;
|
|
this._receivedNetworkLoadMetrics = false;
|
|
this._responseSource = WI.Resource.ResponseSource.Unknown;
|
|
this._security = null;
|
|
this._timingData = new WI.ResourceTimingData(this);
|
|
this._protocol = null;
|
|
this._priority = WI.Resource.NetworkPriority.Unknown;
|
|
this._remoteAddress = null;
|
|
this._connectionIdentifier = null;
|
|
this._target = targetId ? WI.targetManager.targetForIdentifier(targetId) : WI.mainTarget;
|
|
this._redirects = [];
|
|
|
|
// Exact sizes if loaded over the network or cache.
|
|
this._requestHeadersTransferSize = NaN;
|
|
this._requestBodyTransferSize = NaN;
|
|
this._responseHeadersTransferSize = NaN;
|
|
this._responseBodyTransferSize = NaN;
|
|
this._responseBodySize = NaN;
|
|
this._cachedResponseBodySize = NaN;
|
|
|
|
// Estimated sizes (if backend does not provide metrics).
|
|
this._estimatedSize = NaN;
|
|
this._estimatedTransferSize = NaN;
|
|
this._estimatedResponseHeadersSize = NaN;
|
|
|
|
if (this._initiatorSourceCodeLocation && this._initiatorSourceCodeLocation.sourceCode instanceof WI.Resource)
|
|
this._initiatorSourceCodeLocation.sourceCode.addInitiatedResource(this);
|
|
}
|
|
|
|
// Static
|
|
|
|
static resolvedType(type, mimeType)
|
|
{
|
|
if (type && type !== WI.Resource.Type.Other)
|
|
return type;
|
|
|
|
return Resource.typeFromMIMEType(mimeType);
|
|
}
|
|
|
|
static typeFromMIMEType(mimeType)
|
|
{
|
|
if (!mimeType)
|
|
return WI.Resource.Type.Other;
|
|
|
|
mimeType = parseMIMEType(mimeType).type;
|
|
|
|
if (mimeType in WI.Resource._mimeTypeMap)
|
|
return WI.Resource._mimeTypeMap[mimeType];
|
|
|
|
if (mimeType.startsWith("image/"))
|
|
return WI.Resource.Type.Image;
|
|
|
|
if (mimeType.startsWith("font/"))
|
|
return WI.Resource.Type.Font;
|
|
|
|
return WI.Resource.Type.Other;
|
|
}
|
|
|
|
static displayNameForType(type, plural)
|
|
{
|
|
switch (type) {
|
|
case WI.Resource.Type.Document:
|
|
if (plural)
|
|
return WI.UIString("Documents");
|
|
return WI.UIString("Document");
|
|
case WI.Resource.Type.StyleSheet:
|
|
if (plural)
|
|
return WI.UIString("Style Sheets");
|
|
return WI.UIString("Style Sheet");
|
|
case WI.Resource.Type.Image:
|
|
if (plural)
|
|
return WI.UIString("Images");
|
|
return WI.UIString("Image");
|
|
case WI.Resource.Type.Font:
|
|
if (plural)
|
|
return WI.UIString("Fonts");
|
|
return WI.UIString("Font");
|
|
case WI.Resource.Type.Script:
|
|
if (plural)
|
|
return WI.UIString("Scripts");
|
|
return WI.UIString("Script");
|
|
case WI.Resource.Type.XHR:
|
|
if (plural)
|
|
return WI.UIString("XHRs");
|
|
return WI.UIString("XHR");
|
|
case WI.Resource.Type.Fetch:
|
|
if (plural)
|
|
return WI.UIString("Fetches", "Resources loaded via 'fetch' method");
|
|
return WI.repeatedUIString.fetch();
|
|
case WI.Resource.Type.Ping:
|
|
if (plural)
|
|
return WI.UIString("Pings");
|
|
return WI.UIString("Ping");
|
|
case WI.Resource.Type.Beacon:
|
|
if (plural)
|
|
return WI.UIString("Beacons");
|
|
return WI.UIString("Beacon");
|
|
case WI.Resource.Type.WebSocket:
|
|
if (plural)
|
|
return WI.UIString("Sockets");
|
|
return WI.UIString("Socket");
|
|
case WI.Resource.Type.Other:
|
|
return WI.UIString("Other");
|
|
default:
|
|
console.error("Unknown resource type", type);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static classNamesForResource(resource)
|
|
{
|
|
let classes = [];
|
|
|
|
let isOverride = !!resource.localResourceOverride;
|
|
let wasOverridden = resource.responseSource === WI.Resource.ResponseSource.InspectorOverride;
|
|
let shouldBeOverridden = resource.isLoading() && WI.networkManager.localResourceOverridesForURL(resource.url).some((localResourceOverride) => !localResourceOverride.disabled);
|
|
if (isOverride || wasOverridden || shouldBeOverridden)
|
|
classes.push("override");
|
|
|
|
if (resource.type === WI.Resource.Type.Other) {
|
|
if (resource.requestedByteRange)
|
|
classes.push("resource-type-range");
|
|
} else
|
|
classes.push(resource.type);
|
|
|
|
return classes;
|
|
}
|
|
|
|
static displayNameForProtocol(protocol)
|
|
{
|
|
switch (protocol) {
|
|
case "h2":
|
|
return "HTTP/2";
|
|
case "http/1.0":
|
|
return "HTTP/1.0";
|
|
case "http/1.1":
|
|
return "HTTP/1.1";
|
|
case "spdy/2":
|
|
return "SPDY/2";
|
|
case "spdy/3":
|
|
return "SPDY/3";
|
|
case "spdy/3.1":
|
|
return "SPDY/3.1";
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static comparePriority(a, b)
|
|
{
|
|
console.assert(typeof a === "symbol");
|
|
console.assert(typeof b === "symbol");
|
|
|
|
const map = {
|
|
[WI.Resource.NetworkPriority.Unknown]: 0,
|
|
[WI.Resource.NetworkPriority.Low]: 1,
|
|
[WI.Resource.NetworkPriority.Medium]: 2,
|
|
[WI.Resource.NetworkPriority.High]: 3,
|
|
};
|
|
|
|
let aNum = map[a] || 0;
|
|
let bNum = map[b] || 0;
|
|
return aNum - bNum;
|
|
}
|
|
|
|
static displayNameForPriority(priority)
|
|
{
|
|
switch (priority) {
|
|
case WI.Resource.NetworkPriority.Low:
|
|
return WI.UIString("Low", "Low @ Network Priority", "Low network request priority");
|
|
case WI.Resource.NetworkPriority.Medium:
|
|
return WI.UIString("Medium", "Medium @ Network Priority", "Medium network request priority");
|
|
case WI.Resource.NetworkPriority.High:
|
|
return WI.UIString("High", "High @ Network Priority", "High network request priority");
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static responseSourceFromPayload(source)
|
|
{
|
|
if (!source)
|
|
return WI.Resource.ResponseSource.Unknown;
|
|
|
|
switch (source) {
|
|
case InspectorBackend.Enum.Network.ResponseSource.Unknown:
|
|
return WI.Resource.ResponseSource.Unknown;
|
|
case InspectorBackend.Enum.Network.ResponseSource.Network:
|
|
return WI.Resource.ResponseSource.Network;
|
|
case InspectorBackend.Enum.Network.ResponseSource.MemoryCache:
|
|
return WI.Resource.ResponseSource.MemoryCache;
|
|
case InspectorBackend.Enum.Network.ResponseSource.DiskCache:
|
|
return WI.Resource.ResponseSource.DiskCache;
|
|
case InspectorBackend.Enum.Network.ResponseSource.ServiceWorker:
|
|
return WI.Resource.ResponseSource.ServiceWorker;
|
|
case InspectorBackend.Enum.Network.ResponseSource.InspectorOverride:
|
|
return WI.Resource.ResponseSource.InspectorOverride;
|
|
default:
|
|
console.error("Unknown response source type", source);
|
|
return WI.Resource.ResponseSource.Unknown;
|
|
}
|
|
}
|
|
|
|
static networkPriorityFromPayload(priority)
|
|
{
|
|
switch (priority) {
|
|
case InspectorBackend.Enum.Network.MetricsPriority.Low:
|
|
return WI.Resource.NetworkPriority.Low;
|
|
case InspectorBackend.Enum.Network.MetricsPriority.Medium:
|
|
return WI.Resource.NetworkPriority.Medium;
|
|
case InspectorBackend.Enum.Network.MetricsPriority.High:
|
|
return WI.Resource.NetworkPriority.High;
|
|
default:
|
|
console.error("Unknown metrics priority", priority);
|
|
return WI.Resource.NetworkPriority.Unknown;
|
|
}
|
|
}
|
|
|
|
static connectionIdentifierFromPayload(connectionIdentifier)
|
|
{
|
|
// Map backend connection identifiers to an easier to read number.
|
|
if (!WI.Resource.connectionIdentifierMap) {
|
|
WI.Resource.connectionIdentifierMap = new Map;
|
|
WI.Resource.nextConnectionIdentifier = 1;
|
|
}
|
|
|
|
let id = WI.Resource.connectionIdentifierMap.get(connectionIdentifier);
|
|
if (id)
|
|
return id;
|
|
|
|
id = WI.Resource.nextConnectionIdentifier++;
|
|
WI.Resource.connectionIdentifierMap.set(connectionIdentifier, id);
|
|
return id;
|
|
}
|
|
|
|
// Public
|
|
|
|
get mimeType() { return this._mimeType; }
|
|
get target() { return this._target; }
|
|
get type() { return this._type; }
|
|
get loaderIdentifier() { return this._loaderIdentifier; }
|
|
get requestIdentifier() { return this._requestIdentifier; }
|
|
get requestMethod() { return this._requestMethod; }
|
|
get requestData() { return this._requestData; }
|
|
get initiatorCallFrames() { return this._initiatorCallFrames; }
|
|
get initiatorSourceCodeLocation() { return this._initiatorSourceCodeLocation; }
|
|
get initiatorNode() { return this._initiatorNode; }
|
|
get initiatedResources() { return this._initiatedResources; }
|
|
get statusCode() { return this._statusCode; }
|
|
get statusText() { return this._statusText; }
|
|
get responseSource() { return this._responseSource; }
|
|
get security() { return this._security; }
|
|
get timingData() { return this._timingData; }
|
|
get protocol() { return this._protocol; }
|
|
get priority() { return this._priority; }
|
|
get remoteAddress() { return this._remoteAddress; }
|
|
get connectionIdentifier() { return this._connectionIdentifier; }
|
|
get parentFrame() { return this._parentFrame; }
|
|
get finished() { return this._finished; }
|
|
get failed() { return this._failed; }
|
|
get canceled() { return this._canceled; }
|
|
get failureReasonText() { return this._failureReasonText; }
|
|
get requestHeaders() { return this._requestHeaders; }
|
|
get responseHeaders() { return this._responseHeaders; }
|
|
get requestSentTimestamp() { return this._requestSentTimestamp; }
|
|
get requestSentWalltime() { return this._requestSentWalltime; }
|
|
get responseReceivedTimestamp() { return this._responseReceivedTimestamp; }
|
|
get lastDataReceivedTimestamp() { return this._lastDataReceivedTimestamp; }
|
|
get finishedOrFailedTimestamp() { return this._finishedOrFailedTimestamp; }
|
|
get cached() { return this._cached; }
|
|
get requestHeadersTransferSize() { return this._requestHeadersTransferSize; }
|
|
get requestBodyTransferSize() { return this._requestBodyTransferSize; }
|
|
get responseHeadersTransferSize() { return this._responseHeadersTransferSize; }
|
|
get responseBodyTransferSize() { return this._responseBodyTransferSize; }
|
|
get cachedResponseBodySize() { return this._cachedResponseBodySize; }
|
|
get redirects() { return this._redirects; }
|
|
|
|
get loadedSecurely()
|
|
{
|
|
if (this.urlComponents.scheme !== "https" && this.urlComponents.scheme !== "wss" && this.urlComponents.scheme !== "sftp")
|
|
return false;
|
|
if (isNaN(this._timingData.secureConnectionStart) && !isNaN(this._timingData.connectionStart))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
get isScript()
|
|
{
|
|
return this._type === Resource.Type.Script;
|
|
}
|
|
|
|
get supportsScriptBlackboxing()
|
|
{
|
|
if (this.localResourceOverride)
|
|
return false;
|
|
if (!this.finished || this.failed)
|
|
return false;
|
|
return super.supportsScriptBlackboxing;
|
|
}
|
|
|
|
get displayName()
|
|
{
|
|
return WI.displayNameForURL(this._url, this.urlComponents);
|
|
}
|
|
|
|
get displayURL()
|
|
{
|
|
const isMultiLine = true;
|
|
const dataURIMaxSize = 64;
|
|
return WI.truncateURL(this._url, isMultiLine, dataURIMaxSize);
|
|
}
|
|
|
|
get mimeTypeComponents()
|
|
{
|
|
if (!this._mimeTypeComponents)
|
|
this._mimeTypeComponents = parseMIMEType(this._mimeType);
|
|
return this._mimeTypeComponents;
|
|
}
|
|
|
|
get syntheticMIMEType()
|
|
{
|
|
// Resources are often transferred with a MIME-type that doesn't match the purpose the
|
|
// resource was loaded for, which is what WI.Resource.Type represents.
|
|
// This getter generates a MIME-type, if needed, that matches the resource type.
|
|
|
|
// If the type matches the Resource.Type of the MIME-type, then return the actual MIME-type.
|
|
if (this._type === WI.Resource.typeFromMIMEType(this._mimeType))
|
|
return this._mimeType;
|
|
|
|
// Return the default MIME-types for the Resource.Type, since the current MIME-type
|
|
// does not match what is expected for the Resource.Type.
|
|
switch (this._type) {
|
|
case WI.Resource.Type.StyleSheet:
|
|
return "text/css";
|
|
case WI.Resource.Type.Script:
|
|
return "text/javascript";
|
|
}
|
|
|
|
// Return the actual MIME-type since we don't have a better synthesized one to return.
|
|
return this._mimeType;
|
|
}
|
|
|
|
createObjectURL()
|
|
{
|
|
let revision = this.currentRevision;
|
|
let blobContent = revision.blobContent;
|
|
if (blobContent)
|
|
return URL.createObjectURL(blobContent)
|
|
|
|
// If content is not available, fallback to using original URL.
|
|
// The client may try to revoke it, but nothing will happen.
|
|
return this._url;
|
|
}
|
|
|
|
isMainResource()
|
|
{
|
|
return this._parentFrame ? this._parentFrame.mainResource === this : false;
|
|
}
|
|
|
|
addInitiatedResource(resource)
|
|
{
|
|
if (!(resource instanceof WI.Resource))
|
|
return;
|
|
|
|
this._initiatedResources.push(resource);
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.InitiatedResourcesDidChange);
|
|
}
|
|
|
|
get queryStringParameters()
|
|
{
|
|
if (this._queryStringParameters === undefined)
|
|
this._queryStringParameters = parseQueryString(this.urlComponents.queryString, true);
|
|
return this._queryStringParameters;
|
|
}
|
|
|
|
get requestFormParameters()
|
|
{
|
|
if (this._requestFormParameters === undefined)
|
|
this._requestFormParameters = this.hasRequestFormParameters() ? parseQueryString(this.requestData, true) : null;
|
|
return this._requestFormParameters;
|
|
}
|
|
|
|
get requestDataContentType()
|
|
{
|
|
return this._requestHeaders.valueForCaseInsensitiveKey("Content-Type") || null;
|
|
}
|
|
|
|
get requestCookies()
|
|
{
|
|
if (!this._requestCookies)
|
|
this._requestCookies = WI.Cookie.parseCookieRequestHeader(this._requestHeaders.valueForCaseInsensitiveKey("Cookie"));
|
|
|
|
return this._requestCookies;
|
|
}
|
|
|
|
get responseCookies()
|
|
{
|
|
if (!this._responseCookies) {
|
|
// FIXME: The backend sends multiple "Set-Cookie" headers in one "Set-Cookie" with multiple values
|
|
// separated by ", ". This doesn't allow us to safely distinguish between a ", " that separates
|
|
// multiple headers or one that may be valid part of a Cookie's value or attribute, such as the
|
|
// ", " in the the date format "Expires=Tue, 03-Oct-2017 04:39:21 GMT". To improve heuristics
|
|
// we do a negative lookahead for numbers, but we can still fail on cookie values containing ", ".
|
|
let rawCombinedHeader = this._responseHeaders.valueForCaseInsensitiveKey("Set-Cookie") || "";
|
|
let setCookieHeaders = rawCombinedHeader.split(/, (?![0-9])/);
|
|
let cookies = [];
|
|
for (let header of setCookieHeaders) {
|
|
let cookie = WI.Cookie.parseSetCookieResponseHeader(header);
|
|
if (cookie)
|
|
cookies.push(cookie);
|
|
}
|
|
this._responseCookies = cookies;
|
|
}
|
|
|
|
return this._responseCookies;
|
|
}
|
|
|
|
get requestSentDate()
|
|
{
|
|
return isNaN(this._requestSentWalltime) ? null : new Date(this._requestSentWalltime * 1000);
|
|
}
|
|
|
|
get lastRedirectReceivedTimestamp()
|
|
{
|
|
return this._redirects.length ? this._redirects.lastValue.timestamp : NaN;
|
|
}
|
|
|
|
get firstTimestamp()
|
|
{
|
|
return this.timingData.startTime || this.lastRedirectReceivedTimestamp || this.responseReceivedTimestamp || this.lastDataReceivedTimestamp || this.finishedOrFailedTimestamp;
|
|
}
|
|
|
|
get lastTimestamp()
|
|
{
|
|
return this.timingData.responseEnd || this.lastDataReceivedTimestamp || this.responseReceivedTimestamp || this.lastRedirectReceivedTimestamp || this.requestSentTimestamp;
|
|
}
|
|
|
|
get latency()
|
|
{
|
|
return this.timingData.responseStart - this.timingData.requestStart;
|
|
}
|
|
|
|
get receiveDuration()
|
|
{
|
|
return this.timingData.responseEnd - this.timingData.responseStart;
|
|
}
|
|
|
|
get totalDuration()
|
|
{
|
|
return this.timingData.responseEnd - this.timingData.startTime;
|
|
}
|
|
|
|
get size()
|
|
{
|
|
if (!isNaN(this._cachedResponseBodySize))
|
|
return this._cachedResponseBodySize;
|
|
|
|
if (!isNaN(this._responseBodySize) && this._responseBodySize !== 0)
|
|
return this._responseBodySize;
|
|
|
|
return this._estimatedSize;
|
|
}
|
|
|
|
get networkEncodedSize()
|
|
{
|
|
return this._responseBodyTransferSize;
|
|
}
|
|
|
|
get networkDecodedSize()
|
|
{
|
|
return this._responseBodySize;
|
|
}
|
|
|
|
get networkTotalTransferSize()
|
|
{
|
|
return this._responseHeadersTransferSize + this._responseBodyTransferSize;
|
|
}
|
|
|
|
get estimatedNetworkEncodedSize()
|
|
{
|
|
let exact = this.networkEncodedSize;
|
|
if (!isNaN(exact))
|
|
return exact;
|
|
|
|
if (this._cached)
|
|
return 0;
|
|
|
|
// FIXME: <https://webkit.org/b/158463> Network: Correctly report encoded data length (transfer size) from CFNetwork to NetworkResourceLoader
|
|
// macOS provides the decoded transfer size instead of the encoded size
|
|
// for estimatedTransferSize. So prefer the "Content-Length" property
|
|
// on mac if it is available.
|
|
if (WI.Platform.name === "mac") {
|
|
let contentLength = Number(this._responseHeaders.valueForCaseInsensitiveKey("Content-Length"));
|
|
if (!isNaN(contentLength))
|
|
return contentLength;
|
|
}
|
|
|
|
if (!isNaN(this._estimatedTransferSize))
|
|
return this._estimatedTransferSize;
|
|
|
|
// If we did not receive actual transfer size from network
|
|
// stack, we prefer using Content-Length over resourceSize as
|
|
// resourceSize may differ from actual transfer size if platform's
|
|
// network stack performed decoding (e.g. gzip decompression).
|
|
// The Content-Length, though, is expected to come from raw
|
|
// response headers and will reflect actual transfer length.
|
|
// This won't work for chunked content encoding, so fall back to
|
|
// resourceSize when we don't have Content-Length. This still won't
|
|
// work for chunks with non-trivial encodings. We need a way to
|
|
// get actual transfer size from the network stack.
|
|
|
|
return Number(this._responseHeaders.valueForCaseInsensitiveKey("Content-Length") || this._estimatedSize);
|
|
}
|
|
|
|
get estimatedTotalTransferSize()
|
|
{
|
|
let exact = this.networkTotalTransferSize;
|
|
if (!isNaN(exact))
|
|
return exact;
|
|
|
|
if (this.statusCode === 304) // Not modified
|
|
return this._estimatedResponseHeadersSize;
|
|
|
|
if (this._cached)
|
|
return 0;
|
|
|
|
return this._estimatedResponseHeadersSize + this.estimatedNetworkEncodedSize;
|
|
}
|
|
|
|
get compressed()
|
|
{
|
|
let contentEncoding = this._responseHeaders.valueForCaseInsensitiveKey("Content-Encoding");
|
|
return !!(contentEncoding && /\b(?:gzip|deflate|br)\b/.test(contentEncoding));
|
|
}
|
|
|
|
get requestedByteRange()
|
|
{
|
|
let range = this._requestHeaders.valueForCaseInsensitiveKey("Range");
|
|
if (!range)
|
|
return null;
|
|
|
|
let rangeValues = range.match(/bytes=(\d+)-(\d+)/);
|
|
if (!rangeValues)
|
|
return null;
|
|
|
|
let start = parseInt(rangeValues[1]);
|
|
if (isNaN(start))
|
|
return null;
|
|
|
|
let end = parseInt(rangeValues[2]);
|
|
if (isNaN(end))
|
|
return null;
|
|
|
|
return {start, end};
|
|
}
|
|
|
|
get scripts()
|
|
{
|
|
return this._scripts || [];
|
|
}
|
|
|
|
get serverTiming()
|
|
{
|
|
if (!this._serverTimingEntries)
|
|
this._serverTimingEntries = WI.ServerTimingEntry.parseHeaders(this._responseHeaders.valueForCaseInsensitiveKey("Server-Timing"));
|
|
return this._serverTimingEntries;
|
|
}
|
|
|
|
scriptForLocation(sourceCodeLocation)
|
|
{
|
|
console.assert(!(this instanceof WI.SourceMapResource));
|
|
console.assert(sourceCodeLocation.sourceCode === this, "SourceCodeLocation must be in this Resource");
|
|
if (sourceCodeLocation.sourceCode !== this)
|
|
return null;
|
|
|
|
var lineNumber = sourceCodeLocation.lineNumber;
|
|
var columnNumber = sourceCodeLocation.columnNumber;
|
|
for (var i = 0; i < this._scripts.length; ++i) {
|
|
var script = this._scripts[i];
|
|
if (script.range.startLine <= lineNumber && script.range.endLine >= lineNumber) {
|
|
if (script.range.startLine === lineNumber && columnNumber < script.range.startColumn)
|
|
continue;
|
|
if (script.range.endLine === lineNumber && columnNumber > script.range.endColumn)
|
|
continue;
|
|
return script;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
updateForRedirectResponse(request, response, elapsedTime, walltime)
|
|
{
|
|
console.assert(!this._finished);
|
|
console.assert(!this._failed);
|
|
console.assert(!this._canceled);
|
|
|
|
let oldURL = this._url;
|
|
let oldHeaders = this._requestHeaders;
|
|
|
|
if (request.url)
|
|
this._url = request.url;
|
|
|
|
this._requestHeaders = request.headers || {};
|
|
this._requestCookies = null;
|
|
this._redirects.push(new WI.Redirect(oldURL, request.method, oldHeaders, response.status, response.statusText, response.headers, elapsedTime));
|
|
|
|
if (oldURL !== request.url) {
|
|
// Delete the URL components so the URL is re-parsed the next time it is requested.
|
|
this._urlComponents = null;
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.URLDidChange, {oldURL});
|
|
}
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.RequestHeadersDidChange);
|
|
this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange);
|
|
}
|
|
|
|
hasResponse()
|
|
{
|
|
return !isNaN(this._statusCode) || this._finished || this._failed;
|
|
}
|
|
|
|
hasRequestFormParameters()
|
|
{
|
|
let requestDataContentType = this.requestDataContentType;
|
|
return requestDataContentType && requestDataContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i);
|
|
}
|
|
|
|
updateForResponse(url, mimeType, type, responseHeaders, statusCode, statusText, elapsedTime, timingData, source, security)
|
|
{
|
|
console.assert(!this._finished);
|
|
console.assert(!this._failed);
|
|
console.assert(!this._canceled);
|
|
|
|
let oldURL = this._url;
|
|
let oldMIMEType = this._mimeType;
|
|
let oldType = this._type;
|
|
|
|
if (type in WI.Resource.Type)
|
|
type = WI.Resource.Type[type];
|
|
else if (type === "Stylesheet") {
|
|
// COMPATIBILITY (iOS 13): Page.ResourceType.Stylesheet was renamed to Page.ResourceType.StyleSheet.
|
|
type = WI.Resource.Type.StyleSheet;
|
|
}
|
|
|
|
if (url)
|
|
this._url = url;
|
|
|
|
this._mimeType = mimeType;
|
|
this._type = Resource.resolvedType(type, mimeType);
|
|
this._statusCode = statusCode;
|
|
this._statusText = statusText;
|
|
this._responseHeaders = responseHeaders || {};
|
|
this._responseCookies = null;
|
|
this._serverTimingEntries = null;
|
|
this._responseReceivedTimestamp = elapsedTime || NaN;
|
|
this._timingData = WI.ResourceTimingData.fromPayload(timingData, this);
|
|
|
|
if (source)
|
|
this._responseSource = WI.Resource.responseSourceFromPayload(source);
|
|
|
|
this._security = security || {};
|
|
|
|
const headerBaseSize = 12; // Length of "HTTP/1.1 ", " ", and "\r\n".
|
|
const headerPad = 4; // Length of ": " and "\r\n".
|
|
this._estimatedResponseHeadersSize = String(this._statusCode).length + this._statusText.length + headerBaseSize;
|
|
for (let name in this._responseHeaders)
|
|
this._estimatedResponseHeadersSize += name.length + this._responseHeaders[name].length + headerPad;
|
|
|
|
if (!this._cached) {
|
|
if (statusCode === 304 || (this._responseSource === WI.Resource.ResponseSource.MemoryCache || this._responseSource === WI.Resource.ResponseSource.DiskCache))
|
|
this.markAsCached();
|
|
}
|
|
|
|
if (oldURL !== url) {
|
|
// Delete the URL components so the URL is re-parsed the next time it is requested.
|
|
this._urlComponents = null;
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.URLDidChange, {oldURL});
|
|
}
|
|
|
|
if (oldMIMEType !== mimeType) {
|
|
// Delete the MIME-type components so the MIME-type is re-parsed the next time it is requested.
|
|
this._mimeTypeComponents = null;
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.MIMETypeDidChange, {oldMIMEType});
|
|
}
|
|
|
|
if (oldType !== type)
|
|
this.dispatchEventToListeners(WI.Resource.Event.TypeDidChange, {oldType});
|
|
|
|
console.assert(isNaN(this._estimatedSize));
|
|
console.assert(isNaN(this._estimatedTransferSize));
|
|
|
|
// The transferSize becomes 0 when status is 304 or Content-Length is available, so
|
|
// notify listeners of that change.
|
|
if (statusCode === 304 || this._responseHeaders.valueForCaseInsensitiveKey("Content-Length"))
|
|
this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.ResponseReceived);
|
|
this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange);
|
|
}
|
|
|
|
updateWithMetrics(metrics)
|
|
{
|
|
this._receivedNetworkLoadMetrics = true;
|
|
|
|
if (metrics.protocol)
|
|
this._protocol = metrics.protocol;
|
|
if (metrics.priority)
|
|
this._priority = WI.Resource.networkPriorityFromPayload(metrics.priority);
|
|
if (metrics.remoteAddress)
|
|
this._remoteAddress = metrics.remoteAddress;
|
|
if (metrics.connectionIdentifier)
|
|
this._connectionIdentifier = WI.Resource.connectionIdentifierFromPayload(metrics.connectionIdentifier);
|
|
if (metrics.requestHeaders) {
|
|
this._requestHeaders = metrics.requestHeaders;
|
|
this._requestCookies = null;
|
|
this.dispatchEventToListeners(WI.Resource.Event.RequestHeadersDidChange);
|
|
}
|
|
|
|
if ("requestHeaderBytesSent" in metrics) {
|
|
this._requestHeadersTransferSize = metrics.requestHeaderBytesSent;
|
|
this._requestBodyTransferSize = metrics.requestBodyBytesSent;
|
|
this._responseHeadersTransferSize = metrics.responseHeaderBytesReceived;
|
|
this._responseBodyTransferSize = metrics.responseBodyBytesReceived;
|
|
this._responseBodySize = metrics.responseBodyDecodedSize;
|
|
|
|
console.assert(this._requestHeadersTransferSize >= 0);
|
|
console.assert(this._requestBodyTransferSize >= 0);
|
|
console.assert(this._responseHeadersTransferSize >= 0);
|
|
console.assert(this._responseBodyTransferSize >= 0);
|
|
console.assert(this._responseBodySize >= 0);
|
|
|
|
// There may have been no size updates received during load if Content-Length was 0.
|
|
if (isNaN(this._estimatedSize))
|
|
this._estimatedSize = 0;
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.SizeDidChange, {previousSize: this._estimatedSize});
|
|
this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
|
|
}
|
|
|
|
if (metrics.securityConnection) {
|
|
if (!this._security)
|
|
this._security = {};
|
|
this._security.connection = metrics.securityConnection;
|
|
}
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.MetricsDidChange);
|
|
}
|
|
|
|
setCachedResponseBodySize(size)
|
|
{
|
|
console.assert(!isNaN(size), "Size should be a valid number.");
|
|
console.assert(isNaN(this._cachedResponseBodySize), "This should only be set once.");
|
|
console.assert(this._estimatedSize === size, "The legacy path was updated already and matches.");
|
|
|
|
this._cachedResponseBodySize = size;
|
|
}
|
|
|
|
requestContentFromBackend()
|
|
{
|
|
let specialContentPromise = WI.SourceCode.generateSpecialContentForURL(this._url);
|
|
if (specialContentPromise)
|
|
return specialContentPromise;
|
|
|
|
if (this._target.type === WI.TargetType.Worker) {
|
|
console.assert(this.isScript);
|
|
let scriptForTarget = this.scripts.find((script) => script.target === this._target);
|
|
console.assert(scriptForTarget);
|
|
if (scriptForTarget)
|
|
return scriptForTarget.requestContentFromBackend();
|
|
} else {
|
|
// If we have the requestIdentifier we can get the actual response for this specific resource.
|
|
// Otherwise the content will be cached resource data, which might not exist anymore.
|
|
if (this._requestIdentifier)
|
|
return this._target.NetworkAgent.getResponseBody(this._requestIdentifier);
|
|
|
|
// There is no request identifier or frame to request content from.
|
|
if (this._parentFrame)
|
|
return this._target.PageAgent.getResourceContent(this._parentFrame.id, this._url);
|
|
}
|
|
|
|
return Promise.reject(new Error("Content request failed."));
|
|
}
|
|
|
|
increaseSize(dataLength, elapsedTime)
|
|
{
|
|
console.assert(dataLength >= 0);
|
|
console.assert(!this._receivedNetworkLoadMetrics, "If we received metrics we don't need to change the estimated size.");
|
|
|
|
if (isNaN(this._estimatedSize))
|
|
this._estimatedSize = 0;
|
|
|
|
let previousSize = this._estimatedSize;
|
|
|
|
this._estimatedSize += dataLength;
|
|
|
|
this._lastDataReceivedTimestamp = elapsedTime || NaN;
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.SizeDidChange, {previousSize});
|
|
|
|
// The estimatedTransferSize is based off of size when status is not 304 or Content-Length is missing.
|
|
if (isNaN(this._estimatedTransferSize) && this._statusCode !== 304 && !this._responseHeaders.valueForCaseInsensitiveKey("Content-Length"))
|
|
this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
|
|
}
|
|
|
|
increaseTransferSize(encodedDataLength)
|
|
{
|
|
console.assert(encodedDataLength >= 0);
|
|
console.assert(!this._receivedNetworkLoadMetrics, "If we received metrics we don't need to change the estimated transfer size.");
|
|
|
|
if (isNaN(this._estimatedTransferSize))
|
|
this._estimatedTransferSize = 0;
|
|
this._estimatedTransferSize += encodedDataLength;
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
|
|
}
|
|
|
|
markAsCached()
|
|
{
|
|
this._cached = true;
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.CacheStatusDidChange);
|
|
|
|
// The transferSize starts returning 0 when cached is true, unless status is 304.
|
|
if (this._statusCode !== 304)
|
|
this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
|
|
}
|
|
|
|
markAsFinished(elapsedTime)
|
|
{
|
|
console.assert(!this._failed);
|
|
console.assert(!this._canceled);
|
|
|
|
this._finished = true;
|
|
this._finishedOrFailedTimestamp = elapsedTime || NaN;
|
|
this._timingData.markResponseEndTime(elapsedTime || NaN);
|
|
|
|
if (this._finishThenRequestContentPromise)
|
|
this._finishThenRequestContentPromise = null;
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.LoadingDidFinish);
|
|
this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange);
|
|
}
|
|
|
|
markAsFailed(canceled, elapsedTime, errorText)
|
|
{
|
|
console.assert(!this._finished);
|
|
|
|
this._failed = true;
|
|
this._canceled = canceled;
|
|
this._finishedOrFailedTimestamp = elapsedTime || NaN;
|
|
|
|
if (!this._failureReasonText)
|
|
this._failureReasonText = errorText || null;
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.LoadingDidFail);
|
|
this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange);
|
|
}
|
|
|
|
revertMarkAsFinished()
|
|
{
|
|
console.assert(!this._failed);
|
|
console.assert(!this._canceled);
|
|
console.assert(this._finished);
|
|
|
|
this._finished = false;
|
|
this._finishedOrFailedTimestamp = NaN;
|
|
}
|
|
|
|
legacyMarkServedFromMemoryCache()
|
|
{
|
|
// COMPATIBILITY (iOS 10.3): This is a legacy code path where we know the resource came from the MemoryCache.
|
|
console.assert(this._responseSource === WI.Resource.ResponseSource.Unknown);
|
|
|
|
this._responseSource = WI.Resource.ResponseSource.MemoryCache;
|
|
|
|
this.markAsCached();
|
|
}
|
|
|
|
legacyMarkServedFromDiskCache()
|
|
{
|
|
// COMPATIBILITY (iOS 10.3): This is a legacy code path where we know the resource came from the DiskCache.
|
|
console.assert(this._responseSource === WI.Resource.ResponseSource.Unknown);
|
|
|
|
this._responseSource = WI.Resource.ResponseSource.DiskCache;
|
|
|
|
this.markAsCached();
|
|
}
|
|
|
|
isLoading()
|
|
{
|
|
return !this._finished && !this._failed;
|
|
}
|
|
|
|
hadLoadingError()
|
|
{
|
|
return this._failed || this._canceled || this._statusCode >= 400;
|
|
}
|
|
|
|
getImageSize(callback)
|
|
{
|
|
// Throw an error in the case this resource is not an image.
|
|
if (this.type !== WI.Resource.Type.Image)
|
|
throw "Resource is not an image.";
|
|
|
|
// See if we've already computed and cached the image size,
|
|
// in which case we can provide them directly.
|
|
if (this._imageSize !== undefined) {
|
|
callback(this._imageSize);
|
|
return;
|
|
}
|
|
|
|
var objectURL = null;
|
|
|
|
// Event handler for the image "load" event.
|
|
function imageDidLoad() {
|
|
URL.revokeObjectURL(objectURL);
|
|
|
|
// Cache the image metrics.
|
|
this._imageSize = {
|
|
width: image.width,
|
|
height: image.height
|
|
};
|
|
|
|
callback(this._imageSize);
|
|
}
|
|
|
|
function requestContentFailure() {
|
|
this._imageSize = null;
|
|
callback(this._imageSize);
|
|
}
|
|
|
|
// Create an <img> element that we'll use to load the image resource
|
|
// so that we can query its intrinsic size.
|
|
var image = new Image;
|
|
image.addEventListener("load", imageDidLoad.bind(this), false);
|
|
|
|
// Set the image source using an object URL once we've obtained its data.
|
|
this.requestContent().then((content) => {
|
|
objectURL = image.src = content.sourceCode.createObjectURL();
|
|
if (!objectURL)
|
|
requestContentFailure.call(this);
|
|
}, requestContentFailure.bind(this));
|
|
}
|
|
|
|
requestContent()
|
|
{
|
|
if (this._finished)
|
|
return super.requestContent().catch(this._requestContentFailure.bind(this));
|
|
|
|
if (this._failed)
|
|
return this._requestContentFailure();
|
|
|
|
if (!this._finishThenRequestContentPromise) {
|
|
this._finishThenRequestContentPromise = new Promise((resolve, reject) => {
|
|
this.singleFireEventListener(WI.Resource.Event.LoadingDidFinish, resolve, this);
|
|
this.singleFireEventListener(WI.Resource.Event.LoadingDidFail, reject, this);
|
|
}).then(this.requestContent.bind(this));
|
|
}
|
|
|
|
return this._finishThenRequestContentPromise;
|
|
}
|
|
|
|
associateWithScript(script)
|
|
{
|
|
if (!this._scripts)
|
|
this._scripts = [];
|
|
|
|
this._scripts.push(script);
|
|
|
|
if (this._type === WI.Resource.Type.Other || this._type === WI.Resource.Type.XHR) {
|
|
let oldType = this._type;
|
|
this._type = WI.Resource.Type.Script;
|
|
this.dispatchEventToListeners(WI.Resource.Event.TypeDidChange, {oldType});
|
|
}
|
|
}
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
cookie[WI.Resource.URLCookieKey] = this.url.hash;
|
|
cookie[WI.Resource.MainResourceCookieKey] = this.isMainResource();
|
|
}
|
|
|
|
async createLocalResourceOverride(type, {mimeType, base64Encoded, content} = {})
|
|
{
|
|
console.assert(!this.localResourceOverride);
|
|
console.assert(WI.NetworkManager.supportsOverridingResponses());
|
|
|
|
let resourceData = {
|
|
requestURL: this.url,
|
|
};
|
|
|
|
switch (type) {
|
|
case WI.LocalResourceOverride.InterceptType.Request:
|
|
resourceData.requestMethod = this.requestMethod ?? WI.HTTPUtilities.RequestMethod.GET;
|
|
resourceData.requestHeaders = Object.shallowCopy(this.requestHeaders);
|
|
resourceData.requestData = this.requestData ?? "";
|
|
break;
|
|
|
|
case WI.LocalResourceOverride.InterceptType.Response:
|
|
case WI.LocalResourceOverride.InterceptType.ResponseSkippingNetwork:
|
|
resourceData.responseMIMEType = this.mimeType ?? WI.mimeTypeForFileExtension(WI.fileExtensionForFilename(this.urlComponents.lastPathComponent));
|
|
resourceData.responseStatusCode = this.statusCode;
|
|
resourceData.responseStatusText = this.statusText;
|
|
if (!resourceData.responseStatusCode) {
|
|
resourceData.responseStatusCode = 200;
|
|
resourceData.responseStatusText = null;
|
|
}
|
|
resourceData.responseStatusText ||= WI.HTTPUtilities.statusTextForStatusCode(resourceData.responseStatusCode);
|
|
|
|
if (base64Encoded === undefined || content === undefined) {
|
|
try {
|
|
let {rawContent, rawBase64Encoded} = await this.requestContent();
|
|
content ??= rawContent;
|
|
base64Encoded ??= rawBase64Encoded;
|
|
} catch {
|
|
content ??= "";
|
|
base64Encoded ??= !WI.shouldTreatMIMETypeAsText(resourceData.mimeType);
|
|
}
|
|
}
|
|
resourceData.responseContent = content;
|
|
resourceData.responseBase64Encoded = base64Encoded;
|
|
resourceData.responseHeaders = Object.shallowCopy(this.responseHeaders);
|
|
break;
|
|
}
|
|
|
|
return WI.LocalResourceOverride.create(WI.urlWithoutFragment(this.url), type, resourceData);
|
|
}
|
|
|
|
updateLocalResourceOverrideRequestData(data)
|
|
{
|
|
console.assert(this.localResourceOverride);
|
|
|
|
if (data === this._requestData)
|
|
return;
|
|
|
|
this._requestData = data;
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.RequestDataDidChange);
|
|
}
|
|
|
|
generateCURLCommand()
|
|
{
|
|
function escapeStringPosix(str) {
|
|
function escapeCharacter(x) {
|
|
let code = x.charCodeAt(0);
|
|
let hex = code.toString(16);
|
|
if (code < 256)
|
|
return "\\x" + hex.padStart(2, "0");
|
|
return "\\u" + hex.padStart(4, "0");
|
|
}
|
|
|
|
if (/[^\x20-\x7E]|'/.test(str)) {
|
|
// Use ANSI-C quoting syntax.
|
|
return "$'" + str.replace(/\\/g, "\\\\")
|
|
.replace(/'/g, "\\'")
|
|
.replace(/\n/g, "\\n")
|
|
.replace(/\r/g, "\\r")
|
|
.replace(/!/g, "\\041")
|
|
.replace(/[^\x20-\x7E]/g, escapeCharacter) + "'";
|
|
}
|
|
|
|
// Use single quote syntax.
|
|
return `'${str}'`;
|
|
}
|
|
|
|
let command = ["curl " + escapeStringPosix(this.url).replace(/[[{}\]]/g, "\\$&")];
|
|
command.push("-X " + escapeStringPosix(this.requestMethod));
|
|
|
|
for (let key in this.requestHeaders)
|
|
command.push("-H " + escapeStringPosix(`${key}: ${this.requestHeaders[key]}`));
|
|
|
|
if (this.requestDataContentType && this.requestMethod !== "GET" && this.requestData) {
|
|
if (this.requestDataContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i))
|
|
command.push("--data " + escapeStringPosix(this.requestData));
|
|
else
|
|
command.push("--data-binary " + escapeStringPosix(this.requestData));
|
|
}
|
|
|
|
return command.join(" \\\n");
|
|
}
|
|
|
|
stringifyHTTPRequest()
|
|
{
|
|
let lines = [];
|
|
|
|
let protocol = this.protocol || "";
|
|
if (protocol === "h2") {
|
|
// HTTP/2 Request pseudo headers:
|
|
// https://tools.ietf.org/html/rfc7540#section-8.1.2.3
|
|
lines.push(`:method: ${this.requestMethod}`);
|
|
lines.push(`:scheme: ${this.urlComponents.scheme}`);
|
|
lines.push(`:authority: ${WI.h2Authority(this.urlComponents)}`);
|
|
lines.push(`:path: ${WI.h2Path(this.urlComponents)}`);
|
|
} else {
|
|
// HTTP/1.1 request line:
|
|
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1
|
|
lines.push(`${this.requestMethod} ${this.urlComponents.path}${protocol ? " " + protocol.toUpperCase() : ""}`);
|
|
}
|
|
|
|
for (let key in this.requestHeaders)
|
|
lines.push(`${key}: ${this.requestHeaders[key]}`);
|
|
|
|
return lines.join("\n") + "\n";
|
|
}
|
|
|
|
stringifyHTTPResponse()
|
|
{
|
|
let lines = [];
|
|
|
|
let protocol = this.protocol || "";
|
|
if (protocol === "h2") {
|
|
// HTTP/2 Response pseudo headers:
|
|
// https://tools.ietf.org/html/rfc7540#section-8.1.2.4
|
|
lines.push(`:status: ${this.statusCode}`);
|
|
} else {
|
|
// HTTP/1.1 response status line:
|
|
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1
|
|
lines.push(`${protocol ? protocol.toUpperCase() + " " : ""}${this.statusCode} ${this.statusText}`);
|
|
}
|
|
|
|
for (let key in this.responseHeaders)
|
|
lines.push(`${key}: ${this.responseHeaders[key]}`);
|
|
|
|
return lines.join("\n") + "\n";
|
|
}
|
|
|
|
async showCertificate()
|
|
{
|
|
let errorString = WI.UIString("Unable to show certificate for \u201C%s\u201D").format(this.url);
|
|
|
|
try {
|
|
let {serializedCertificate} = await this._target.NetworkAgent.getSerializedCertificate(this._requestIdentifier);
|
|
if (InspectorFrontendHost.showCertificate(serializedCertificate))
|
|
return;
|
|
} catch (e) {
|
|
console.error(e);
|
|
throw errorString;
|
|
}
|
|
|
|
let consoleMessage = new WI.ConsoleMessage(this._target, WI.ConsoleMessage.MessageSource.Other, WI.ConsoleMessage.MessageLevel.Error, errorString);
|
|
consoleMessage.shouldRevealConsole = true;
|
|
WI.consoleLogViewController.appendConsoleMessage(consoleMessage);
|
|
|
|
throw errorString;
|
|
}
|
|
|
|
// Private
|
|
|
|
_requestContentFailure(error)
|
|
{
|
|
return Promise.resolve({
|
|
error: WI.UIString("An error occurred trying to load the resource."),
|
|
reason: error?.message || this._failureReasonText,
|
|
sourceCode: this,
|
|
});
|
|
}
|
|
};
|
|
|
|
WI.Resource.TypeIdentifier = "resource";
|
|
WI.Resource.URLCookieKey = "resource-url";
|
|
WI.Resource.MainResourceCookieKey = "resource-is-main-resource";
|
|
|
|
WI.Resource.Event = {
|
|
URLDidChange: "resource-url-did-change",
|
|
MIMETypeDidChange: "resource-mime-type-did-change",
|
|
TypeDidChange: "resource-type-did-change",
|
|
RequestHeadersDidChange: "resource-request-headers-did-change",
|
|
RequestDataDidChange: "resource-request-data-did-change",
|
|
ResponseReceived: "resource-response-received",
|
|
LoadingDidFinish: "resource-loading-did-finish",
|
|
LoadingDidFail: "resource-loading-did-fail",
|
|
TimestampsDidChange: "resource-timestamps-did-change",
|
|
SizeDidChange: "resource-size-did-change",
|
|
TransferSizeDidChange: "resource-transfer-size-did-change",
|
|
CacheStatusDidChange: "resource-cached-did-change",
|
|
MetricsDidChange: "resource-metrics-did-change",
|
|
InitiatedResourcesDidChange: "resource-initiated-resources-did-change",
|
|
};
|
|
|
|
// Keep these in sync with the "ResourceType" enum defined by the "Page" domain.
|
|
WI.Resource.Type = {
|
|
Document: "resource-type-document",
|
|
StyleSheet: "resource-type-style-sheet",
|
|
Image: "resource-type-image",
|
|
Font: "resource-type-font",
|
|
Script: "resource-type-script",
|
|
XHR: "resource-type-xhr",
|
|
Fetch: "resource-type-fetch",
|
|
Ping: "resource-type-ping",
|
|
Beacon: "resource-type-beacon",
|
|
WebSocket: "resource-type-websocket",
|
|
Other: "resource-type-other",
|
|
};
|
|
|
|
WI.Resource.ResponseSource = {
|
|
Unknown: Symbol("unknown"),
|
|
Network: Symbol("network"),
|
|
MemoryCache: Symbol("memory-cache"),
|
|
DiskCache: Symbol("disk-cache"),
|
|
ServiceWorker: Symbol("service-worker"),
|
|
InspectorOverride: Symbol("inspector-override"),
|
|
};
|
|
|
|
WI.Resource.NetworkPriority = {
|
|
Unknown: Symbol("unknown"),
|
|
Low: Symbol("low"),
|
|
Medium: Symbol("medium"),
|
|
High: Symbol("high"),
|
|
};
|
|
|
|
WI.Resource.GroupingMode = {
|
|
Path: "group-resource-by-path",
|
|
Type: "group-resource-by-type",
|
|
};
|
|
WI.settings.resourceGroupingMode = new WI.Setting("resource-grouping-mode", WI.Resource.GroupingMode.Type);
|
|
|
|
// This MIME Type map is private, use WI.Resource.typeFromMIMEType().
|
|
WI.Resource._mimeTypeMap = {
|
|
"text/html": WI.Resource.Type.Document,
|
|
"text/xml": WI.Resource.Type.Document,
|
|
"application/xhtml+xml": WI.Resource.Type.Document,
|
|
|
|
"text/plain": WI.Resource.Type.Other,
|
|
|
|
"text/css": WI.Resource.Type.StyleSheet,
|
|
"text/xsl": WI.Resource.Type.StyleSheet,
|
|
"text/x-less": WI.Resource.Type.StyleSheet,
|
|
"text/x-sass": WI.Resource.Type.StyleSheet,
|
|
"text/x-scss": WI.Resource.Type.StyleSheet,
|
|
|
|
"application/pdf": WI.Resource.Type.Image,
|
|
"image/svg+xml": WI.Resource.Type.Image,
|
|
|
|
"application/x-font-type1": WI.Resource.Type.Font,
|
|
"application/x-font-ttf": WI.Resource.Type.Font,
|
|
"application/x-font-woff": WI.Resource.Type.Font,
|
|
"application/x-truetype-font": WI.Resource.Type.Font,
|
|
|
|
"text/javascript": WI.Resource.Type.Script,
|
|
"text/ecmascript": WI.Resource.Type.Script,
|
|
"application/javascript": WI.Resource.Type.Script,
|
|
"application/ecmascript": WI.Resource.Type.Script,
|
|
"application/x-javascript": WI.Resource.Type.Script,
|
|
"application/json": WI.Resource.Type.Script,
|
|
"application/x-json": WI.Resource.Type.Script,
|
|
"text/x-javascript": WI.Resource.Type.Script,
|
|
"text/x-json": WI.Resource.Type.Script,
|
|
"text/javascript1.1": WI.Resource.Type.Script,
|
|
"text/javascript1.2": WI.Resource.Type.Script,
|
|
"text/javascript1.3": WI.Resource.Type.Script,
|
|
"text/jscript": WI.Resource.Type.Script,
|
|
"text/livescript": WI.Resource.Type.Script,
|
|
"text/x-livescript": WI.Resource.Type.Script,
|
|
"text/typescript": WI.Resource.Type.Script,
|
|
"text/typescript-jsx": WI.Resource.Type.Script,
|
|
"text/jsx": WI.Resource.Type.Script,
|
|
"text/x-clojure": WI.Resource.Type.Script,
|
|
"text/x-coffeescript": WI.Resource.Type.Script,
|
|
};
|