diff -r 000000000000 -r 6474c204b198 toolkit/devtools/webconsole/network-monitor.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolkit/devtools/webconsole/network-monitor.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,1501 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {Cc, Ci, Cu} = require("chrome"); + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +loader.lazyGetter(this, "NetworkHelper", () => require("devtools/toolkit/webconsole/network-helper")); +loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm"); +loader.lazyImporter(this, "DevToolsUtils", "resource://gre/modules/devtools/DevToolsUtils.jsm"); +loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); +loader.lazyImporter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); +loader.lazyServiceGetter(this, "gActivityDistributor", + "@mozilla.org/network/http-activity-distributor;1", + "nsIHttpActivityDistributor"); + +/////////////////////////////////////////////////////////////////////////////// +// Network logging +/////////////////////////////////////////////////////////////////////////////// + +// The maximum uint32 value. +const PR_UINT32_MAX = 4294967295; + +// HTTP status codes. +const HTTP_MOVED_PERMANENTLY = 301; +const HTTP_FOUND = 302; +const HTTP_SEE_OTHER = 303; +const HTTP_TEMPORARY_REDIRECT = 307; + +// The maximum number of bytes a NetworkResponseListener can hold. +const RESPONSE_BODY_LIMIT = 1048576; // 1 MB + +/** + * The network response listener implements the nsIStreamListener and + * nsIRequestObserver interfaces. This is used within the NetworkMonitor feature + * to get the response body of the request. + * + * The code is mostly based on code listings from: + * + * http://www.softwareishard.com/blog/firebug/ + * nsitraceablechannel-intercept-http-traffic/ + * + * @constructor + * @param object aOwner + * The response listener owner. This object needs to hold the + * |openResponses| object. + * @param object aHttpActivity + * HttpActivity object associated with this request. See NetworkMonitor + * for more information. + */ +function NetworkResponseListener(aOwner, aHttpActivity) +{ + this.owner = aOwner; + this.receivedData = ""; + this.httpActivity = aHttpActivity; + this.bodySize = 0; +} +exports.NetworkResponseListener = NetworkResponseListener; + +NetworkResponseListener.prototype = { + QueryInterface: + XPCOMUtils.generateQI([Ci.nsIStreamListener, Ci.nsIInputStreamCallback, + Ci.nsIRequestObserver, Ci.nsISupports]), + + /** + * This NetworkResponseListener tracks the NetworkMonitor.openResponses object + * to find the associated uncached headers. + * @private + */ + _foundOpenResponse: false, + + /** + * The response listener owner. + */ + owner: null, + + /** + * The response will be written into the outputStream of this nsIPipe. + * Both ends of the pipe must be blocking. + */ + sink: null, + + /** + * The HttpActivity object associated with this response. + */ + httpActivity: null, + + /** + * Stores the received data as a string. + */ + receivedData: null, + + /** + * The network response body size. + */ + bodySize: null, + + /** + * The nsIRequest we are started for. + */ + request: null, + + /** + * Set the async listener for the given nsIAsyncInputStream. This allows us to + * wait asynchronously for any data coming from the stream. + * + * @param nsIAsyncInputStream aStream + * The input stream from where we are waiting for data to come in. + * @param nsIInputStreamCallback aListener + * The input stream callback you want. This is an object that must have + * the onInputStreamReady() method. If the argument is null, then the + * current callback is removed. + * @return void + */ + setAsyncListener: function NRL_setAsyncListener(aStream, aListener) + { + // Asynchronously wait for the stream to be readable or closed. + aStream.asyncWait(aListener, 0, 0, Services.tm.mainThread); + }, + + /** + * Stores the received data, if request/response body logging is enabled. It + * also does limit the number of stored bytes, based on the + * RESPONSE_BODY_LIMIT constant. + * + * Learn more about nsIStreamListener at: + * https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIStreamListener + * + * @param nsIRequest aRequest + * @param nsISupports aContext + * @param nsIInputStream aInputStream + * @param unsigned long aOffset + * @param unsigned long aCount + */ + onDataAvailable: + function NRL_onDataAvailable(aRequest, aContext, aInputStream, aOffset, aCount) + { + this._findOpenResponse(); + let data = NetUtil.readInputStreamToString(aInputStream, aCount); + + this.bodySize += aCount; + + if (!this.httpActivity.discardResponseBody && + this.receivedData.length < RESPONSE_BODY_LIMIT) { + this.receivedData += NetworkHelper. + convertToUnicode(data, aRequest.contentCharset); + } + }, + + /** + * See documentation at + * https://developer.mozilla.org/En/NsIRequestObserver + * + * @param nsIRequest aRequest + * @param nsISupports aContext + */ + onStartRequest: function NRL_onStartRequest(aRequest) + { + this.request = aRequest; + this._findOpenResponse(); + // Asynchronously wait for the data coming from the request. + this.setAsyncListener(this.sink.inputStream, this); + }, + + /** + * Handle the onStopRequest by closing the sink output stream. + * + * For more documentation about nsIRequestObserver go to: + * https://developer.mozilla.org/En/NsIRequestObserver + */ + onStopRequest: function NRL_onStopRequest() + { + this._findOpenResponse(); + this.sink.outputStream.close(); + }, + + /** + * Find the open response object associated to the current request. The + * NetworkMonitor._httpResponseExaminer() method saves the response headers in + * NetworkMonitor.openResponses. This method takes the data from the open + * response object and puts it into the HTTP activity object, then sends it to + * the remote Web Console instance. + * + * @private + */ + _findOpenResponse: function NRL__findOpenResponse() + { + if (!this.owner || this._foundOpenResponse) { + return; + } + + let openResponse = null; + + for each (let item in this.owner.openResponses) { + if (item.channel === this.httpActivity.channel) { + openResponse = item; + break; + } + } + + if (!openResponse) { + return; + } + this._foundOpenResponse = true; + + delete this.owner.openResponses[openResponse.id]; + + this.httpActivity.owner.addResponseHeaders(openResponse.headers); + this.httpActivity.owner.addResponseCookies(openResponse.cookies); + }, + + /** + * Clean up the response listener once the response input stream is closed. + * This is called from onStopRequest() or from onInputStreamReady() when the + * stream is closed. + * @return void + */ + onStreamClose: function NRL_onStreamClose() + { + if (!this.httpActivity) { + return; + } + // Remove our listener from the request input stream. + this.setAsyncListener(this.sink.inputStream, null); + + this._findOpenResponse(); + + if (!this.httpActivity.discardResponseBody && this.receivedData.length) { + this._onComplete(this.receivedData); + } + else if (!this.httpActivity.discardResponseBody && + this.httpActivity.responseStatus == 304) { + // Response is cached, so we load it from cache. + let charset = this.request.contentCharset || this.httpActivity.charset; + NetworkHelper.loadFromCache(this.httpActivity.url, charset, + this._onComplete.bind(this)); + } + else { + this._onComplete(); + } + }, + + /** + * Handler for when the response completes. This function cleans up the + * response listener. + * + * @param string [aData] + * Optional, the received data coming from the response listener or + * from the cache. + */ + _onComplete: function NRL__onComplete(aData) + { + let response = { + mimeType: "", + text: aData || "", + }; + + response.size = response.text.length; + + try { + response.mimeType = this.request.contentType; + } + catch (ex) { } + + if (!response.mimeType || !NetworkHelper.isTextMimeType(response.mimeType)) { + response.encoding = "base64"; + response.text = btoa(response.text); + } + + if (response.mimeType && this.request.contentCharset) { + response.mimeType += "; charset=" + this.request.contentCharset; + } + + this.receivedData = ""; + + this.httpActivity.owner. + addResponseContent(response, this.httpActivity.discardResponseBody); + + this.httpActivity.channel = null; + this.httpActivity.owner = null; + this.httpActivity = null; + this.sink = null; + this.inputStream = null; + this.request = null; + this.owner = null; + }, + + /** + * The nsIInputStreamCallback for when the request input stream is ready - + * either it has more data or it is closed. + * + * @param nsIAsyncInputStream aStream + * The sink input stream from which data is coming. + * @returns void + */ + onInputStreamReady: function NRL_onInputStreamReady(aStream) + { + if (!(aStream instanceof Ci.nsIAsyncInputStream) || !this.httpActivity) { + return; + } + + let available = -1; + try { + // This may throw if the stream is closed normally or due to an error. + available = aStream.available(); + } + catch (ex) { } + + if (available != -1) { + if (available != 0) { + // Note that passing 0 as the offset here is wrong, but the + // onDataAvailable() method does not use the offset, so it does not + // matter. + this.onDataAvailable(this.request, null, aStream, 0, available); + } + this.setAsyncListener(aStream, this); + } + else { + this.onStreamClose(); + } + }, +}; // NetworkResponseListener.prototype + + +/** + * The network monitor uses the nsIHttpActivityDistributor to monitor network + * requests. The nsIObserverService is also used for monitoring + * http-on-examine-response notifications. All network request information is + * routed to the remote Web Console. + * + * @constructor + * @param object aFilters + * Object with the filters to use for network requests: + * - window (nsIDOMWindow): filter network requests by the associated + * window object. + * - appId (number): filter requests by the appId. + * - topFrame (nsIDOMElement): filter requests by their topFrameElement. + * Filters are optional. If any of these filters match the request is + * logged (OR is applied). If no filter is provided then all requests are + * logged. + * @param object aOwner + * The network monitor owner. This object needs to hold: + * - onNetworkEvent(aRequestInfo, aChannel, aNetworkMonitor). + * This method is invoked once for every new network request and it is + * given the following arguments: the initial network request + * information, and the channel. The third argument is the NetworkMonitor + * instance. + * onNetworkEvent() must return an object which holds several add*() + * methods which are used to add further network request/response + * information. + */ +function NetworkMonitor(aFilters, aOwner) +{ + if (aFilters) { + this.window = aFilters.window; + this.appId = aFilters.appId; + this.topFrame = aFilters.topFrame; + } + if (!this.window && !this.appId && !this.topFrame) { + this._logEverything = true; + } + this.owner = aOwner; + this.openRequests = {}; + this.openResponses = {}; + this._httpResponseExaminer = + DevToolsUtils.makeInfallible(this._httpResponseExaminer).bind(this); +} +exports.NetworkMonitor = NetworkMonitor; + +NetworkMonitor.prototype = { + _logEverything: false, + window: null, + appId: null, + topFrame: null, + + httpTransactionCodes: { + 0x5001: "REQUEST_HEADER", + 0x5002: "REQUEST_BODY_SENT", + 0x5003: "RESPONSE_START", + 0x5004: "RESPONSE_HEADER", + 0x5005: "RESPONSE_COMPLETE", + 0x5006: "TRANSACTION_CLOSE", + + 0x804b0003: "STATUS_RESOLVING", + 0x804b000b: "STATUS_RESOLVED", + 0x804b0007: "STATUS_CONNECTING_TO", + 0x804b0004: "STATUS_CONNECTED_TO", + 0x804b0005: "STATUS_SENDING_TO", + 0x804b000a: "STATUS_WAITING_FOR", + 0x804b0006: "STATUS_RECEIVING_FROM" + }, + + // Network response bodies are piped through a buffer of the given size (in + // bytes). + responsePipeSegmentSize: null, + + owner: null, + + /** + * Whether to save the bodies of network requests and responses. Disabled by + * default to save memory. + * @type boolean + */ + saveRequestAndResponseBodies: false, + + /** + * Object that holds the HTTP activity objects for ongoing requests. + */ + openRequests: null, + + /** + * Object that holds response headers coming from this._httpResponseExaminer. + */ + openResponses: null, + + /** + * The network monitor initializer. + */ + init: function NM_init() + { + this.responsePipeSegmentSize = Services.prefs + .getIntPref("network.buffer.cache.size"); + + gActivityDistributor.addObserver(this); + + if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) { + Services.obs.addObserver(this._httpResponseExaminer, + "http-on-examine-response", false); + } + }, + + /** + * Observe notifications for the http-on-examine-response topic, coming from + * the nsIObserverService. + * + * @private + * @param nsIHttpChannel aSubject + * @param string aTopic + * @returns void + */ + _httpResponseExaminer: function NM__httpResponseExaminer(aSubject, aTopic) + { + // The httpResponseExaminer is used to retrieve the uncached response + // headers. The data retrieved is stored in openResponses. The + // NetworkResponseListener is responsible with updating the httpActivity + // object with the data from the new object in openResponses. + + if (!this.owner || aTopic != "http-on-examine-response" || + !(aSubject instanceof Ci.nsIHttpChannel)) { + return; + } + + let channel = aSubject.QueryInterface(Ci.nsIHttpChannel); + + if (!this._matchRequest(channel)) { + return; + } + + let response = { + id: gSequenceId(), + channel: channel, + headers: [], + cookies: [], + }; + + let setCookieHeader = null; + + channel.visitResponseHeaders({ + visitHeader: function NM__visitHeader(aName, aValue) { + let lowerName = aName.toLowerCase(); + if (lowerName == "set-cookie") { + setCookieHeader = aValue; + } + response.headers.push({ name: aName, value: aValue }); + } + }); + + if (!response.headers.length) { + return; // No need to continue. + } + + if (setCookieHeader) { + response.cookies = NetworkHelper.parseSetCookieHeader(setCookieHeader); + } + + // Determine the HTTP version. + let httpVersionMaj = {}; + let httpVersionMin = {}; + + channel.QueryInterface(Ci.nsIHttpChannelInternal); + channel.getResponseVersion(httpVersionMaj, httpVersionMin); + + response.status = channel.responseStatus; + response.statusText = channel.responseStatusText; + response.httpVersion = "HTTP/" + httpVersionMaj.value + "." + + httpVersionMin.value; + + this.openResponses[response.id] = response; + }, + + /** + * Begin observing HTTP traffic that originates inside the current tab. + * + * @see https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIHttpActivityObserver + * + * @param nsIHttpChannel aChannel + * @param number aActivityType + * @param number aActivitySubtype + * @param number aTimestamp + * @param number aExtraSizeData + * @param string aExtraStringData + */ + observeActivity: DevToolsUtils.makeInfallible(function NM_observeActivity(aChannel, aActivityType, aActivitySubtype, aTimestamp, aExtraSizeData, aExtraStringData) + { + if (!this.owner || + aActivityType != gActivityDistributor.ACTIVITY_TYPE_HTTP_TRANSACTION && + aActivityType != gActivityDistributor.ACTIVITY_TYPE_SOCKET_TRANSPORT) { + return; + } + + if (!(aChannel instanceof Ci.nsIHttpChannel)) { + return; + } + + aChannel = aChannel.QueryInterface(Ci.nsIHttpChannel); + + if (aActivitySubtype == + gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_HEADER) { + this._onRequestHeader(aChannel, aTimestamp, aExtraStringData); + return; + } + + // Iterate over all currently ongoing requests. If aChannel can't + // be found within them, then exit this function. + let httpActivity = null; + for each (let item in this.openRequests) { + if (item.channel === aChannel) { + httpActivity = item; + break; + } + } + + if (!httpActivity) { + return; + } + + let transCodes = this.httpTransactionCodes; + + // Store the time information for this activity subtype. + if (aActivitySubtype in transCodes) { + let stage = transCodes[aActivitySubtype]; + if (stage in httpActivity.timings) { + httpActivity.timings[stage].last = aTimestamp; + } + else { + httpActivity.timings[stage] = { + first: aTimestamp, + last: aTimestamp, + }; + } + } + + switch (aActivitySubtype) { + case gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_BODY_SENT: + this._onRequestBodySent(httpActivity); + break; + case gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER: + this._onResponseHeader(httpActivity, aExtraStringData); + break; + case gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE: + this._onTransactionClose(httpActivity); + break; + default: + break; + } + }), + + /** + * Check if a given network request should be logged by this network monitor + * instance based on the current filters. + * + * @private + * @param nsIHttpChannel aChannel + * Request to check. + * @return boolean + * True if the network request should be logged, false otherwise. + */ + _matchRequest: function NM__matchRequest(aChannel) + { + if (this._logEverything) { + return true; + } + + if (this.window) { + let win = NetworkHelper.getWindowForRequest(aChannel); + if (win && win.top === this.window) { + return true; + } + } + + if (this.topFrame) { + let topFrame = NetworkHelper.getTopFrameForRequest(aChannel); + if (topFrame && topFrame === this.topFrame) { + return true; + } + } + + if (this.appId) { + let appId = NetworkHelper.getAppIdForRequest(aChannel); + if (appId && appId == this.appId) { + return true; + } + } + + return false; + }, + + /** + * Handler for ACTIVITY_SUBTYPE_REQUEST_HEADER. When a request starts the + * headers are sent to the server. This method creates the |httpActivity| + * object where we store the request and response information that is + * collected through its lifetime. + * + * @private + * @param nsIHttpChannel aChannel + * @param number aTimestamp + * @param string aExtraStringData + * @return void + */ + _onRequestHeader: + function NM__onRequestHeader(aChannel, aTimestamp, aExtraStringData) + { + if (!this._matchRequest(aChannel)) { + return; + } + + let win = NetworkHelper.getWindowForRequest(aChannel); + let httpActivity = this.createActivityObject(aChannel); + + // see NM__onRequestBodySent() + httpActivity.charset = win ? win.document.characterSet : null; + httpActivity.private = win ? PrivateBrowsingUtils.isWindowPrivate(win) : false; + + httpActivity.timings.REQUEST_HEADER = { + first: aTimestamp, + last: aTimestamp + }; + + let httpVersionMaj = {}; + let httpVersionMin = {}; + let event = {}; + event.startedDateTime = new Date(Math.round(aTimestamp / 1000)).toISOString(); + event.headersSize = aExtraStringData.length; + event.method = aChannel.requestMethod; + event.url = aChannel.URI.spec; + event.private = httpActivity.private; + + // Determine if this is an XHR request. + try { + let callbacks = aChannel.notificationCallbacks; + let xhrRequest = callbacks ? callbacks.getInterface(Ci.nsIXMLHttpRequest) : null; + httpActivity.isXHR = event.isXHR = !!xhrRequest; + } catch (e) { + httpActivity.isXHR = event.isXHR = false; + } + + // Determine the HTTP version. + aChannel.QueryInterface(Ci.nsIHttpChannelInternal); + aChannel.getRequestVersion(httpVersionMaj, httpVersionMin); + + event.httpVersion = "HTTP/" + httpVersionMaj.value + "." + + httpVersionMin.value; + + event.discardRequestBody = !this.saveRequestAndResponseBodies; + event.discardResponseBody = !this.saveRequestAndResponseBodies; + + let headers = []; + let cookies = []; + let cookieHeader = null; + + // Copy the request header data. + aChannel.visitRequestHeaders({ + visitHeader: function NM__visitHeader(aName, aValue) + { + if (aName == "Cookie") { + cookieHeader = aValue; + } + headers.push({ name: aName, value: aValue }); + } + }); + + if (cookieHeader) { + cookies = NetworkHelper.parseCookieHeader(cookieHeader); + } + + httpActivity.owner = this.owner.onNetworkEvent(event, aChannel, this); + + this._setupResponseListener(httpActivity); + + this.openRequests[httpActivity.id] = httpActivity; + + httpActivity.owner.addRequestHeaders(headers); + httpActivity.owner.addRequestCookies(cookies); + }, + + /** + * Create the empty HTTP activity object. This object is used for storing all + * the request and response information. + * + * This is a HAR-like object. Conformance to the spec is not guaranteed at + * this point. + * + * TODO: Bug 708717 - Add support for network log export to HAR + * + * @see http://www.softwareishard.com/blog/har-12-spec + * @param nsIHttpChannel aChannel + * The HTTP channel for which the HTTP activity object is created. + * @return object + * The new HTTP activity object. + */ + createActivityObject: function NM_createActivityObject(aChannel) + { + return { + id: gSequenceId(), + channel: aChannel, + charset: null, // see NM__onRequestHeader() + url: aChannel.URI.spec, + discardRequestBody: !this.saveRequestAndResponseBodies, + discardResponseBody: !this.saveRequestAndResponseBodies, + timings: {}, // internal timing information, see NM_observeActivity() + responseStatus: null, // see NM__onResponseHeader() + owner: null, // the activity owner which is notified when changes happen + }; + }, + + /** + * Setup the network response listener for the given HTTP activity. The + * NetworkResponseListener is responsible for storing the response body. + * + * @private + * @param object aHttpActivity + * The HTTP activity object we are tracking. + */ + _setupResponseListener: function NM__setupResponseListener(aHttpActivity) + { + let channel = aHttpActivity.channel; + channel.QueryInterface(Ci.nsITraceableChannel); + + // The response will be written into the outputStream of this pipe. + // This allows us to buffer the data we are receiving and read it + // asynchronously. + // Both ends of the pipe must be blocking. + let sink = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); + + // The streams need to be blocking because this is required by the + // stream tee. + sink.init(false, false, this.responsePipeSegmentSize, PR_UINT32_MAX, null); + + // Add listener for the response body. + let newListener = new NetworkResponseListener(this, aHttpActivity); + + // Remember the input stream, so it isn't released by GC. + newListener.inputStream = sink.inputStream; + newListener.sink = sink; + + let tee = Cc["@mozilla.org/network/stream-listener-tee;1"]. + createInstance(Ci.nsIStreamListenerTee); + + let originalListener = channel.setNewListener(tee); + + tee.init(originalListener, sink.outputStream, newListener); + }, + + /** + * Handler for ACTIVITY_SUBTYPE_REQUEST_BODY_SENT. The request body is logged + * here. + * + * @private + * @param object aHttpActivity + * The HTTP activity object we are working with. + */ + _onRequestBodySent: function NM__onRequestBodySent(aHttpActivity) + { + if (aHttpActivity.discardRequestBody) { + return; + } + + let sentBody = NetworkHelper. + readPostTextFromRequest(aHttpActivity.channel, + aHttpActivity.charset); + + if (!sentBody && this.window && + aHttpActivity.url == this.window.location.href) { + // If the request URL is the same as the current page URL, then + // we can try to get the posted text from the page directly. + // This check is necessary as otherwise the + // NetworkHelper.readPostTextFromPageViaWebNav() + // function is called for image requests as well but these + // are not web pages and as such don't store the posted text + // in the cache of the webpage. + let webNav = this.window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIWebNavigation); + sentBody = NetworkHelper. + readPostTextFromPageViaWebNav(webNav, aHttpActivity.charset); + } + + if (sentBody) { + aHttpActivity.owner.addRequestPostData({ text: sentBody }); + } + }, + + /** + * Handler for ACTIVITY_SUBTYPE_RESPONSE_HEADER. This method stores + * information about the response headers. + * + * @private + * @param object aHttpActivity + * The HTTP activity object we are working with. + * @param string aExtraStringData + * The uncached response headers. + */ + _onResponseHeader: + function NM__onResponseHeader(aHttpActivity, aExtraStringData) + { + // aExtraStringData contains the uncached response headers. The first line + // contains the response status (e.g. HTTP/1.1 200 OK). + // + // Note: The response header is not saved here. Calling the + // channel.visitResponseHeaders() methood at this point sometimes causes an + // NS_ERROR_NOT_AVAILABLE exception. + // + // We could parse aExtraStringData to get the headers and their values, but + // that is not trivial to do in an accurate manner. Hence, we save the + // response headers in this._httpResponseExaminer(). + + let headers = aExtraStringData.split(/\r\n|\n|\r/); + let statusLine = headers.shift(); + let statusLineArray = statusLine.split(" "); + + let response = {}; + response.httpVersion = statusLineArray.shift(); + response.status = statusLineArray.shift(); + response.statusText = statusLineArray.join(" "); + response.headersSize = aExtraStringData.length; + + aHttpActivity.responseStatus = response.status; + + // Discard the response body for known response statuses. + switch (parseInt(response.status)) { + case HTTP_MOVED_PERMANENTLY: + case HTTP_FOUND: + case HTTP_SEE_OTHER: + case HTTP_TEMPORARY_REDIRECT: + aHttpActivity.discardResponseBody = true; + break; + } + + response.discardResponseBody = aHttpActivity.discardResponseBody; + + aHttpActivity.owner.addResponseStart(response); + }, + + /** + * Handler for ACTIVITY_SUBTYPE_TRANSACTION_CLOSE. This method updates the HAR + * timing information on the HTTP activity object and clears the request + * from the list of known open requests. + * + * @private + * @param object aHttpActivity + * The HTTP activity object we work with. + */ + _onTransactionClose: function NM__onTransactionClose(aHttpActivity) + { + let result = this._setupHarTimings(aHttpActivity); + aHttpActivity.owner.addEventTimings(result.total, result.timings); + delete this.openRequests[aHttpActivity.id]; + }, + + /** + * Update the HTTP activity object to include timing information as in the HAR + * spec. The HTTP activity object holds the raw timing information in + * |timings| - these are timings stored for each activity notification. The + * HAR timing information is constructed based on these lower level data. + * + * @param object aHttpActivity + * The HTTP activity object we are working with. + * @return object + * This object holds two properties: + * - total - the total time for all of the request and response. + * - timings - the HAR timings object. + */ + _setupHarTimings: function NM__setupHarTimings(aHttpActivity) + { + let timings = aHttpActivity.timings; + let harTimings = {}; + + // Not clear how we can determine "blocked" time. + harTimings.blocked = -1; + + // DNS timing information is available only in when the DNS record is not + // cached. + harTimings.dns = timings.STATUS_RESOLVING && timings.STATUS_RESOLVED ? + timings.STATUS_RESOLVED.last - + timings.STATUS_RESOLVING.first : -1; + + if (timings.STATUS_CONNECTING_TO && timings.STATUS_CONNECTED_TO) { + harTimings.connect = timings.STATUS_CONNECTED_TO.last - + timings.STATUS_CONNECTING_TO.first; + } + else if (timings.STATUS_SENDING_TO) { + harTimings.connect = timings.STATUS_SENDING_TO.first - + timings.REQUEST_HEADER.first; + } + else { + harTimings.connect = -1; + } + + if ((timings.STATUS_WAITING_FOR || timings.STATUS_RECEIVING_FROM) && + (timings.STATUS_CONNECTED_TO || timings.STATUS_SENDING_TO)) { + harTimings.send = (timings.STATUS_WAITING_FOR || + timings.STATUS_RECEIVING_FROM).first - + (timings.STATUS_CONNECTED_TO || + timings.STATUS_SENDING_TO).last; + } + else { + harTimings.send = -1; + } + + if (timings.RESPONSE_START) { + harTimings.wait = timings.RESPONSE_START.first - + (timings.REQUEST_BODY_SENT || + timings.STATUS_SENDING_TO).last; + } + else { + harTimings.wait = -1; + } + + if (timings.RESPONSE_START && timings.RESPONSE_COMPLETE) { + harTimings.receive = timings.RESPONSE_COMPLETE.last - + timings.RESPONSE_START.first; + } + else { + harTimings.receive = -1; + } + + let totalTime = 0; + for (let timing in harTimings) { + let time = Math.max(Math.round(harTimings[timing] / 1000), -1); + harTimings[timing] = time; + if (time > -1) { + totalTime += time; + } + } + + return { + total: totalTime, + timings: harTimings, + }; + }, + + /** + * Suspend Web Console activity. This is called when all Web Consoles are + * closed. + */ + destroy: function NM_destroy() + { + if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) { + Services.obs.removeObserver(this._httpResponseExaminer, + "http-on-examine-response"); + } + + gActivityDistributor.removeObserver(this); + + this.openRequests = {}; + this.openResponses = {}; + this.owner = null; + this.window = null; + this.topFrame = null; + }, +}; // NetworkMonitor.prototype + + +/** + * The NetworkMonitorChild is used to proxy all of the network activity of the + * child app process from the main process. The child WebConsoleActor creates an + * instance of this object. + * + * Network requests for apps happen in the main process. As such, + * a NetworkMonitor instance is used by the WebappsActor in the main process to + * log the network requests for this child process. + * + * The main process creates NetworkEventActorProxy instances per request. These + * send the data to this object using the nsIMessageManager. Here we proxy the + * data to the WebConsoleActor or to a NetworkEventActor. + * + * @constructor + * @param number appId + * The web appId of the child process. + * @param nsIMessageManager messageManager + * The nsIMessageManager to use to communicate with the parent process. + * @param string connID + * The connection ID to use for send messages to the parent process. + * @param object owner + * The WebConsoleActor that is listening for the network requests. + */ +function NetworkMonitorChild(appId, messageManager, connID, owner) { + this.appId = appId; + this.connID = connID; + this.owner = owner; + this._messageManager = messageManager; + this._onNewEvent = this._onNewEvent.bind(this); + this._onUpdateEvent = this._onUpdateEvent.bind(this); + this._netEvents = new Map(); +} +exports.NetworkMonitorChild = NetworkMonitorChild; + +NetworkMonitorChild.prototype = { + appId: null, + owner: null, + _netEvents: null, + _saveRequestAndResponseBodies: false, + + get saveRequestAndResponseBodies() { + return this._saveRequestAndResponseBodies; + }, + + set saveRequestAndResponseBodies(val) { + this._saveRequestAndResponseBodies = val; + + this._messageManager.sendAsyncMessage("debug:netmonitor:" + this.connID, { + appId: this.appId, + action: "setPreferences", + preferences: { + saveRequestAndResponseBodies: this._saveRequestAndResponseBodies, + }, + }); + }, + + init: function() { + let mm = this._messageManager; + mm.addMessageListener("debug:netmonitor:" + this.connID + ":newEvent", + this._onNewEvent); + mm.addMessageListener("debug:netmonitor:" + this.connID + ":updateEvent", + this._onUpdateEvent); + mm.sendAsyncMessage("debug:netmonitor:" + this.connID, { + appId: this.appId, + action: "start", + }); + }, + + _onNewEvent: DevToolsUtils.makeInfallible(function _onNewEvent(msg) { + let {id, event} = msg.data; + let actor = this.owner.onNetworkEvent(event); + this._netEvents.set(id, Cu.getWeakReference(actor)); + }), + + _onUpdateEvent: DevToolsUtils.makeInfallible(function _onUpdateEvent(msg) { + let {id, method, args} = msg.data; + let weakActor = this._netEvents.get(id); + let actor = weakActor ? weakActor.get() : null; + if (!actor) { + Cu.reportError("Received debug:netmonitor:updateEvent for unknown event ID: " + id); + return; + } + if (!(method in actor)) { + Cu.reportError("Received debug:netmonitor:updateEvent unsupported method: " + method); + return; + } + actor[method].apply(actor, args); + }), + + destroy: function() { + let mm = this._messageManager; + mm.removeMessageListener("debug:netmonitor:" + this.connID + ":newEvent", + this._onNewEvent); + mm.removeMessageListener("debug:netmonitor:" + this.connID + ":updateEvent", + this._onUpdateEvent); + mm.sendAsyncMessage("debug:netmonitor:" + this.connID, { + action: "disconnect", + }); + this._netEvents.clear(); + this._messageManager = null; + this.owner = null; + }, +}; // NetworkMonitorChild.prototype + +/** + * The NetworkEventActorProxy is used to send network request information from + * the main process to the child app process. One proxy is used per request. + * Similarly, one NetworkEventActor in the child app process is used per + * request. The client receives all network logs from the child actors. + * + * The child process has a NetworkMonitorChild instance that is listening for + * all network logging from the main process. The net monitor shim is used to + * proxy the data to the WebConsoleActor instance of the child process. + * + * @constructor + * @param nsIMessageManager messageManager + * The message manager for the child app process. This is used for + * communication with the NetworkMonitorChild instance of the process. + * @param string connID + * The connection ID to use to send messages to the child process. + */ +function NetworkEventActorProxy(messageManager, connID) { + this.id = gSequenceId(); + this.connID = connID; + this.messageManager = messageManager; +} +exports.NetworkEventActorProxy = NetworkEventActorProxy; + +NetworkEventActorProxy.methodFactory = function(method) { + return DevToolsUtils.makeInfallible(function() { + let args = Array.slice(arguments); + let mm = this.messageManager; + mm.sendAsyncMessage("debug:netmonitor:" + this.connID + ":updateEvent", { + id: this.id, + method: method, + args: args, + }); + }, "NetworkEventActorProxy." + method); +}; + +NetworkEventActorProxy.prototype = { + /** + * Initialize the network event. This method sends the network request event + * to the content process. + * + * @param object event + * Object describing the network request. + * @return object + * This object. + */ + init: DevToolsUtils.makeInfallible(function(event) + { + let mm = this.messageManager; + mm.sendAsyncMessage("debug:netmonitor:" + this.connID + ":newEvent", { + id: this.id, + event: event, + }); + return this; + }), +}; + +(function() { + // Listeners for new network event data coming from the NetworkMonitor. + let methods = ["addRequestHeaders", "addRequestCookies", "addRequestPostData", + "addResponseStart", "addResponseHeaders", "addResponseCookies", + "addResponseContent", "addEventTimings"]; + let factory = NetworkEventActorProxy.methodFactory; + for (let method of methods) { + NetworkEventActorProxy.prototype[method] = factory(method); + } +})(); + + +/** + * The NetworkMonitor manager used by the Webapps actor in the main process. + * This object uses the message manager to listen for requests from the child + * process to start/stop the network monitor. + * + * @constructor + * @param nsIDOMElement frame + * The browser frame to work with (mozbrowser). + * @param string id + * Instance identifier to use for messages. + */ +function NetworkMonitorManager(frame, id) +{ + this.id = id; + let mm = frame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager; + this.messageManager = mm; + this.frame = frame; + this.onNetMonitorMessage = this.onNetMonitorMessage.bind(this); + this.onNetworkEvent = this.onNetworkEvent.bind(this); + + mm.addMessageListener("debug:netmonitor:" + id, this.onNetMonitorMessage); +} +exports.NetworkMonitorManager = NetworkMonitorManager; + +NetworkMonitorManager.prototype = { + netMonitor: null, + frame: null, + messageManager: null, + + /** + * Handler for "debug:monitor" messages received through the message manager + * from the content process. + * + * @param object msg + * Message from the content. + */ + onNetMonitorMessage: DevToolsUtils.makeInfallible(function _onNetMonitorMessage(msg) { + let { action, appId } = msg.json; + // Pipe network monitor data from parent to child via the message manager. + switch (action) { + case "start": + if (!this.netMonitor) { + this.netMonitor = new NetworkMonitor({ + topFrame: this.frame, + appId: appId, + }, this); + this.netMonitor.init(); + } + break; + + case "setPreferences": { + let {preferences} = msg.json; + for (let key of Object.keys(preferences)) { + if (key == "saveRequestAndResponseBodies" && this.netMonitor) { + this.netMonitor.saveRequestAndResponseBodies = preferences[key]; + } + } + break; + } + + case "stop": + if (this.netMonitor) { + this.netMonitor.destroy(); + this.netMonitor = null; + } + break; + + case "disconnect": + this.destroy(); + break; + } + }), + + /** + * Handler for new network requests. This method is invoked by the current + * NetworkMonitor instance. + * + * @param object event + * Object describing the network request. + * @return object + * A NetworkEventActorProxy instance which is notified when further + * data about the request is available. + */ + onNetworkEvent: DevToolsUtils.makeInfallible(function _onNetworkEvent(event) { + return new NetworkEventActorProxy(this.messageManager, this.id).init(event); + }), + + destroy: function() + { + if (this.messageManager) { + this.messageManager.removeMessageListener("debug:netmonitor:" + this.id, + this.onNetMonitorMessage); + } + this.messageManager = null; + this.filters = null; + + if (this.netMonitor) { + this.netMonitor.destroy(); + this.netMonitor = null; + } + }, +}; // NetworkMonitorManager.prototype + + +/** + * A WebProgressListener that listens for location changes. + * + * This progress listener is used to track file loads and other kinds of + * location changes. + * + * @constructor + * @param object aWindow + * The window for which we need to track location changes. + * @param object aOwner + * The listener owner which needs to implement two methods: + * - onFileActivity(aFileURI) + * - onLocationChange(aState, aTabURI, aPageTitle) + */ +function ConsoleProgressListener(aWindow, aOwner) +{ + this.window = aWindow; + this.owner = aOwner; +} +exports.ConsoleProgressListener = ConsoleProgressListener; + +ConsoleProgressListener.prototype = { + /** + * Constant used for startMonitor()/stopMonitor() that tells you want to + * monitor file loads. + */ + MONITOR_FILE_ACTIVITY: 1, + + /** + * Constant used for startMonitor()/stopMonitor() that tells you want to + * monitor page location changes. + */ + MONITOR_LOCATION_CHANGE: 2, + + /** + * Tells if you want to monitor file activity. + * @private + * @type boolean + */ + _fileActivity: false, + + /** + * Tells if you want to monitor location changes. + * @private + * @type boolean + */ + _locationChange: false, + + /** + * Tells if the console progress listener is initialized or not. + * @private + * @type boolean + */ + _initialized: false, + + _webProgress: null, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference]), + + /** + * Initialize the ConsoleProgressListener. + * @private + */ + _init: function CPL__init() + { + if (this._initialized) { + return; + } + + this._webProgress = this.window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIWebProgress); + this._webProgress.addProgressListener(this, + Ci.nsIWebProgress.NOTIFY_STATE_ALL); + + this._initialized = true; + }, + + /** + * Start a monitor/tracker related to the current nsIWebProgressListener + * instance. + * + * @param number aMonitor + * Tells what you want to track. Available constants: + * - this.MONITOR_FILE_ACTIVITY + * Track file loads. + * - this.MONITOR_LOCATION_CHANGE + * Track location changes for the top window. + */ + startMonitor: function CPL_startMonitor(aMonitor) + { + switch (aMonitor) { + case this.MONITOR_FILE_ACTIVITY: + this._fileActivity = true; + break; + case this.MONITOR_LOCATION_CHANGE: + this._locationChange = true; + break; + default: + throw new Error("ConsoleProgressListener: unknown monitor type " + + aMonitor + "!"); + } + this._init(); + }, + + /** + * Stop a monitor. + * + * @param number aMonitor + * Tells what you want to stop tracking. See this.startMonitor() for + * the list of constants. + */ + stopMonitor: function CPL_stopMonitor(aMonitor) + { + switch (aMonitor) { + case this.MONITOR_FILE_ACTIVITY: + this._fileActivity = false; + break; + case this.MONITOR_LOCATION_CHANGE: + this._locationChange = false; + break; + default: + throw new Error("ConsoleProgressListener: unknown monitor type " + + aMonitor + "!"); + } + + if (!this._fileActivity && !this._locationChange) { + this.destroy(); + } + }, + + onStateChange: + function CPL_onStateChange(aProgress, aRequest, aState, aStatus) + { + if (!this.owner) { + return; + } + + if (this._fileActivity) { + this._checkFileActivity(aProgress, aRequest, aState, aStatus); + } + + if (this._locationChange) { + this._checkLocationChange(aProgress, aRequest, aState, aStatus); + } + }, + + /** + * Check if there is any file load, given the arguments of + * nsIWebProgressListener.onStateChange. If the state change tells that a file + * URI has been loaded, then the remote Web Console instance is notified. + * @private + */ + _checkFileActivity: + function CPL__checkFileActivity(aProgress, aRequest, aState, aStatus) + { + if (!(aState & Ci.nsIWebProgressListener.STATE_START)) { + return; + } + + let uri = null; + if (aRequest instanceof Ci.imgIRequest) { + let imgIRequest = aRequest.QueryInterface(Ci.imgIRequest); + uri = imgIRequest.URI; + } + else if (aRequest instanceof Ci.nsIChannel) { + let nsIChannel = aRequest.QueryInterface(Ci.nsIChannel); + uri = nsIChannel.URI; + } + + if (!uri || !uri.schemeIs("file") && !uri.schemeIs("ftp")) { + return; + } + + this.owner.onFileActivity(uri.spec); + }, + + /** + * Check if the current window.top location is changing, given the arguments + * of nsIWebProgressListener.onStateChange. If that is the case, the remote + * Web Console instance is notified. + * @private + */ + _checkLocationChange: + function CPL__checkLocationChange(aProgress, aRequest, aState, aStatus) + { + let isStart = aState & Ci.nsIWebProgressListener.STATE_START; + let isStop = aState & Ci.nsIWebProgressListener.STATE_STOP; + let isNetwork = aState & Ci.nsIWebProgressListener.STATE_IS_NETWORK; + let isWindow = aState & Ci.nsIWebProgressListener.STATE_IS_WINDOW; + + // Skip non-interesting states. + if (!isNetwork || !isWindow || aProgress.DOMWindow != this.window) { + return; + } + + if (isStart && aRequest instanceof Ci.nsIChannel) { + this.owner.onLocationChange("start", aRequest.URI.spec, ""); + } + else if (isStop) { + this.owner.onLocationChange("stop", this.window.location.href, + this.window.document.title); + } + }, + + onLocationChange: function() {}, + onStatusChange: function() {}, + onProgressChange: function() {}, + onSecurityChange: function() {}, + + /** + * Destroy the ConsoleProgressListener. + */ + destroy: function CPL_destroy() + { + if (!this._initialized) { + return; + } + + this._initialized = false; + this._fileActivity = false; + this._locationChange = false; + + try { + this._webProgress.removeProgressListener(this); + } + catch (ex) { + // This can throw during browser shutdown. + } + + this._webProgress = null; + this.window = null; + this.owner = null; + }, +}; // ConsoleProgressListener.prototype + +function gSequenceId() { return gSequenceId.n++; } +gSequenceId.n = 1;