blob: 80c4115450eeab1dd6b493fb44d57591edd236f9 [file] [log] [blame]
/*
* Copyright (C) 2017-2018 Apple 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.
*/
// HTTP Archive (HAR) format - Version 1.2
// https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/HAR/Overview.html#sec-har-object-types-creator
// http://www.softwareishard.com/blog/har-12-spec/
WI.HARBuilder = class HARBuilder
{
static async buildArchive(resources)
{
let promises = [];
for (let resource of resources) {
console.assert(resource.finished);
promises.push(new Promise((resolve, reject) => {
// Always resolve.
resource.requestContent().then(
(x) => resolve(x),
() => resolve(null)
);
}));
}
let contents = await Promise.all(promises);
console.assert(contents.length === resources.length);
return {
log: {
version: "1.2",
creator: HARBuilder.creator(),
pages: HARBuilder.pages(),
entries: resources.map((resource, index) => HARBuilder.entry(resource, contents[index])),
}
};
}
static creator()
{
return {
name: "WebKit Web Inspector",
version: WI.Platform.version.build || "1.0",
};
}
static pages()
{
return [{
startedDateTime: HARBuilder.date(WI.networkManager.mainFrame.mainResource.requestSentDate),
id: "page_0",
title: WI.networkManager.mainFrame.url || "",
pageTimings: HARBuilder.pageTimings(),
}];
}
static pageTimings()
{
let result = {};
let domContentReadyEventTimestamp = WI.networkManager.mainFrame.domContentReadyEventTimestamp;
if (!isNaN(domContentReadyEventTimestamp))
result.onContentLoad = domContentReadyEventTimestamp * 1000;
let loadEventTimestamp = WI.networkManager.mainFrame.loadEventTimestamp;
if (!isNaN(loadEventTimestamp))
result.onLoad = loadEventTimestamp * 1000;
return result;
}
static entry(resource, content)
{
let entry = {
pageref: "page_0",
startedDateTime: HARBuilder.date(resource.requestSentDate),
time: 0,
request: HARBuilder.request(resource),
response: HARBuilder.response(resource, content),
cache: HARBuilder.cache(resource),
timings: HARBuilder.timings(resource),
};
if (resource.timingData.startTime && resource.timingData.responseEnd)
entry.time = (resource.timingData.responseEnd - resource.timingData.startTime) * 1000;
if (resource.remoteAddress) {
entry.serverIPAddress = HARBuilder.ipAddress(resource.remoteAddress);
// WebKit Custom Field `_serverPort`.
if (entry.serverIPAddress)
entry._serverPort = HARBuilder.port(resource.remoteAddress);
}
if (resource.connectionIdentifier)
entry.connection = "" + resource.connectionIdentifier;
// CFNetwork Custom Field `_fetchType`.
if (resource.responseSource !== WI.Resource.ResponseSource.Unknown)
entry._fetchType = HARBuilder.fetchType(resource.responseSource);
// WebKit Custom Field `_priority`.
if (resource.priority !== WI.Resource.NetworkPriority.Unknown)
entry._priority = HARBuilder.priority(resource.priority);
return entry;
}
static request(resource)
{
let result = {
method: resource.requestMethod || "",
url: resource.url || "",
httpVersion: WI.Resource.displayNameForProtocol(resource.protocol) || "",
cookies: HARBuilder.cookies(resource.requestCookies, null),
headers: HARBuilder.headers(resource.requestHeaders),
queryString: resource.queryStringParameters || [],
headersSize: !isNaN(resource.requestHeadersTransferSize) ? resource.requestHeadersTransferSize : -1,
bodySize: !isNaN(resource.requestBodyTransferSize) ? resource.requestBodyTransferSize : -1,
};
if (resource.requestData)
result.postData = HARBuilder.postData(resource);
return result;
}
static response(resource, content)
{
let result = {
status: resource.statusCode || 0,
statusText: resource.statusText || "",
httpVersion: WI.Resource.displayNameForProtocol(resource.protocol) || "",
cookies: HARBuilder.cookies(resource.responseCookies, resource.requestSentDate),
headers: HARBuilder.headers(resource.responseHeaders),
content: HARBuilder.content(resource, content),
redirectURL: resource.responseHeaders.valueForCaseInsensitiveKey("Location") || "",
headersSize: !isNaN(resource.responseHeadersTransferSize) ? resource.responseHeadersTransferSize : -1,
bodySize: !isNaN(resource.responseBodyTransferSize) ? resource.responseBodyTransferSize : -1,
};
// Chrome Custom Field `_transferSize`.
if (!isNaN(resource.networkTotalTransferSize))
result._transferSize = resource.networkTotalTransferSize;
// Chrome Custom Field `_error`.
if (resource.failureReasonText)
result._error = resource.failureReasonText;
return result;
}
static cookies(cookies, requestSentDate)
{
let result = [];
for (let cookie of cookies) {
let json = {
name: cookie.name,
value: cookie.value,
};
if (cookie.type === WI.Cookie.Type.Response) {
if (cookie.path)
json.path = cookie.path;
if (cookie.domain)
json.domain = cookie.domain;
json.expires = HARBuilder.date(cookie.expirationDate(requestSentDate));
json.httpOnly = cookie.httpOnly;
json.secure = cookie.secure;
if (cookie.sameSite !== WI.Cookie.SameSiteType.None)
json.sameSite = cookie.sameSite;
}
result.push(json);
}
return result;
}
static headers(headers)
{
let result = [];
for (let key in headers)
result.push({name: key, value: headers[key]});
return result;
}
static content(resource, content)
{
let encodedSize = !isNaN(resource.networkEncodedSize) ? resource.networkEncodedSize : resource.estimatedNetworkEncodedSize;
let decodedSize = !isNaN(resource.networkDecodedSize) ? resource.networkDecodedSize : resource.size;
if (isNaN(decodedSize))
decodedSize = 0;
if (isNaN(encodedSize))
encodedSize = 0;
let result = {
size: decodedSize,
compression: decodedSize - encodedSize,
mimeType: resource.mimeType || "x-unknown",
};
if (content) {
if (content.rawContent)
result.text = content.rawContent;
if (content.rawBase64Encoded)
result.encoding = "base64";
}
return result;
}
static postData(resource)
{
return {
mimeType: resource.requestDataContentType || "",
text: resource.requestData,
params: resource.requestFormParameters || [],
};
}
static cache(resource)
{
// FIXME: <https://webkit.org/b/178682> Web Inspector: Include <cache> details in HAR Export
// http://www.softwareishard.com/blog/har-12-spec/#cache
return {};
}
static timings(resource)
{
// FIXME: <https://webkit.org/b/195694> Web Inspector: HAR Extension for Redirect Timing Info
// Chrome has Custom Fields `_blocked_queueing` and `_blocked_proxy`.
let result = {
blocked: -1,
dns: -1,
connect: -1,
ssl: -1,
send: 0,
wait: 0,
receive: 0,
};
if (resource.timingData.startTime && resource.timingData.responseEnd) {
let {startTime, domainLookupStart, domainLookupEnd, connectStart, connectEnd, secureConnectionStart, requestStart, responseStart, responseEnd} = resource.timingData;
result.blocked = ((domainLookupStart || connectStart || requestStart) - startTime) * 1000;
if (domainLookupStart)
result.dns = ((domainLookupEnd || connectStart || requestStart) - domainLookupStart) * 1000;
if (connectStart)
result.connect = ((connectEnd || requestStart) - connectStart) * 1000;
if (secureConnectionStart)
result.ssl = ((connectEnd || requestStart) - secureConnectionStart) * 1000;
// If all the time before requestStart was included in blocked, then make send time zero
// as send time is essentially just blocked time after dns / connection time, and we
// do not want to double count it.
result.send = (domainLookupEnd || connectEnd) ? (requestStart - (connectEnd || domainLookupEnd)) * 1000 : 0;
result.wait = (responseStart - requestStart) * 1000;
result.receive = (responseEnd - responseStart) * 1000;
}
return result;
}
// Helpers
static ipAddress(remoteAddress)
{
// IP Address, without port.
if (!remoteAddress)
return "";
// NOTE: Resource.remoteAddress always includes the port at the end.
// So this always strips the last part.
return remoteAddress.replace(/:\d+$/, "");
}
static port(remoteAddress)
{
// IP Address, without port.
if (!remoteAddress)
return undefined;
// NOTE: Resource.remoteAddress always includes the port at the end.
// So this always matches the last part.
let index = remoteAddress.lastIndexOf(":");
if (!index)
return undefined;
let portString = remoteAddress.substr(index + 1);
let port = parseInt(portString);
if (isNaN(port))
return undefined;
return port;
}
static date(date)
{
// ISO 8601
if (!date)
return "";
return date.toISOString();
}
static fetchType(responseSource)
{
switch (responseSource) {
case WI.Resource.ResponseSource.Network:
return "Network Load";
case WI.Resource.ResponseSource.MemoryCache:
return "Memory Cache";
case WI.Resource.ResponseSource.DiskCache:
return "Disk Cache";
case WI.Resource.ResponseSource.ServiceWorker:
return "Service Worker";
}
console.assert();
return undefined;
}
static priority(priority)
{
switch (priority) {
case WI.Resource.NetworkPriority.Low:
return "low";
case WI.Resource.NetworkPriority.Medium:
return "medium";
case WI.Resource.NetworkPriority.High:
return "high";
}
console.assert();
return undefined;
}
// Consuming.
static dateFromHARDate(isoString)
{
return Date.parse(isoString);
}
static protocolFromHARProtocol(protocol)
{
switch (protocol) {
case "HTTP/2":
return "h2";
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";
}
if (protocol)
console.warn("Unknown HAR protocol value", protocol);
return null;
}
static responseSourceFromHARFetchType(fetchType)
{
switch (fetchType) {
case "Network Load":
return WI.Resource.ResponseSource.Network;
case "Memory Cache":
return WI.Resource.ResponseSource.MemoryCache;
case "Disk Cache":
return WI.Resource.ResponseSource.DiskCache;
}
if (fetchType)
console.warn("Unknown HAR _fetchType value", fetchType);
return WI.Resource.ResponseSource.Other;
}
static networkPriorityFromHARPriority(priority)
{
switch (priority) {
case "low":
return WI.Resource.NetworkPriority.Low;
case "medium":
return WI.Resource.NetworkPriority.Medium;
case "high":
return WI.Resource.NetworkPriority.High;
}
if (priority)
console.warn("Unknown HAR priority value", priority);
return WI.Resource.NetworkPriority.Unknown;
}
};