| /* |
| * 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; |
| } |
| }; |