/* -*- 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(); // XXX: // Sometimes, when using a proxy server (manual proxy configuration), // throws an errors: // 0x80040111 (NS_ERROR_NOT_AVAILABLE) // [nsIHttpChannelInternal.remoteAddress] // Bug 1337791 is the suspect. response.remoteAddress = null; try { response.remoteAddress = httpActivity.channel.remoteAddress; } catch (e) { Cu.reportError(e); } response.remotePort = null; try { response.remotePort = httpActivity.channel.remotePort; } catch (e) { Cu.reportError(e); } 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;