Home | History | Annotate | Download | only in network
      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 !== "0")
     62             entry.connection = 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  */
    264 WebInspector.HAREntry._toMilliseconds = function(time)
    265 {
    266     return time === -1 ? -1 : time * 1000;
    267 }
    268 
    269 /**
    270  * @constructor
    271  * @param {!Array.<!WebInspector.NetworkRequest>} requests
    272  */
    273 WebInspector.HARLog = function(requests)
    274 {
    275     this._requests = requests;
    276 }
    277 
    278 WebInspector.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  */
    362 WebInspector.HARWriter = function()
    363 {
    364 }
    365 
    366 WebInspector.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