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 */ 41 WebInspector.HAREntry = function(request) 42 { 43 this._request = request; 44 } 45 46 WebInspector.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 = WebInspector.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 return { 97 status: this._request.statusCode, 98 statusText: this._request.statusText, 99 httpVersion: this._request.responseHttpVersion, 100 headers: this._request.responseHeaders, 101 cookies: this._buildCookies(this._request.responseCookies || []), 102 content: this._buildContent(), 103 redirectURL: this._request.responseHeaderValue("Location") || "", 104 headersSize: this._request.responseHeadersSize, 105 bodySize: this.responseBodySize 106 }; 107 }, 108 109 /** 110 * @return {!Object} 111 */ 112 _buildContent: function() 113 { 114 var content = { 115 size: this._request.resourceSize, 116 mimeType: this._request.mimeType, 117 // 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) 118 }; 119 var compression = this.responseCompression; 120 if (typeof compression === "number") 121 content.compression = compression; 122 return content; 123 }, 124 125 /** 126 * @return {!Object} 127 */ 128 _buildTimings: function() 129 { 130 // Order of events: request_start = 0, [proxy], [dns], [connect [ssl]], [send], receive_headers_end 131 // HAR 'blocked' time is time before first network activity. 132 133 var timing = this._request.timing; 134 if (!timing) 135 return {blocked: -1, dns: -1, connect: -1, send: 0, wait: 0, receive: 0, ssl: -1}; 136 137 function firstNonNegative(values) 138 { 139 for (var i = 0; i < values.length; ++i) { 140 if (values[i] >= 0) 141 return values[i]; 142 } 143 console.assert(false, "Incomplete requet timing information."); 144 } 145 146 var blocked = firstNonNegative([timing.dnsStart, timing.connectStart, timing.sendStart]); 147 148 var dns = -1; 149 if (timing.dnsStart >= 0) 150 dns = firstNonNegative([timing.connectStart, timing.sendStart]) - timing.dnsStart; 151 152 var connect = -1; 153 if (timing.connectStart >= 0) 154 connect = timing.sendStart - timing.connectStart; 155 156 var send = timing.sendEnd - timing.sendStart; 157 var wait = timing.receiveHeadersEnd - timing.sendEnd; 158 var receive = WebInspector.HAREntry._toMilliseconds(this._request.duration) - timing.receiveHeadersEnd; 159 160 var ssl = -1; 161 if (timing.sslStart >= 0 && timing.sslEnd >= 0) 162 ssl = timing.sslEnd - timing.sslStart; 163 164 return {blocked: blocked, dns: dns, connect: connect, send: send, wait: wait, receive: receive, ssl: ssl}; 165 }, 166 167 /** 168 * @return {!Object} 169 */ 170 _buildPostData: function() 171 { 172 var res = { 173 mimeType: this._request.requestContentType(), 174 text: this._request.requestFormData 175 }; 176 if (this._request.formParameters) 177 res.params = this._buildParameters(this._request.formParameters); 178 return res; 179 }, 180 181 /** 182 * @param {!Array.<!Object>} parameters 183 * @return {!Array.<!Object>} 184 */ 185 _buildParameters: function(parameters) 186 { 187 return parameters.slice(); 188 }, 189 190 /** 191 * @param {string} url 192 * @return {string} 193 */ 194 _buildRequestURL: function(url) 195 { 196 return url.split("#", 2)[0]; 197 }, 198 199 /** 200 * @param {!Array.<!WebInspector.Cookie>} cookies 201 * @return {!Array.<!Object>} 202 */ 203 _buildCookies: function(cookies) 204 { 205 return cookies.map(this._buildCookie.bind(this)); 206 }, 207 208 /** 209 * @param {!WebInspector.Cookie} cookie 210 * @return {!Object} 211 */ 212 _buildCookie: function(cookie) 213 { 214 return { 215 name: cookie.name(), 216 value: cookie.value(), 217 path: cookie.path(), 218 domain: cookie.domain(), 219 expires: cookie.expiresDate(new Date(this._request.startTime * 1000)), 220 httpOnly: cookie.httpOnly(), 221 secure: cookie.secure() 222 }; 223 }, 224 225 /** 226 * @return {number} 227 */ 228 get requestBodySize() 229 { 230 return !this._request.requestFormData ? 0 : this._request.requestFormData.length; 231 }, 232 233 /** 234 * @return {number} 235 */ 236 get responseBodySize() 237 { 238 if (this._request.cached || this._request.statusCode === 304) 239 return 0; 240 return this._request.transferSize - this._request.responseHeadersSize; 241 }, 242 243 /** 244 * @return {number|undefined} 245 */ 246 get responseCompression() 247 { 248 if (this._request.cached || this._request.statusCode === 304 || this._request.statusCode === 206) 249 return; 250 return this._request.resourceSize - this.responseBodySize; 251 } 252 } 253 254 /** 255 * @param {number} time 256 * @return {number} 257 */ 258 WebInspector.HAREntry._toMilliseconds = function(time) 259 { 260 return time === -1 ? -1 : time * 1000; 261 } 262 263 /** 264 * @constructor 265 * @param {!Array.<!WebInspector.NetworkRequest>} requests 266 */ 267 WebInspector.HARLog = function(requests) 268 { 269 this._requests = requests; 270 } 271 272 WebInspector.HARLog.prototype = { 273 /** 274 * @return {!Object} 275 */ 276 build: function() 277 { 278 return { 279 version: "1.2", 280 creator: this._creator(), 281 pages: this._buildPages(), 282 entries: this._requests.map(this._convertResource.bind(this)) 283 } 284 }, 285 286 _creator: function() 287 { 288 var webKitVersion = /AppleWebKit\/([^ ]+)/.exec(window.navigator.userAgent); 289 290 return { 291 name: "WebInspector", 292 version: webKitVersion ? webKitVersion[1] : "n/a" 293 }; 294 }, 295 296 /** 297 * @return {!Array.<!Object>} 298 */ 299 _buildPages: function() 300 { 301 var seenIdentifiers = {}; 302 var pages = []; 303 for (var i = 0; i < this._requests.length; ++i) { 304 var page = WebInspector.networkLog.pageLoadForRequest(this._requests[i]); 305 if (!page || seenIdentifiers[page.id]) 306 continue; 307 seenIdentifiers[page.id] = true; 308 pages.push(this._convertPage(page)); 309 } 310 return pages; 311 }, 312 313 /** 314 * @param {!WebInspector.PageLoad} page 315 * @return {!Object} 316 */ 317 _convertPage: function(page) 318 { 319 return { 320 startedDateTime: new Date(page.startTime * 1000), 321 id: "page_" + page.id, 322 title: page.url, // We don't have actual page title here. URL is probably better than nothing. 323 pageTimings: { 324 onContentLoad: this._pageEventTime(page, page.contentLoadTime), 325 onLoad: this._pageEventTime(page, page.loadTime) 326 } 327 } 328 }, 329 330 /** 331 * @param {!WebInspector.NetworkRequest} request 332 * @return {!Object} 333 */ 334 _convertResource: function(request) 335 { 336 return (new WebInspector.HAREntry(request)).build(); 337 }, 338 339 /** 340 * @param {!WebInspector.PageLoad} page 341 * @param {number} time 342 * @return {number} 343 */ 344 _pageEventTime: function(page, time) 345 { 346 var startTime = page.startTime; 347 if (time === -1 || startTime === -1) 348 return -1; 349 return WebInspector.HAREntry._toMilliseconds(time - startTime); 350 } 351 } 352 353 /** 354 * @constructor 355 */ 356 WebInspector.HARWriter = function() 357 { 358 } 359 360 WebInspector.HARWriter.prototype = { 361 /** 362 * @param {!WebInspector.OutputStream} stream 363 * @param {!Array.<!WebInspector.NetworkRequest>} requests 364 * @param {!WebInspector.Progress} progress 365 */ 366 write: function(stream, requests, progress) 367 { 368 this._stream = stream; 369 this._harLog = (new WebInspector.HARLog(requests)).build(); 370 this._pendingRequests = 1; // Guard against completing resource transfer before all requests are made. 371 var entries = this._harLog.entries; 372 for (var i = 0; i < entries.length; ++i) { 373 var content = requests[i].content; 374 if (typeof content === "undefined" && requests[i].finished) { 375 ++this._pendingRequests; 376 requests[i].requestContent(this._onContentAvailable.bind(this, entries[i])); 377 } else if (content !== null) 378 entries[i].response.content.text = content; 379 } 380 var compositeProgress = new WebInspector.CompositeProgress(progress); 381 this._writeProgress = compositeProgress.createSubProgress(); 382 if (--this._pendingRequests) { 383 this._requestsProgress = compositeProgress.createSubProgress(); 384 this._requestsProgress.setTitle(WebInspector.UIString("Collecting content")); 385 this._requestsProgress.setTotalWork(this._pendingRequests); 386 } else 387 this._beginWrite(); 388 }, 389 390 /** 391 * @param {!Object} entry 392 * @param {?string} content 393 */ 394 _onContentAvailable: function(entry, content) 395 { 396 if (content !== null) 397 entry.response.content.text = content; 398 if (this._requestsProgress) 399 this._requestsProgress.worked(); 400 if (!--this._pendingRequests) { 401 this._requestsProgress.done(); 402 this._beginWrite(); 403 } 404 }, 405 406 _beginWrite: function() 407 { 408 const jsonIndent = 2; 409 this._text = JSON.stringify({log: this._harLog}, null, jsonIndent); 410 this._writeProgress.setTitle(WebInspector.UIString("Writing file")); 411 this._writeProgress.setTotalWork(this._text.length); 412 this._bytesWritten = 0; 413 this._writeNextChunk(this._stream); 414 }, 415 416 /** 417 * @param {!WebInspector.OutputStream} stream 418 * @param {string=} error 419 */ 420 _writeNextChunk: function(stream, error) 421 { 422 if (this._bytesWritten >= this._text.length || error) { 423 stream.close(); 424 this._writeProgress.done(); 425 return; 426 } 427 const chunkSize = 100000; 428 var text = this._text.substring(this._bytesWritten, this._bytesWritten + chunkSize); 429 this._bytesWritten += text.length; 430 stream.write(text, this._writeNextChunk.bind(this)); 431 this._writeProgress.setWorked(this._bytesWritten); 432 } 433 } 434