1/* 2 * Copyright (C) 2012 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31// See http://www.softwareishard.com/blog/har-12-spec/ 32// for HAR specification. 33 34// FIXME: Some fields are not yet supported due to back-end limitations. 35// See https://bugs.webkit.org/show_bug.cgi?id=58127 for details. 36 37/** 38 * @constructor 39 * @param {!WebInspector.NetworkRequest} request 40 */ 41WebInspector.HAREntry = function(request) 42{ 43 this._request = request; 44} 45 46WebInspector.HAREntry.prototype = { 47 /** 48 * @return {!Object} 49 */ 50 build: function() 51 { 52 var entry = { 53 startedDateTime: new Date(this._request.startTime * 1000), 54 time: this._request.timing ? WebInspector.HAREntry._toMilliseconds(this._request.duration) : 0, 55 request: this._buildRequest(), 56 response: this._buildResponse(), 57 cache: { }, // Not supported yet. 58 timings: this._buildTimings() 59 }; 60 61 if (this._request.connectionId) 62 entry.connection = String(this._request.connectionId); 63 var page = this._request.target().networkLog.pageLoadForRequest(this._request); 64 if (page) 65 entry.pageref = "page_" + page.id; 66 return entry; 67 }, 68 69 /** 70 * @return {!Object} 71 */ 72 _buildRequest: function() 73 { 74 var headersText = this._request.requestHeadersText(); 75 var res = { 76 method: this._request.requestMethod, 77 url: this._buildRequestURL(this._request.url), 78 httpVersion: this._request.requestHttpVersion(), 79 headers: this._request.requestHeaders(), 80 queryString: this._buildParameters(this._request.queryParameters || []), 81 cookies: this._buildCookies(this._request.requestCookies || []), 82 headersSize: headersText ? headersText.length : -1, 83 bodySize: this.requestBodySize 84 }; 85 if (this._request.requestFormData) 86 res.postData = this._buildPostData(); 87 88 return res; 89 }, 90 91 /** 92 * @return {!Object} 93 */ 94 _buildResponse: function() 95 { 96 var headersText = this._request.responseHeadersText; 97 return { 98 status: this._request.statusCode, 99 statusText: this._request.statusText, 100 httpVersion: this._request.responseHttpVersion, 101 headers: this._request.responseHeaders, 102 cookies: this._buildCookies(this._request.responseCookies || []), 103 content: this._buildContent(), 104 redirectURL: this._request.responseHeaderValue("Location") || "", 105 headersSize: headersText ? headersText.length : -1, 106 bodySize: this.responseBodySize, 107 _error: this._request.localizedFailDescription 108 }; 109 }, 110 111 /** 112 * @return {!Object} 113 */ 114 _buildContent: function() 115 { 116 var content = { 117 size: this._request.resourceSize, 118 mimeType: this._request.mimeType || "x-unknown", 119 // text: this._request.content // TODO: pull out into a boolean flag, as content can be huge (and needs to be requested with an async call) 120 }; 121 var compression = this.responseCompression; 122 if (typeof compression === "number") 123 content.compression = compression; 124 return content; 125 }, 126 127 /** 128 * @return {!Object} 129 */ 130 _buildTimings: function() 131 { 132 // Order of events: request_start = 0, [proxy], [dns], [connect [ssl]], [send], receive_headers_end 133 // HAR 'blocked' time is time before first network activity. 134 135 var timing = this._request.timing; 136 if (!timing) 137 return {blocked: -1, dns: -1, connect: -1, send: 0, wait: 0, receive: 0, ssl: -1}; 138 139 function firstNonNegative(values) 140 { 141 for (var i = 0; i < values.length; ++i) { 142 if (values[i] >= 0) 143 return values[i]; 144 } 145 console.assert(false, "Incomplete requet timing information."); 146 } 147 148 var blocked = firstNonNegative([timing.dnsStart, timing.connectStart, timing.sendStart]); 149 150 var dns = -1; 151 if (timing.dnsStart >= 0) 152 dns = firstNonNegative([timing.connectStart, timing.sendStart]) - timing.dnsStart; 153 154 var connect = -1; 155 if (timing.connectStart >= 0) 156 connect = timing.sendStart - timing.connectStart; 157 158 var send = timing.sendEnd - timing.sendStart; 159 var wait = timing.receiveHeadersEnd - timing.sendEnd; 160 var receive = WebInspector.HAREntry._toMilliseconds(this._request.duration) - timing.receiveHeadersEnd; 161 162 var ssl = -1; 163 if (timing.sslStart >= 0 && timing.sslEnd >= 0) 164 ssl = timing.sslEnd - timing.sslStart; 165 166 return {blocked: blocked, dns: dns, connect: connect, send: send, wait: wait, receive: receive, ssl: ssl}; 167 }, 168 169 /** 170 * @return {!Object} 171 */ 172 _buildPostData: function() 173 { 174 var res = { 175 mimeType: this._request.requestContentType(), 176 text: this._request.requestFormData 177 }; 178 if (this._request.formParameters) 179 res.params = this._buildParameters(this._request.formParameters); 180 return res; 181 }, 182 183 /** 184 * @param {!Array.<!Object>} parameters 185 * @return {!Array.<!Object>} 186 */ 187 _buildParameters: function(parameters) 188 { 189 return parameters.slice(); 190 }, 191 192 /** 193 * @param {string} url 194 * @return {string} 195 */ 196 _buildRequestURL: function(url) 197 { 198 return url.split("#", 2)[0]; 199 }, 200 201 /** 202 * @param {!Array.<!WebInspector.Cookie>} cookies 203 * @return {!Array.<!Object>} 204 */ 205 _buildCookies: function(cookies) 206 { 207 return cookies.map(this._buildCookie.bind(this)); 208 }, 209 210 /** 211 * @param {!WebInspector.Cookie} cookie 212 * @return {!Object} 213 */ 214 _buildCookie: function(cookie) 215 { 216 return { 217 name: cookie.name(), 218 value: cookie.value(), 219 path: cookie.path(), 220 domain: cookie.domain(), 221 expires: cookie.expiresDate(new Date(this._request.startTime * 1000)), 222 httpOnly: cookie.httpOnly(), 223 secure: cookie.secure() 224 }; 225 }, 226 227 /** 228 * @return {number} 229 */ 230 get requestBodySize() 231 { 232 return !this._request.requestFormData ? 0 : this._request.requestFormData.length; 233 }, 234 235 /** 236 * @return {number} 237 */ 238 get responseBodySize() 239 { 240 if (this._request.cached || this._request.statusCode === 304) 241 return 0; 242 if (!this._request.responseHeadersText) 243 return -1; 244 return this._request.transferSize - this._request.responseHeadersText.length; 245 }, 246 247 /** 248 * @return {number|undefined} 249 */ 250 get responseCompression() 251 { 252 if (this._request.cached || this._request.statusCode === 304 || this._request.statusCode === 206) 253 return; 254 if (!this._request.responseHeadersText) 255 return; 256 return this._request.resourceSize - this.responseBodySize; 257 } 258} 259 260/** 261 * @param {number} time 262 * @return {number} 263 */ 264WebInspector.HAREntry._toMilliseconds = function(time) 265{ 266 return time === -1 ? -1 : time * 1000; 267} 268 269/** 270 * @constructor 271 * @param {!Array.<!WebInspector.NetworkRequest>} requests 272 */ 273WebInspector.HARLog = function(requests) 274{ 275 this._requests = requests; 276} 277 278WebInspector.HARLog.prototype = { 279 /** 280 * @return {!Object} 281 */ 282 build: function() 283 { 284 return { 285 version: "1.2", 286 creator: this._creator(), 287 pages: this._buildPages(), 288 entries: this._requests.map(this._convertResource.bind(this)) 289 } 290 }, 291 292 _creator: function() 293 { 294 var webKitVersion = /AppleWebKit\/([^ ]+)/.exec(window.navigator.userAgent); 295 296 return { 297 name: "WebInspector", 298 version: webKitVersion ? webKitVersion[1] : "n/a" 299 }; 300 }, 301 302 /** 303 * @return {!Array.<!Object>} 304 */ 305 _buildPages: function() 306 { 307 var seenIdentifiers = {}; 308 var pages = []; 309 for (var i = 0; i < this._requests.length; ++i) { 310 var page = this._requests[i].target().networkLog.pageLoadForRequest(this._requests[i]); 311 if (!page || seenIdentifiers[page.id]) 312 continue; 313 seenIdentifiers[page.id] = true; 314 pages.push(this._convertPage(page)); 315 } 316 return pages; 317 }, 318 319 /** 320 * @param {!WebInspector.PageLoad} page 321 * @return {!Object} 322 */ 323 _convertPage: function(page) 324 { 325 return { 326 startedDateTime: new Date(page.startTime * 1000), 327 id: "page_" + page.id, 328 title: page.url, // We don't have actual page title here. URL is probably better than nothing. 329 pageTimings: { 330 onContentLoad: this._pageEventTime(page, page.contentLoadTime), 331 onLoad: this._pageEventTime(page, page.loadTime) 332 } 333 } 334 }, 335 336 /** 337 * @param {!WebInspector.NetworkRequest} request 338 * @return {!Object} 339 */ 340 _convertResource: function(request) 341 { 342 return (new WebInspector.HAREntry(request)).build(); 343 }, 344 345 /** 346 * @param {!WebInspector.PageLoad} page 347 * @param {number} time 348 * @return {number} 349 */ 350 _pageEventTime: function(page, time) 351 { 352 var startTime = page.startTime; 353 if (time === -1 || startTime === -1) 354 return -1; 355 return WebInspector.HAREntry._toMilliseconds(time - startTime); 356 } 357} 358 359/** 360 * @constructor 361 */ 362WebInspector.HARWriter = function() 363{ 364} 365 366WebInspector.HARWriter.prototype = { 367 /** 368 * @param {!WebInspector.OutputStream} stream 369 * @param {!Array.<!WebInspector.NetworkRequest>} requests 370 * @param {!WebInspector.Progress} progress 371 */ 372 write: function(stream, requests, progress) 373 { 374 this._stream = stream; 375 this._harLog = (new WebInspector.HARLog(requests)).build(); 376 this._pendingRequests = 1; // Guard against completing resource transfer before all requests are made. 377 var entries = this._harLog.entries; 378 for (var i = 0; i < entries.length; ++i) { 379 var content = requests[i].content; 380 if (typeof content === "undefined" && requests[i].finished) { 381 ++this._pendingRequests; 382 requests[i].requestContent(this._onContentAvailable.bind(this, entries[i])); 383 } else if (content !== null) 384 entries[i].response.content.text = content; 385 } 386 var compositeProgress = new WebInspector.CompositeProgress(progress); 387 this._writeProgress = compositeProgress.createSubProgress(); 388 if (--this._pendingRequests) { 389 this._requestsProgress = compositeProgress.createSubProgress(); 390 this._requestsProgress.setTitle(WebInspector.UIString("Collecting content…")); 391 this._requestsProgress.setTotalWork(this._pendingRequests); 392 } else 393 this._beginWrite(); 394 }, 395 396 /** 397 * @param {!Object} entry 398 * @param {?string} content 399 */ 400 _onContentAvailable: function(entry, content) 401 { 402 if (content !== null) 403 entry.response.content.text = content; 404 if (this._requestsProgress) 405 this._requestsProgress.worked(); 406 if (!--this._pendingRequests) { 407 this._requestsProgress.done(); 408 this._beginWrite(); 409 } 410 }, 411 412 _beginWrite: function() 413 { 414 const jsonIndent = 2; 415 this._text = JSON.stringify({log: this._harLog}, null, jsonIndent); 416 this._writeProgress.setTitle(WebInspector.UIString("Writing file…")); 417 this._writeProgress.setTotalWork(this._text.length); 418 this._bytesWritten = 0; 419 this._writeNextChunk(this._stream); 420 }, 421 422 /** 423 * @param {!WebInspector.OutputStream} stream 424 * @param {string=} error 425 */ 426 _writeNextChunk: function(stream, error) 427 { 428 if (this._bytesWritten >= this._text.length || error) { 429 stream.close(); 430 this._writeProgress.done(); 431 return; 432 } 433 const chunkSize = 100000; 434 var text = this._text.substring(this._bytesWritten, this._bytesWritten + chunkSize); 435 this._bytesWritten += text.length; 436 stream.write(text, this._writeNextChunk.bind(this)); 437 this._writeProgress.setWorked(this._bytesWritten); 438 } 439} 440