mirror of
https://github.com/classilla/tenfourfox.git
synced 2024-09-17 12:55:56 +00:00
1729 lines
54 KiB
JavaScript
1729 lines
54 KiB
JavaScript
/* 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, Cr} = require("chrome");
|
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
loader.lazyRequireGetter(this, "NetworkHelper",
|
|
"devtools/shared/webconsole/network-helper");
|
|
loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm");
|
|
loader.lazyRequireGetter(this, "DevToolsUtils",
|
|
"devtools/shared/DevToolsUtils");
|
|
loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.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;
|
|
let channel = this.httpActivity.channel;
|
|
this._wrappedNotificationCallbacks = channel.notificationCallbacks;
|
|
channel.notificationCallbacks = this;
|
|
}
|
|
exports.NetworkResponseListener = NetworkResponseListener;
|
|
|
|
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 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)
|
|
{
|
|
// Converter will call this again, we should just ignore that.
|
|
if (this.request)
|
|
return;
|
|
|
|
this.request = aRequest;
|
|
this._getSecurityInfo();
|
|
this._findOpenResponse();
|
|
// We need to track the offset for the onDataAvailable calls where we pass the data
|
|
// from our pipe to the coverter.
|
|
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.
|
|
let channel = this.request;
|
|
if (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", "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 NRL_getSecurityInfo() {
|
|
// 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 NRL_onStopRequest()
|
|
{
|
|
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 NRL__findOpenResponse()
|
|
{
|
|
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 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;
|
|
response.transferredSize = this.transferredSize;
|
|
|
|
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._wrappedNotificationCallbacks = null;
|
|
this.httpActivity.channel = null;
|
|
this.httpActivity.owner = 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 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) {
|
|
if (this.converter) {
|
|
this.converter.onDataAvailable(this.request, null, aStream, this.offset, available);
|
|
} else {
|
|
this.onDataAvailable(this.request, null, aStream, this.offset, available);
|
|
}
|
|
}
|
|
this.offset += available;
|
|
this.setAsyncListener(aStream, this);
|
|
}
|
|
else {
|
|
this.onStreamClose();
|
|
this.offset = 0;
|
|
}
|
|
},
|
|
}; // 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);
|
|
Services.obs.addObserver(this._httpResponseExaminer,
|
|
"http-on-examine-cached-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" &&
|
|
aTopic != "http-on-examine-cached-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;
|
|
|
|
if (aTopic === "http-on-examine-cached-response") {
|
|
// 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: true });
|
|
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);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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 (let id in this.openRequests) {
|
|
let item = this.openRequests[id];
|
|
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;
|
|
}
|
|
|
|
// 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 DevToolsUtils.testing check. We will move to a better way to serve
|
|
// its needs in bug 1167188, where this check should be removed.
|
|
if (!DevToolsUtils.testing && aChannel.loadInfo &&
|
|
aChannel.loadInfo.loadingDocument === null &&
|
|
aChannel.loadInfo.loadingPrincipal === Services.scriptSecurityManager.getSystemPrincipal()) {
|
|
return false;
|
|
}
|
|
|
|
if (this.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(aChannel);
|
|
while(win) {
|
|
if (win == this.window) {
|
|
return true;
|
|
}
|
|
if (win.parent == win) {
|
|
break;
|
|
}
|
|
win = win.parent;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// The following check is necessary because beacon channels don't come
|
|
// associated with a load group. Bug 1160837 will hopefully introduce a
|
|
// platform fix that will render the following code entirely useless.
|
|
if (aChannel.loadInfo &&
|
|
aChannel.loadInfo.externalContentPolicyType == Ci.nsIContentPolicy.TYPE_BEACON) {
|
|
let nonE10sMatch = this.window &&
|
|
aChannel.loadInfo.loadingDocument === this.window.document;
|
|
let e10sMatch = this.topFrame &&
|
|
this.topFrame.contentPrincipal &&
|
|
this.topFrame.contentPrincipal.equals(aChannel.loadInfo.loadingPrincipal) &&
|
|
this.topFrame.contentPrincipal.URI.spec == aChannel.referrer.spec;
|
|
let b2gMatch = this.appId &&
|
|
aChannel.loadInfo.loadingPrincipal.appId === this.appId;
|
|
if (nonE10sMatch || e10sMatch || b2gMatch) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
*
|
|
*/
|
|
_createNetworkEvent: function(aChannel, { timestamp, extraStringData, fromCache }) {
|
|
let win = NetworkHelper.getWindowForRequest(aChannel);
|
|
let httpActivity = this.createActivityObject(aChannel);
|
|
|
|
// see NM__onRequestBodySent()
|
|
httpActivity.charset = win ? win.document.characterSet : null;
|
|
|
|
aChannel.QueryInterface(Ci.nsIPrivateBrowsingChannel);
|
|
httpActivity.private = aChannel.isChannelPrivate;
|
|
|
|
if (timestamp) {
|
|
httpActivity.timings.REQUEST_HEADER = {
|
|
first: timestamp,
|
|
last: timestamp
|
|
};
|
|
}
|
|
|
|
let event = {};
|
|
event.method = aChannel.requestMethod;
|
|
event.url = aChannel.URI.spec;
|
|
event.private = httpActivity.private;
|
|
event.headersSize = 0;
|
|
event.startedDateTime = (timestamp ? new Date(Math.round(timestamp / 1000)) : new Date()).toISOString();
|
|
event.fromCache = fromCache;
|
|
|
|
if (extraStringData) {
|
|
event.headersSize = extraStringData.length;
|
|
}
|
|
|
|
// Determine if this is an XHR request.
|
|
httpActivity.isXHR = event.isXHR =
|
|
(aChannel.loadInfo.externalContentPolicyType === Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST ||
|
|
aChannel.loadInfo.externalContentPolicyType === Ci.nsIContentPolicy.TYPE_FETCH);
|
|
|
|
// Determine the HTTP version.
|
|
let httpVersionMaj = {};
|
|
let httpVersionMin = {};
|
|
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._setupResponseListener(httpActivity);
|
|
|
|
httpActivity.owner.addRequestHeaders(headers, extraStringData);
|
|
httpActivity.owner.addRequestCookies(cookies);
|
|
|
|
this.openRequests[httpActivity.id] = httpActivity;
|
|
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 aChannel
|
|
* @param number aTimestamp
|
|
* @param string aExtraStringData
|
|
* @return void
|
|
*/
|
|
_onRequestHeader:
|
|
function NM__onRequestHeader(aChannel, aTimestamp, aExtraStringData)
|
|
{
|
|
if (!this._matchRequest(aChannel)) {
|
|
return;
|
|
}
|
|
|
|
this._createNetworkEvent(aChannel, { timestamp: aTimestamp,
|
|
extraStringData: aExtraStringData });
|
|
},
|
|
|
|
/**
|
|
* 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,
|
|
hostname: aChannel.URI.host, // needed for host specific security info
|
|
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() method 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.remoteAddress = aHttpActivity.channel.remoteAddress;
|
|
response.remotePort = aHttpActivity.channel.remotePort;
|
|
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, aExtraStringData);
|
|
},
|
|
|
|
/**
|
|
* 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.
|
|
* @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 NM__setupHarTimings(aHttpActivity, 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 = 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;
|
|
try {
|
|
mm.removeMessageListener("debug:netmonitor:" + this.connID + ":newEvent",
|
|
this._onNewEvent);
|
|
mm.removeMessageListener("debug:netmonitor:" + this.connID + ":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.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", "addSecurityInfo", "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;
|