summaryrefslogtreecommitdiffstats
path: root/devtools/shared/webconsole/network-monitor.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/webconsole/network-monitor.js')
-rw-r--r--devtools/shared/webconsole/network-monitor.js2044
1 files changed, 2044 insertions, 0 deletions
diff --git a/devtools/shared/webconsole/network-monitor.js b/devtools/shared/webconsole/network-monitor.js
new file mode 100644
index 000000000..084493432
--- /dev/null
+++ b/devtools/shared/webconsole/network-monitor.js
@@ -0,0 +1,2044 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft= javascript ts=2 et sw=2 tw=80: */
+/* 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, Cm, Cu, Cr, components} = require("chrome");
+const Services = require("Services");
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+
+loader.lazyRequireGetter(this, "NetworkHelper",
+ "devtools/shared/webconsole/network-helper");
+loader.lazyRequireGetter(this, "DevToolsUtils",
+ "devtools/shared/DevToolsUtils");
+loader.lazyRequireGetter(this, "flags",
+ "devtools/shared/flags");
+loader.lazyRequireGetter(this, "DebuggerServer",
+ "devtools/server/main", true);
+loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
+loader.lazyServiceGetter(this, "gActivityDistributor",
+ "@mozilla.org/network/http-activity-distributor;1",
+ "nsIHttpActivityDistributor");
+const {NetworkThrottleManager} = require("devtools/shared/webconsole/throttle");
+
+// 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: 1 MB
+const RESPONSE_BODY_LIMIT = 1048576;
+// Exported for testing.
+exports.RESPONSE_BODY_LIMIT = RESPONSE_BODY_LIMIT;
+
+/**
+ * Check if a given network request should be logged by a network monitor
+ * based on the specified filters.
+ *
+ * @param nsIHttpChannel channel
+ * Request to check.
+ * @param filters
+ * NetworkMonitor filters to match against.
+ * @return boolean
+ * True if the network request should be logged, false otherwise.
+ */
+function matchRequest(channel, filters) {
+ // Log everything if no filter is specified
+ if (!filters.outerWindowID && !filters.window && !filters.appId) {
+ return true;
+ }
+
+ // Ignore requests from chrome or add-on code when we are monitoring
+ // content.
+ // TODO: one particular test (browser_styleeditor_fetch-from-cache.js) needs
+ // the flags.testing check. We will move to a better way to serve
+ // its needs in bug 1167188, where this check should be removed.
+ if (!flags.testing && channel.loadInfo &&
+ channel.loadInfo.loadingDocument === null &&
+ channel.loadInfo.loadingPrincipal ===
+ Services.scriptSecurityManager.getSystemPrincipal()) {
+ return false;
+ }
+
+ if (filters.window) {
+ // Since frames support, this.window may not be the top level content
+ // frame, so that we can't only compare with win.top.
+ let win = NetworkHelper.getWindowForRequest(channel);
+ while (win) {
+ if (win == filters.window) {
+ return true;
+ }
+ if (win.parent == win) {
+ break;
+ }
+ win = win.parent;
+ }
+ }
+
+ if (filters.outerWindowID) {
+ let topFrame = NetworkHelper.getTopFrameForRequest(channel);
+ if (topFrame && topFrame.outerWindowID &&
+ topFrame.outerWindowID == filters.outerWindowID) {
+ return true;
+ }
+ }
+
+ if (filters.appId) {
+ let appId = NetworkHelper.getAppIdForRequest(channel);
+ if (appId && appId == filters.appId) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * This is a nsIChannelEventSink implementation that monitors channel redirects and
+ * informs the registered StackTraceCollector about the old and new channels.
+ */
+const SINK_CLASS_DESCRIPTION = "NetworkMonitor Channel Event Sink";
+const SINK_CLASS_ID = components.ID("{e89fa076-c845-48a8-8c45-2604729eba1d}");
+const SINK_CONTRACT_ID = "@mozilla.org/network/monitor/channeleventsink;1";
+const SINK_CATEGORY_NAME = "net-channel-event-sinks";
+
+function ChannelEventSink() {
+ this.wrappedJSObject = this;
+ this.collectors = new Set();
+}
+
+ChannelEventSink.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannelEventSink]),
+
+ registerCollector(collector) {
+ this.collectors.add(collector);
+ },
+
+ unregisterCollector(collector) {
+ this.collectors.delete(collector);
+
+ if (this.collectors.size == 0) {
+ ChannelEventSinkFactory.unregister();
+ }
+ },
+
+ asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
+ for (let collector of this.collectors) {
+ try {
+ collector.onChannelRedirect(oldChannel, newChannel, flags);
+ } catch (ex) {
+ console.error("StackTraceCollector.onChannelRedirect threw an exception", ex);
+ }
+ }
+ callback.onRedirectVerifyCallback(Cr.NS_OK);
+ }
+};
+
+const ChannelEventSinkFactory = XPCOMUtils.generateSingletonFactory(ChannelEventSink);
+
+ChannelEventSinkFactory.register = function () {
+ const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+ if (registrar.isCIDRegistered(SINK_CLASS_ID)) {
+ return;
+ }
+
+ registrar.registerFactory(SINK_CLASS_ID,
+ SINK_CLASS_DESCRIPTION,
+ SINK_CONTRACT_ID,
+ ChannelEventSinkFactory);
+
+ XPCOMUtils.categoryManager.addCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID,
+ SINK_CONTRACT_ID, false, true);
+};
+
+ChannelEventSinkFactory.unregister = function () {
+ const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.unregisterFactory(SINK_CLASS_ID, ChannelEventSinkFactory);
+
+ XPCOMUtils.categoryManager.deleteCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID,
+ false);
+};
+
+ChannelEventSinkFactory.getService = function () {
+ // Make sure the ChannelEventSink service is registered before accessing it
+ ChannelEventSinkFactory.register();
+
+ return Cc[SINK_CONTRACT_ID].getService(Ci.nsIChannelEventSink).wrappedJSObject;
+};
+
+function StackTraceCollector(filters) {
+ this.filters = filters;
+ this.stacktracesById = new Map();
+}
+
+StackTraceCollector.prototype = {
+ init() {
+ Services.obs.addObserver(this, "http-on-opening-request", false);
+ ChannelEventSinkFactory.getService().registerCollector(this);
+ },
+
+ destroy() {
+ Services.obs.removeObserver(this, "http-on-opening-request");
+ ChannelEventSinkFactory.getService().unregisterCollector(this);
+ },
+
+ _saveStackTrace(channel, stacktrace) {
+ this.stacktracesById.set(channel.channelId, stacktrace);
+ },
+
+ observe(subject) {
+ let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+
+ if (!matchRequest(channel, this.filters)) {
+ return;
+ }
+
+ // Convert the nsIStackFrame XPCOM objects to a nice JSON that can be
+ // passed around through message managers etc.
+ let frame = components.stack;
+ let stacktrace = [];
+ if (frame && frame.caller) {
+ frame = frame.caller;
+ while (frame) {
+ stacktrace.push({
+ filename: frame.filename,
+ lineNumber: frame.lineNumber,
+ columnNumber: frame.columnNumber,
+ functionName: frame.name,
+ asyncCause: frame.asyncCause,
+ });
+ frame = frame.caller || frame.asyncCaller;
+ }
+ }
+
+ this._saveStackTrace(channel, stacktrace);
+ },
+
+ onChannelRedirect(oldChannel, newChannel, flags) {
+ // We can be called with any nsIChannel, but are interested only in HTTP channels
+ try {
+ oldChannel.QueryInterface(Ci.nsIHttpChannel);
+ newChannel.QueryInterface(Ci.nsIHttpChannel);
+ } catch (ex) {
+ return;
+ }
+
+ let oldId = oldChannel.channelId;
+ let stacktrace = this.stacktracesById.get(oldId);
+ if (stacktrace) {
+ this.stacktracesById.delete(oldId);
+ this._saveStackTrace(newChannel, stacktrace);
+ }
+ },
+
+ getStackTrace(channelId) {
+ let trace = this.stacktracesById.get(channelId);
+ this.stacktracesById.delete(channelId);
+ return trace;
+ }
+};
+
+exports.StackTraceCollector = StackTraceCollector;
+
+/**
+ * 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 owner
+ * The response listener owner. This object needs to hold the
+ * |openResponses| object.
+ * @param object httpActivity
+ * HttpActivity object associated with this request. See NetworkMonitor
+ * for more information.
+ */
+function NetworkResponseListener(owner, httpActivity) {
+ this.owner = owner;
+ this.receivedData = "";
+ this.httpActivity = httpActivity;
+ this.bodySize = 0;
+ // Note that this is really only needed for the non-e10s case.
+ // See bug 1309523.
+ let channel = this.httpActivity.channel;
+ this._wrappedNotificationCallbacks = channel.notificationCallbacks;
+ channel.notificationCallbacks = this;
+}
+
+NetworkResponseListener.prototype = {
+ QueryInterface:
+ XPCOMUtils.generateQI([Ci.nsIStreamListener, Ci.nsIInputStreamCallback,
+ Ci.nsIRequestObserver, Ci.nsIInterfaceRequestor,
+ Ci.nsISupports]),
+
+ // nsIInterfaceRequestor implementation
+
+ /**
+ * This object implements nsIProgressEventSink, but also needs to forward
+ * interface requests to the notification callbacks of other objects.
+ */
+ getInterface(iid) {
+ if (iid.equals(Ci.nsIProgressEventSink)) {
+ return this;
+ }
+ if (this._wrappedNotificationCallbacks) {
+ return this._wrappedNotificationCallbacks.getInterface(iid);
+ }
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+
+ /**
+ * Forward notifications for interfaces this object implements, in case other
+ * objects also implemented them.
+ */
+ _forwardNotification(iid, method, args) {
+ if (!this._wrappedNotificationCallbacks) {
+ return;
+ }
+ try {
+ let impl = this._wrappedNotificationCallbacks.getInterface(iid);
+ impl[method].apply(impl, args);
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_NO_INTERFACE) {
+ throw e;
+ }
+ }
+ },
+
+ /**
+ * This NetworkResponseListener tracks the NetworkMonitor.openResponses object
+ * to find the associated uncached headers.
+ * @private
+ */
+ _foundOpenResponse: false,
+
+ /**
+ * If the channel already had notificationCallbacks, hold them here internally
+ * so that we can forward getInterface requests to that object.
+ */
+ _wrappedNotificationCallbacks: null,
+
+ /**
+ * 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 uncompressed, decoded response body size.
+ */
+ bodySize: null,
+
+ /**
+ * Response body size on the wire, potentially compressed / encoded.
+ */
+ transferredSize: 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 stream
+ * The input stream from where we are waiting for data to come in.
+ * @param nsIInputStreamCallback listener
+ * 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 (stream, listener) {
+ // Asynchronously wait for the stream to be readable or closed.
+ stream.asyncWait(listener, 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 request
+ * @param nsISupports context
+ * @param nsIInputStream inputStream
+ * @param unsigned long offset
+ * @param unsigned long count
+ */
+ onDataAvailable: function (request, context, inputStream, offset, count) {
+ this._findOpenResponse();
+ let data = NetUtil.readInputStreamToString(inputStream, count);
+
+ this.bodySize += count;
+
+ if (!this.httpActivity.discardResponseBody &&
+ this.receivedData.length < RESPONSE_BODY_LIMIT) {
+ this.receivedData +=
+ NetworkHelper.convertToUnicode(data, request.contentCharset);
+ }
+ },
+
+ /**
+ * See documentation at
+ * https://developer.mozilla.org/En/NsIRequestObserver
+ *
+ * @param nsIRequest request
+ * @param nsISupports context
+ */
+ onStartRequest: function (request) {
+ // Converter will call this again, we should just ignore that.
+ if (this.request) {
+ return;
+ }
+
+ this.request = request;
+ this._getSecurityInfo();
+ this._findOpenResponse();
+ // We need to track the offset for the onDataAvailable calls where
+ // we pass the data from our pipe to the converter.
+ this.offset = 0;
+
+ // In the multi-process mode, the conversion happens on the child
+ // side while we can only monitor the channel on the parent
+ // side. If the content is gzipped, we have to unzip it
+ // ourself. For that we use the stream converter services. Do not
+ // do that for Service workers as they are run in the child
+ // process.
+ let channel = this.request;
+ if (!this.httpActivity.fromServiceWorker &&
+ channel instanceof Ci.nsIEncodedChannel &&
+ channel.contentEncodings &&
+ !channel.applyConversion) {
+ let encodingHeader = channel.getResponseHeader("Content-Encoding");
+ let scs = Cc["@mozilla.org/streamConverters;1"]
+ .getService(Ci.nsIStreamConverterService);
+ let encodings = encodingHeader.split(/\s*\t*,\s*\t*/);
+ let nextListener = this;
+ let acceptedEncodings = ["gzip", "deflate", "br", "x-gzip", "x-deflate"];
+ for (let i in encodings) {
+ // There can be multiple conversions applied
+ let enc = encodings[i].toLowerCase();
+ if (acceptedEncodings.indexOf(enc) > -1) {
+ this.converter = scs.asyncConvertData(enc, "uncompressed",
+ nextListener, null);
+ nextListener = this.converter;
+ }
+ }
+ if (this.converter) {
+ this.converter.onStartRequest(this.request, null);
+ }
+ }
+ // Asynchronously wait for the data coming from the request.
+ this.setAsyncListener(this.sink.inputStream, this);
+ },
+
+ /**
+ * Parse security state of this request and report it to the client.
+ */
+ _getSecurityInfo: DevToolsUtils.makeInfallible(function () {
+ // Many properties of the securityInfo (e.g., the server certificate or HPKP
+ // status) are not available in the content process and can't be even touched safely,
+ // because their C++ getters trigger assertions. This function is called in content
+ // process for synthesized responses from service workers, in the parent otherwise.
+ if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
+ return;
+ }
+
+ // Take the security information from the original nsIHTTPChannel instead of
+ // the nsIRequest received in onStartRequest. If response to this request
+ // was a redirect from http to https, the request object seems to contain
+ // security info for the https request after redirect.
+ let secinfo = this.httpActivity.channel.securityInfo;
+ let info = NetworkHelper.parseSecurityInfo(secinfo, this.httpActivity);
+
+ this.httpActivity.owner.addSecurityInfo(info);
+ }),
+
+ /**
+ * Handle the onStopRequest by closing the sink output stream.
+ *
+ * For more documentation about nsIRequestObserver go to:
+ * https://developer.mozilla.org/En/NsIRequestObserver
+ */
+ onStopRequest: function () {
+ this._findOpenResponse();
+ this.sink.outputStream.close();
+ },
+
+ // nsIProgressEventSink implementation
+
+ /**
+ * Handle progress event as data is transferred. This is used to record the
+ * size on the wire, which may be compressed / encoded.
+ */
+ onProgress: function (request, context, progress, progressMax) {
+ this.transferredSize = progress;
+ // Need to forward as well to keep things like Download Manager's progress
+ // bar working properly.
+ this._forwardNotification(Ci.nsIProgressEventSink, "onProgress", arguments);
+ },
+
+ onStatus: function () {
+ this._forwardNotification(Ci.nsIProgressEventSink, "onStatus", arguments);
+ },
+
+ /**
+ * 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 () {
+ if (!this.owner || this._foundOpenResponse) {
+ return;
+ }
+
+ let openResponse = null;
+
+ for (let id in this.owner.openResponses) {
+ let item = this.owner.openResponses[id];
+ 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 () {
+ 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 [data]
+ * Optional, the received data coming from the response listener or
+ * from the cache.
+ */
+ _onComplete: function (data) {
+ let response = {
+ mimeType: "",
+ text: data || "",
+ };
+
+ response.size = this.bodySize;
+ response.transferredSize = this.transferredSize;
+
+ try {
+ response.mimeType = this.request.contentType;
+ } catch (ex) {
+ // Ignore.
+ }
+
+ if (!response.mimeType ||
+ !NetworkHelper.isTextMimeType(response.mimeType)) {
+ response.encoding = "base64";
+ try {
+ response.text = btoa(response.text);
+ } catch (err) {
+ // Ignore.
+ }
+ }
+
+ if (response.mimeType && this.request.contentCharset) {
+ response.mimeType += "; charset=" + this.request.contentCharset;
+ }
+
+ this.receivedData = "";
+
+ this.httpActivity.owner.addResponseContent(
+ response,
+ this.httpActivity.discardResponseBody
+ );
+
+ this._wrappedNotificationCallbacks = null;
+ this.httpActivity = null;
+ this.sink = null;
+ this.inputStream = null;
+ this.converter = 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 stream
+ * The sink input stream from which data is coming.
+ * @returns void
+ */
+ onInputStreamReady: function (stream) {
+ if (!(stream 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 = stream.available();
+ } catch (ex) {
+ // Ignore.
+ }
+
+ if (available != -1) {
+ if (available != 0) {
+ if (this.converter) {
+ this.converter.onDataAvailable(this.request, null, stream,
+ this.offset, available);
+ } else {
+ this.onDataAvailable(this.request, null, stream, this.offset,
+ available);
+ }
+ }
+ this.offset += available;
+ this.setAsyncListener(stream, this);
+ } else {
+ this.onStreamClose();
+ this.offset = 0;
+ }
+ },
+};
+
+/**
+ * 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 filters
+ * 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.
+ * - outerWindowID (number): filter requests by their top frame's outerWindowID.
+ * 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 owner
+ * The network monitor owner. This object needs to hold:
+ * - onNetworkEvent(requestInfo)
+ * This method is invoked once for every new network request and it is
+ * given the initial network request information as an argument.
+ * onNetworkEvent() must return an object which holds several add*()
+ * methods which are used to add further network request/response information.
+ * - stackTraceCollector
+ * If the owner has this optional property, it will be used as a
+ * StackTraceCollector by the NetworkMonitor.
+ */
+function NetworkMonitor(filters, owner) {
+ this.filters = filters;
+ this.owner = owner;
+ this.openRequests = {};
+ this.openResponses = {};
+ this._httpResponseExaminer =
+ DevToolsUtils.makeInfallible(this._httpResponseExaminer).bind(this);
+ this._httpModifyExaminer =
+ DevToolsUtils.makeInfallible(this._httpModifyExaminer).bind(this);
+ this._serviceWorkerRequest = this._serviceWorkerRequest.bind(this);
+ this._throttleData = null;
+ this._throttler = null;
+}
+
+exports.NetworkMonitor = NetworkMonitor;
+
+NetworkMonitor.prototype = {
+ filters: 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"
+ },
+
+ httpDownloadActivities: [
+ gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_START,
+ gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER,
+ gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE,
+ gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE
+ ],
+
+ // 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.
+ * @type boolean
+ */
+ saveRequestAndResponseBodies: true,
+
+ /**
+ * 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 () {
+ this.responsePipeSegmentSize = Services.prefs
+ .getIntPref("network.buffer.cache.size");
+ this.interceptedChannels = new Set();
+
+ if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
+ gActivityDistributor.addObserver(this);
+ Services.obs.addObserver(this._httpResponseExaminer,
+ "http-on-examine-response", false);
+ Services.obs.addObserver(this._httpResponseExaminer,
+ "http-on-examine-cached-response", false);
+ Services.obs.addObserver(this._httpModifyExaminer,
+ "http-on-modify-request", false);
+ }
+ // In child processes, only watch for service worker requests
+ // everything else only happens in the parent process
+ Services.obs.addObserver(this._serviceWorkerRequest,
+ "service-worker-synthesized-response", false);
+ },
+
+ get throttleData() {
+ return this._throttleData;
+ },
+
+ set throttleData(value) {
+ this._throttleData = value;
+ // Clear out any existing throttlers
+ this._throttler = null;
+ },
+
+ _getThrottler: function () {
+ if (this.throttleData !== null && this._throttler === null) {
+ this._throttler = new NetworkThrottleManager(this.throttleData);
+ }
+ return this._throttler;
+ },
+
+ _serviceWorkerRequest: function (subject, topic, data) {
+ let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+
+ if (!matchRequest(channel, this.filters)) {
+ return;
+ }
+
+ this.interceptedChannels.add(subject);
+
+ // On e10s, we never receive http-on-examine-cached-response, so fake one.
+ if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
+ this._httpResponseExaminer(channel, "http-on-examine-cached-response");
+ }
+ },
+
+ /**
+ * Observe notifications for the http-on-examine-response topic, coming from
+ * the nsIObserverService.
+ *
+ * @private
+ * @param nsIHttpChannel subject
+ * @param string topic
+ * @returns void
+ */
+ _httpResponseExaminer: function (subject, topic) {
+ // 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 ||
+ (topic != "http-on-examine-response" &&
+ topic != "http-on-examine-cached-response") ||
+ !(subject instanceof Ci.nsIHttpChannel)) {
+ return;
+ }
+
+ let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+
+ if (!matchRequest(channel, this.filters)) {
+ return;
+ }
+
+ let response = {
+ id: gSequenceId(),
+ channel: channel,
+ headers: [],
+ cookies: [],
+ };
+
+ let setCookieHeader = null;
+
+ channel.visitResponseHeaders({
+ visitHeader: function (name, value) {
+ let lowerName = name.toLowerCase();
+ if (lowerName == "set-cookie") {
+ setCookieHeader = value;
+ }
+ response.headers.push({ name: name, value: value });
+ }
+ });
+
+ if (!response.headers.length) {
+ // No need to continue.
+ return;
+ }
+
+ 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;
+
+ if (topic === "http-on-examine-cached-response") {
+ // Service worker requests emits cached-reponse notification on non-e10s,
+ // and we fake one on e10s.
+ let fromServiceWorker = this.interceptedChannels.has(channel);
+ this.interceptedChannels.delete(channel);
+
+ // If this is a cached response, there never was a request event
+ // so we need to construct one here so the frontend gets all the
+ // expected events.
+ let httpActivity = this._createNetworkEvent(channel, {
+ fromCache: !fromServiceWorker,
+ fromServiceWorker: fromServiceWorker
+ });
+ httpActivity.owner.addResponseStart({
+ httpVersion: response.httpVersion,
+ remoteAddress: "",
+ remotePort: "",
+ status: response.status,
+ statusText: response.statusText,
+ headersSize: 0,
+ }, "", true);
+
+ // There also is never any timing events, so we can fire this
+ // event with zeroed out values.
+ let timings = this._setupHarTimings(httpActivity, true);
+ httpActivity.owner.addEventTimings(timings.total, timings.timings);
+ }
+ },
+
+ /**
+ * Observe notifications for the http-on-modify-request topic, coming from
+ * the nsIObserverService.
+ *
+ * @private
+ * @param nsIHttpChannel aSubject
+ * @returns void
+ */
+ _httpModifyExaminer: function (subject) {
+ let throttler = this._getThrottler();
+ if (throttler) {
+ let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ if (matchRequest(channel, this.filters)) {
+ // Read any request body here, before it is throttled.
+ let httpActivity = this.createOrGetActivityObject(channel);
+ this._onRequestBodySent(httpActivity);
+ throttler.manageUpload(channel);
+ }
+ }
+ },
+
+ /**
+ * A helper function for observeActivity. This does whatever work
+ * is required by a particular http activity event. Arguments are
+ * the same as for observeActivity.
+ */
+ _dispatchActivity: function (httpActivity, channel, activityType,
+ activitySubtype, timestamp, extraSizeData,
+ extraStringData) {
+ let transCodes = this.httpTransactionCodes;
+
+ // Store the time information for this activity subtype.
+ if (activitySubtype in transCodes) {
+ let stage = transCodes[activitySubtype];
+ if (stage in httpActivity.timings) {
+ httpActivity.timings[stage].last = timestamp;
+ } else {
+ httpActivity.timings[stage] = {
+ first: timestamp,
+ last: timestamp,
+ };
+ }
+ }
+
+ switch (activitySubtype) {
+ case gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_BODY_SENT:
+ this._onRequestBodySent(httpActivity);
+ if (httpActivity.sentBody !== null) {
+ httpActivity.owner.addRequestPostData({ text: httpActivity.sentBody });
+ httpActivity.sentBody = null;
+ }
+ break;
+ case gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER:
+ this._onResponseHeader(httpActivity, extraStringData);
+ break;
+ case gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE:
+ this._onTransactionClose(httpActivity);
+ break;
+ default:
+ break;
+ }
+ },
+
+ /**
+ * Begin observing HTTP traffic that originates inside the current tab.
+ *
+ * @see https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIHttpActivityObserver
+ *
+ * @param nsIHttpChannel channel
+ * @param number activityType
+ * @param number activitySubtype
+ * @param number timestamp
+ * @param number extraSizeData
+ * @param string extraStringData
+ */
+ observeActivity:
+ DevToolsUtils.makeInfallible(function (channel, activityType, activitySubtype,
+ timestamp, extraSizeData,
+ extraStringData) {
+ if (!this.owner ||
+ activityType != gActivityDistributor.ACTIVITY_TYPE_HTTP_TRANSACTION &&
+ activityType != gActivityDistributor.ACTIVITY_TYPE_SOCKET_TRANSPORT) {
+ return;
+ }
+
+ if (!(channel instanceof Ci.nsIHttpChannel)) {
+ return;
+ }
+
+ channel = channel.QueryInterface(Ci.nsIHttpChannel);
+
+ if (activitySubtype ==
+ gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_HEADER) {
+ this._onRequestHeader(channel, timestamp, extraStringData);
+ return;
+ }
+
+ // Iterate over all currently ongoing requests. If channel can't
+ // be found within them, then exit this function.
+ let httpActivity = this._findActivityObject(channel);
+ if (!httpActivity) {
+ return;
+ }
+
+ // If we're throttling, we must not report events as they arrive
+ // from platform, but instead let the throttler emit the events
+ // after some time has elapsed.
+ if (httpActivity.downloadThrottle &&
+ this.httpDownloadActivities.indexOf(activitySubtype) >= 0) {
+ let callback = this._dispatchActivity.bind(this);
+ httpActivity.downloadThrottle
+ .addActivityCallback(callback, httpActivity, channel, activityType,
+ activitySubtype, timestamp, extraSizeData,
+ extraStringData);
+ } else {
+ this._dispatchActivity(httpActivity, channel, activityType,
+ activitySubtype, timestamp, extraSizeData,
+ extraStringData);
+ }
+ }),
+
+ /**
+ *
+ */
+ _createNetworkEvent: function (channel, { timestamp, extraStringData,
+ fromCache, fromServiceWorker }) {
+ let httpActivity = this.createOrGetActivityObject(channel);
+
+ channel.QueryInterface(Ci.nsIPrivateBrowsingChannel);
+ httpActivity.private = channel.isChannelPrivate;
+
+ if (timestamp) {
+ httpActivity.timings.REQUEST_HEADER = {
+ first: timestamp,
+ last: timestamp
+ };
+ }
+
+ let event = {};
+ event.method = channel.requestMethod;
+ event.channelId = channel.channelId;
+ event.url = channel.URI.spec;
+ event.private = httpActivity.private;
+ event.headersSize = 0;
+ event.startedDateTime =
+ (timestamp ? new Date(Math.round(timestamp / 1000)) : new Date())
+ .toISOString();
+ event.fromCache = fromCache;
+ event.fromServiceWorker = fromServiceWorker;
+ httpActivity.fromServiceWorker = fromServiceWorker;
+
+ if (extraStringData) {
+ event.headersSize = extraStringData.length;
+ }
+
+ // Determine the cause and if this is an XHR request.
+ let causeType = channel.loadInfo.externalContentPolicyType;
+ let loadingPrincipal = channel.loadInfo.loadingPrincipal;
+ let causeUri = loadingPrincipal ? loadingPrincipal.URI : null;
+ let stacktrace;
+ // If this is the parent process, there is no stackTraceCollector - the stack
+ // trace will be added in NetworkMonitorChild._onNewEvent.
+ if (this.owner.stackTraceCollector) {
+ stacktrace = this.owner.stackTraceCollector.getStackTrace(event.channelId);
+ }
+
+ event.cause = {
+ type: causeType,
+ loadingDocumentUri: causeUri ? causeUri.spec : null,
+ stacktrace
+ };
+
+ httpActivity.isXHR = event.isXHR =
+ (causeType === Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST ||
+ causeType === Ci.nsIContentPolicy.TYPE_FETCH);
+
+ // Determine the HTTP version.
+ let httpVersionMaj = {};
+ let httpVersionMin = {};
+ channel.QueryInterface(Ci.nsIHttpChannelInternal);
+ channel.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.
+ channel.visitRequestHeaders({
+ visitHeader: function (name, value) {
+ if (name == "Cookie") {
+ cookieHeader = value;
+ }
+ headers.push({ name: name, value: value });
+ }
+ });
+
+ if (cookieHeader) {
+ cookies = NetworkHelper.parseCookieHeader(cookieHeader);
+ }
+
+ httpActivity.owner = this.owner.onNetworkEvent(event);
+
+ this._setupResponseListener(httpActivity, fromCache);
+
+ httpActivity.owner.addRequestHeaders(headers, extraStringData);
+ httpActivity.owner.addRequestCookies(cookies);
+
+ return httpActivity;
+ },
+
+ /**
+ * 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 channel
+ * @param number timestamp
+ * @param string extraStringData
+ * @return void
+ */
+ _onRequestHeader: function (channel, timestamp, extraStringData) {
+ if (!matchRequest(channel, this.filters)) {
+ return;
+ }
+
+ this._createNetworkEvent(channel, { timestamp, extraStringData });
+ },
+
+ /**
+ * Find an HTTP activity object for the channel.
+ *
+ * @param nsIHttpChannel channel
+ * The HTTP channel whose activity object we want to find.
+ * @return object
+ * The HTTP activity object, or null if it is not found.
+ */
+ _findActivityObject: function (channel) {
+ for (let id in this.openRequests) {
+ let item = this.openRequests[id];
+ if (item.channel === channel) {
+ return item;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Find an existing HTTP activity object, or create a new one. 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.
+ *
+ * @see http://www.softwareishard.com/blog/har-12-spec
+ * @param nsIHttpChannel channel
+ * The HTTP channel for which the HTTP activity object is created.
+ * @return object
+ * The new HTTP activity object.
+ */
+ createOrGetActivityObject: function (channel) {
+ let httpActivity = this._findActivityObject(channel);
+ if (!httpActivity) {
+ let win = NetworkHelper.getWindowForRequest(channel);
+ let charset = win ? win.document.characterSet : null;
+
+ httpActivity = {
+ id: gSequenceId(),
+ channel: channel,
+ // see _onRequestBodySent()
+ charset: charset,
+ sentBody: null,
+ url: channel.URI.spec,
+ // needed for host specific security info
+ hostname: channel.URI.host,
+ discardRequestBody: !this.saveRequestAndResponseBodies,
+ discardResponseBody: !this.saveRequestAndResponseBodies,
+ // internal timing information, see observeActivity()
+ timings: {},
+ // see _onResponseHeader()
+ responseStatus: null,
+ // the activity owner which is notified when changes happen
+ owner: null,
+ };
+
+ this.openRequests[httpActivity.id] = httpActivity;
+ }
+
+ return httpActivity;
+ },
+
+ /**
+ * Setup the network response listener for the given HTTP activity. The
+ * NetworkResponseListener is responsible for storing the response body.
+ *
+ * @private
+ * @param object httpActivity
+ * The HTTP activity object we are tracking.
+ */
+ _setupResponseListener: function (httpActivity, fromCache) {
+ let channel = httpActivity.channel;
+ channel.QueryInterface(Ci.nsITraceableChannel);
+
+ if (!fromCache) {
+ let throttler = this._getThrottler();
+ if (throttler) {
+ httpActivity.downloadThrottle = throttler.manage(channel);
+ }
+ }
+
+ // 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, httpActivity);
+
+ // 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 httpActivity
+ * The HTTP activity object we are working with.
+ */
+ _onRequestBodySent: function (httpActivity) {
+ // Return early if we don't need the request body, or if we've
+ // already found it.
+ if (httpActivity.discardRequestBody || httpActivity.sentBody !== null) {
+ return;
+ }
+
+ let sentBody = NetworkHelper.readPostTextFromRequest(httpActivity.channel,
+ httpActivity.charset);
+
+ if (sentBody !== null && this.window &&
+ httpActivity.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, httpActivity.charset);
+ }
+
+ if (sentBody !== null) {
+ httpActivity.sentBody = sentBody;
+ }
+ },
+
+ /**
+ * Handler for ACTIVITY_SUBTYPE_RESPONSE_HEADER. This method stores
+ * information about the response headers.
+ *
+ * @private
+ * @param object httpActivity
+ * The HTTP activity object we are working with.
+ * @param string extraStringData
+ * The uncached response headers.
+ */
+ _onResponseHeader: function (httpActivity, extraStringData) {
+ // extraStringData 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() method at this point sometimes causes an
+ // NS_ERROR_NOT_AVAILABLE exception.
+ //
+ // We could parse extraStringData 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 = extraStringData.split(/\r\n|\n|\r/);
+ let statusLine = headers.shift();
+ let statusLineArray = statusLine.split(" ");
+
+ let response = {};
+ response.httpVersion = statusLineArray.shift();
+ response.remoteAddress = httpActivity.channel.remoteAddress;
+ response.remotePort = httpActivity.channel.remotePort;
+ response.status = statusLineArray.shift();
+ response.statusText = statusLineArray.join(" ");
+ response.headersSize = extraStringData.length;
+
+ httpActivity.responseStatus = response.status;
+
+ // Discard the response body for known response statuses.
+ switch (parseInt(response.status, 10)) {
+ case HTTP_MOVED_PERMANENTLY:
+ case HTTP_FOUND:
+ case HTTP_SEE_OTHER:
+ case HTTP_TEMPORARY_REDIRECT:
+ httpActivity.discardResponseBody = true;
+ break;
+ }
+
+ response.discardResponseBody = httpActivity.discardResponseBody;
+
+ httpActivity.owner.addResponseStart(response, extraStringData);
+ },
+
+ /**
+ * 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 httpActivity
+ * The HTTP activity object we work with.
+ */
+ _onTransactionClose: function (httpActivity) {
+ let result = this._setupHarTimings(httpActivity);
+ httpActivity.owner.addEventTimings(result.total, result.timings);
+ delete this.openRequests[httpActivity.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 httpActivity
+ * The HTTP activity object we are working with.
+ * @param boolean fromCache
+ * Indicates that the result was returned from the browser cache
+ * @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 (httpActivity, fromCache) {
+ if (fromCache) {
+ // If it came from the browser cache, we have no timing
+ // information and these should all be 0
+ return {
+ total: 0,
+ timings: {
+ blocked: 0,
+ dns: 0,
+ connect: 0,
+ send: 0,
+ wait: 0,
+ receive: 0
+ }
+ };
+ }
+
+ let timings = httpActivity.timings;
+ let harTimings = {};
+
+ if (timings.STATUS_RESOLVING && timings.STATUS_CONNECTING_TO) {
+ harTimings.blocked = timings.STATUS_RESOLVING.first -
+ timings.REQUEST_HEADER.first;
+ } else if (timings.STATUS_SENDING_TO) {
+ harTimings.blocked = timings.STATUS_SENDING_TO.first -
+ timings.REQUEST_HEADER.first;
+ } else {
+ 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 {
+ harTimings.connect = -1;
+ }
+
+ if (timings.STATUS_SENDING_TO) {
+ harTimings.send = timings.STATUS_SENDING_TO.last - timings.STATUS_SENDING_TO.first;
+ } else if (timings.REQUEST_HEADER && timings.REQUEST_BODY_SENT) {
+ harTimings.send = timings.REQUEST_BODY_SENT.last - timings.REQUEST_HEADER.first;
+ } 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 () {
+ if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
+ gActivityDistributor.removeObserver(this);
+ Services.obs.removeObserver(this._httpResponseExaminer,
+ "http-on-examine-response");
+ Services.obs.removeObserver(this._httpResponseExaminer,
+ "http-on-examine-cached-response");
+ Services.obs.removeObserver(this._httpModifyExaminer,
+ "http-on-modify-request", false);
+ }
+
+ Services.obs.removeObserver(this._serviceWorkerRequest,
+ "service-worker-synthesized-response");
+
+ this.interceptedChannels.clear();
+ this.openRequests = {};
+ this.openResponses = {};
+ this.owner = null;
+ this.filters = null;
+ this._throttler = null;
+ },
+};
+
+/**
+ * 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 number outerWindowID
+ * The outerWindowID of the TabActor's main window.
+ * @param nsIMessageManager messageManager
+ * The nsIMessageManager to use to communicate with the parent process.
+ * @param object DebuggerServerConnection
+ * The RDP connection to the client.
+ * @param object owner
+ * The WebConsoleActor that is listening for the network requests.
+ */
+function NetworkMonitorChild(appId, outerWindowID, messageManager, conn, owner) {
+ this.appId = appId;
+ this.outerWindowID = outerWindowID;
+ this.conn = conn;
+ this.owner = owner;
+ this._messageManager = messageManager;
+ this._onNewEvent = this._onNewEvent.bind(this);
+ this._onUpdateEvent = this._onUpdateEvent.bind(this);
+ this._netEvents = new Map();
+ this._msgName = `debug:${this.conn.prefix}netmonitor`;
+}
+
+exports.NetworkMonitorChild = NetworkMonitorChild;
+
+NetworkMonitorChild.prototype = {
+ appId: null,
+ owner: null,
+ _netEvents: null,
+ _saveRequestAndResponseBodies: true,
+ _throttleData: null,
+
+ get saveRequestAndResponseBodies() {
+ return this._saveRequestAndResponseBodies;
+ },
+
+ set saveRequestAndResponseBodies(val) {
+ this._saveRequestAndResponseBodies = val;
+
+ this._messageManager.sendAsyncMessage(this._msgName, {
+ action: "setPreferences",
+ preferences: {
+ saveRequestAndResponseBodies: this._saveRequestAndResponseBodies,
+ },
+ });
+ },
+
+ get throttleData() {
+ return this._throttleData;
+ },
+
+ set throttleData(val) {
+ this._throttleData = val;
+
+ this._messageManager.sendAsyncMessage(this._msgName, {
+ action: "setPreferences",
+ preferences: {
+ throttleData: this._throttleData,
+ },
+ });
+ },
+
+ init: function () {
+ this.conn.setupInParent({
+ module: "devtools/shared/webconsole/network-monitor",
+ setupParent: "setupParentProcess"
+ });
+
+ let mm = this._messageManager;
+ mm.addMessageListener(`${this._msgName}:newEvent`, this._onNewEvent);
+ mm.addMessageListener(`${this._msgName}:updateEvent`, this._onUpdateEvent);
+ mm.sendAsyncMessage(this._msgName, {
+ appId: this.appId,
+ outerWindowID: this.outerWindowID,
+ action: "start",
+ });
+ },
+
+ _onNewEvent: DevToolsUtils.makeInfallible(function _onNewEvent(msg) {
+ let {id, event} = msg.data;
+
+ // Try to add stack trace to the event data received from parent
+ if (this.owner.stackTraceCollector) {
+ event.cause.stacktrace =
+ this.owner.stackTraceCollector.getStackTrace(event.channelId);
+ }
+
+ 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) {
+ console.error(`Received ${this._msgName}:updateEvent for unknown event ID: ${id}`);
+ return;
+ }
+ if (!(method in actor)) {
+ console.error(`Received ${this._msgName}:updateEvent unsupported ` +
+ `method: ${method}`);
+ return;
+ }
+ actor[method].apply(actor, args);
+ }),
+
+ destroy: function () {
+ let mm = this._messageManager;
+ try {
+ mm.removeMessageListener(`${this._msgName}:newEvent`, this._onNewEvent);
+ mm.removeMessageListener(`${this._msgName}:updateEvent`, this._onUpdateEvent);
+ } catch (e) {
+ // On b2g, when registered to a new root docshell,
+ // all message manager functions throw when trying to call them during
+ // message-manager-disconnect event.
+ // As there is no attribute/method on message manager to know
+ // if they are still usable or not, we can only catch the exception...
+ }
+ this._netEvents.clear();
+ this._messageManager = null;
+ this.conn = null;
+ this.owner = null;
+ },
+};
+
+/**
+ * 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 msgName
+ * The message name to be used for this connection.
+ */
+function NetworkEventActorProxy(messageManager, msgName) {
+ this.id = gSequenceId();
+ this.messageManager = messageManager;
+ this._msgName = msgName;
+}
+exports.NetworkEventActorProxy = NetworkEventActorProxy;
+
+NetworkEventActorProxy.methodFactory = function (method) {
+ return DevToolsUtils.makeInfallible(function () {
+ let args = Array.slice(arguments);
+ let mm = this.messageManager;
+ mm.sendAsyncMessage(`${this._msgName}: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(`${this._msgName}: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", "addSecurityInfo", "addResponseHeaders",
+ "addResponseCookies", "addResponseContent", "addEventTimings"];
+ let factory = NetworkEventActorProxy.methodFactory;
+ for (let method of methods) {
+ NetworkEventActorProxy.prototype[method] = factory(method);
+ }
+})();
+
+/**
+ * This is triggered by the child calling `setupInParent` when the child's network monitor
+ * is starting up. This initializes the parent process side of the monitoring.
+ */
+function setupParentProcess({ mm, prefix }) {
+ let networkMonitor = new NetworkMonitorParent(mm, prefix);
+ return {
+ onBrowserSwap: newMM => networkMonitor.setMessageManager(newMM),
+ onDisconnected: () => {
+ networkMonitor.destroy();
+ networkMonitor = null;
+ }
+ };
+}
+
+exports.setupParentProcess = setupParentProcess;
+
+/**
+ * The NetworkMonitorParent runs in the parent process and uses the message manager to
+ * listen for requests from the child process to start/stop the network monitor. Most
+ * request data is only available from the parent process, so that's why the network
+ * monitor needs to run there when debugging tabs that are in the child.
+ *
+ * @param nsIMessageManager mm
+ * The message manager for the browser we're filtering on.
+ * @param string prefix
+ * The RDP connection prefix that uniquely identifies the connection.
+ */
+function NetworkMonitorParent(mm, prefix) {
+ this._msgName = `debug:${prefix}netmonitor`;
+ this.onNetMonitorMessage = this.onNetMonitorMessage.bind(this);
+ this.onNetworkEvent = this.onNetworkEvent.bind(this);
+ this.setMessageManager(mm);
+}
+
+NetworkMonitorParent.prototype = {
+ netMonitor: null,
+ messageManager: null,
+
+ setMessageManager(mm) {
+ if (this.messageManager) {
+ let oldMM = this.messageManager;
+ oldMM.removeMessageListener(this._msgName, this.onNetMonitorMessage);
+ }
+ this.messageManager = mm;
+ if (mm) {
+ mm.addMessageListener(this._msgName, this.onNetMonitorMessage);
+ }
+ },
+
+ /**
+ * Handler for `debug:${prefix}netmonitor` messages received through the message manager
+ * from the content process.
+ *
+ * @param object msg
+ * Message from the content.
+ */
+ onNetMonitorMessage: DevToolsUtils.makeInfallible(function (msg) {
+ let {action} = msg.json;
+ // Pipe network monitor data from parent to child via the message manager.
+ switch (action) {
+ case "start":
+ if (!this.netMonitor) {
+ let {appId, outerWindowID} = msg.json;
+ this.netMonitor = new NetworkMonitor({
+ outerWindowID,
+ appId,
+ }, this);
+ this.netMonitor.init();
+ }
+ break;
+ case "setPreferences": {
+ let {preferences} = msg.json;
+ for (let key of Object.keys(preferences)) {
+ if ((key == "saveRequestAndResponseBodies" ||
+ key == "throttleData") && this.netMonitor) {
+ this.netMonitor[key] = 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 (event) {
+ return new NetworkEventActorProxy(this.messageManager, this._msgName).init(event);
+ }),
+
+ destroy: function () {
+ this.setMessageManager(null);
+
+ if (this.netMonitor) {
+ this.netMonitor.destroy();
+ this.netMonitor = null;
+ }
+ },
+};
+
+/**
+ * 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 window
+ * The window for which we need to track location changes.
+ * @param object owner
+ * The listener owner which needs to implement two methods:
+ * - onFileActivity(aFileURI)
+ * - onLocationChange(aState, aTabURI, aPageTitle)
+ */
+function ConsoleProgressListener(window, owner) {
+ this.window = window;
+ this.owner = owner;
+}
+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 () {
+ 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 monitor
+ * 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 (monitor) {
+ switch (monitor) {
+ 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 " +
+ monitor + "!");
+ }
+ this._init();
+ },
+
+ /**
+ * Stop a monitor.
+ *
+ * @param number monitor
+ * Tells what you want to stop tracking. See this.startMonitor() for
+ * the list of constants.
+ */
+ stopMonitor: function (monitor) {
+ switch (monitor) {
+ 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 " +
+ monitor + "!");
+ }
+
+ if (!this._fileActivity && !this._locationChange) {
+ this.destroy();
+ }
+ },
+
+ onStateChange: function (progress, request, state, status) {
+ if (!this.owner) {
+ return;
+ }
+
+ if (this._fileActivity) {
+ this._checkFileActivity(progress, request, state, status);
+ }
+
+ if (this._locationChange) {
+ this._checkLocationChange(progress, request, state, status);
+ }
+ },
+
+ /**
+ * 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 (progress, request, state, status) {
+ if (!(state & Ci.nsIWebProgressListener.STATE_START)) {
+ return;
+ }
+
+ let uri = null;
+ if (request instanceof Ci.imgIRequest) {
+ let imgIRequest = request.QueryInterface(Ci.imgIRequest);
+ uri = imgIRequest.URI;
+ } else if (request instanceof Ci.nsIChannel) {
+ let nsIChannel = request.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 (progress, request, state) {
+ let isStart = state & Ci.nsIWebProgressListener.STATE_START;
+ let isStop = state & Ci.nsIWebProgressListener.STATE_STOP;
+ let isNetwork = state & Ci.nsIWebProgressListener.STATE_IS_NETWORK;
+ let isWindow = state & Ci.nsIWebProgressListener.STATE_IS_WINDOW;
+
+ // Skip non-interesting states.
+ if (!isNetwork || !isWindow || progress.DOMWindow != this.window) {
+ return;
+ }
+
+ if (isStart && request instanceof Ci.nsIChannel) {
+ this.owner.onLocationChange("start", request.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 () {
+ 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;
+ },
+};
+
+function gSequenceId() {
+ return gSequenceId.n++;
+}
+gSequenceId.n = 1;