12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093 |
- /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
- /* 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",
- 0x804b000c: "STATUS_TLS_STARTING",
- 0x804b000d: "STATUS_TLS_ENDING"
- },
- 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 error:
- // 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,
- ssl: 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_TLS_STARTING && timings.STATUS_TLS_ENDING) {
- harTimings.ssl = timings.STATUS_TLS_ENDING.last -
- timings.STATUS_TLS_STARTING.first;
- } else {
- harTimings.ssl = -1;
- }
- // sometimes the connection information events are attached to a speculative
- // channel instead of this one, but necko might glue them back together in the
- // nsITimedChannel interface used by Resource and Navigation Timing
- let timedChannel = httpActivity.channel.QueryInterface(Ci.nsITimedChannel);
- if ((harTimings.connect <= 0) && timedChannel) {
- if (timedChannel.secureConnectionStartTime > timedChannel.connectStartTime) {
- harTimings.connect =
- timedChannel.secureConnectionStartTime - timedChannel.connectStartTime;
- harTimings.ssl =
- timedChannel.connectEndTime - timedChannel.secureConnectionStartTime;
- } else {
- harTimings.connect =
- timedChannel.connectEndTime - timedChannel.connectStartTime;
- harTimings.ssl = -1;
- }
- }
- if ((harTimings.dns <= 0) && timedChannel) {
- harTimings.dns =
- timedChannel.domainLookupEndTime - timedChannel.domainLookupStartTime;
- }
- 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;
|