• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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