/* * 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: "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: Web Inspector: Include details in HAR Export // http://www.softwareishard.com/blog/har-12-spec/#cache return {}; } static timings(resource) { // FIXME: 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"; case WI.Resource.ResponseSource.InspectorOverride: return "Inspector Override"; } 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; case "Service Worker": return WI.Resource.ResponseSource.ServiceWorker; case "Inspector Override": return WI.Resource.ResponseSource.InspectorOverride; } 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; } };