toolkit/devtools/webconsole/network-monitor.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 const {Cc, Ci, Cu} = require("chrome");
michael@0 8
michael@0 9 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 10
michael@0 11 loader.lazyGetter(this, "NetworkHelper", () => require("devtools/toolkit/webconsole/network-helper"));
michael@0 12 loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm");
michael@0 13 loader.lazyImporter(this, "DevToolsUtils", "resource://gre/modules/devtools/DevToolsUtils.jsm");
michael@0 14 loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
michael@0 15 loader.lazyImporter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm");
michael@0 16 loader.lazyServiceGetter(this, "gActivityDistributor",
michael@0 17 "@mozilla.org/network/http-activity-distributor;1",
michael@0 18 "nsIHttpActivityDistributor");
michael@0 19
michael@0 20 ///////////////////////////////////////////////////////////////////////////////
michael@0 21 // Network logging
michael@0 22 ///////////////////////////////////////////////////////////////////////////////
michael@0 23
michael@0 24 // The maximum uint32 value.
michael@0 25 const PR_UINT32_MAX = 4294967295;
michael@0 26
michael@0 27 // HTTP status codes.
michael@0 28 const HTTP_MOVED_PERMANENTLY = 301;
michael@0 29 const HTTP_FOUND = 302;
michael@0 30 const HTTP_SEE_OTHER = 303;
michael@0 31 const HTTP_TEMPORARY_REDIRECT = 307;
michael@0 32
michael@0 33 // The maximum number of bytes a NetworkResponseListener can hold.
michael@0 34 const RESPONSE_BODY_LIMIT = 1048576; // 1 MB
michael@0 35
michael@0 36 /**
michael@0 37 * The network response listener implements the nsIStreamListener and
michael@0 38 * nsIRequestObserver interfaces. This is used within the NetworkMonitor feature
michael@0 39 * to get the response body of the request.
michael@0 40 *
michael@0 41 * The code is mostly based on code listings from:
michael@0 42 *
michael@0 43 * http://www.softwareishard.com/blog/firebug/
michael@0 44 * nsitraceablechannel-intercept-http-traffic/
michael@0 45 *
michael@0 46 * @constructor
michael@0 47 * @param object aOwner
michael@0 48 * The response listener owner. This object needs to hold the
michael@0 49 * |openResponses| object.
michael@0 50 * @param object aHttpActivity
michael@0 51 * HttpActivity object associated with this request. See NetworkMonitor
michael@0 52 * for more information.
michael@0 53 */
michael@0 54 function NetworkResponseListener(aOwner, aHttpActivity)
michael@0 55 {
michael@0 56 this.owner = aOwner;
michael@0 57 this.receivedData = "";
michael@0 58 this.httpActivity = aHttpActivity;
michael@0 59 this.bodySize = 0;
michael@0 60 }
michael@0 61 exports.NetworkResponseListener = NetworkResponseListener;
michael@0 62
michael@0 63 NetworkResponseListener.prototype = {
michael@0 64 QueryInterface:
michael@0 65 XPCOMUtils.generateQI([Ci.nsIStreamListener, Ci.nsIInputStreamCallback,
michael@0 66 Ci.nsIRequestObserver, Ci.nsISupports]),
michael@0 67
michael@0 68 /**
michael@0 69 * This NetworkResponseListener tracks the NetworkMonitor.openResponses object
michael@0 70 * to find the associated uncached headers.
michael@0 71 * @private
michael@0 72 */
michael@0 73 _foundOpenResponse: false,
michael@0 74
michael@0 75 /**
michael@0 76 * The response listener owner.
michael@0 77 */
michael@0 78 owner: null,
michael@0 79
michael@0 80 /**
michael@0 81 * The response will be written into the outputStream of this nsIPipe.
michael@0 82 * Both ends of the pipe must be blocking.
michael@0 83 */
michael@0 84 sink: null,
michael@0 85
michael@0 86 /**
michael@0 87 * The HttpActivity object associated with this response.
michael@0 88 */
michael@0 89 httpActivity: null,
michael@0 90
michael@0 91 /**
michael@0 92 * Stores the received data as a string.
michael@0 93 */
michael@0 94 receivedData: null,
michael@0 95
michael@0 96 /**
michael@0 97 * The network response body size.
michael@0 98 */
michael@0 99 bodySize: null,
michael@0 100
michael@0 101 /**
michael@0 102 * The nsIRequest we are started for.
michael@0 103 */
michael@0 104 request: null,
michael@0 105
michael@0 106 /**
michael@0 107 * Set the async listener for the given nsIAsyncInputStream. This allows us to
michael@0 108 * wait asynchronously for any data coming from the stream.
michael@0 109 *
michael@0 110 * @param nsIAsyncInputStream aStream
michael@0 111 * The input stream from where we are waiting for data to come in.
michael@0 112 * @param nsIInputStreamCallback aListener
michael@0 113 * The input stream callback you want. This is an object that must have
michael@0 114 * the onInputStreamReady() method. If the argument is null, then the
michael@0 115 * current callback is removed.
michael@0 116 * @return void
michael@0 117 */
michael@0 118 setAsyncListener: function NRL_setAsyncListener(aStream, aListener)
michael@0 119 {
michael@0 120 // Asynchronously wait for the stream to be readable or closed.
michael@0 121 aStream.asyncWait(aListener, 0, 0, Services.tm.mainThread);
michael@0 122 },
michael@0 123
michael@0 124 /**
michael@0 125 * Stores the received data, if request/response body logging is enabled. It
michael@0 126 * also does limit the number of stored bytes, based on the
michael@0 127 * RESPONSE_BODY_LIMIT constant.
michael@0 128 *
michael@0 129 * Learn more about nsIStreamListener at:
michael@0 130 * https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIStreamListener
michael@0 131 *
michael@0 132 * @param nsIRequest aRequest
michael@0 133 * @param nsISupports aContext
michael@0 134 * @param nsIInputStream aInputStream
michael@0 135 * @param unsigned long aOffset
michael@0 136 * @param unsigned long aCount
michael@0 137 */
michael@0 138 onDataAvailable:
michael@0 139 function NRL_onDataAvailable(aRequest, aContext, aInputStream, aOffset, aCount)
michael@0 140 {
michael@0 141 this._findOpenResponse();
michael@0 142 let data = NetUtil.readInputStreamToString(aInputStream, aCount);
michael@0 143
michael@0 144 this.bodySize += aCount;
michael@0 145
michael@0 146 if (!this.httpActivity.discardResponseBody &&
michael@0 147 this.receivedData.length < RESPONSE_BODY_LIMIT) {
michael@0 148 this.receivedData += NetworkHelper.
michael@0 149 convertToUnicode(data, aRequest.contentCharset);
michael@0 150 }
michael@0 151 },
michael@0 152
michael@0 153 /**
michael@0 154 * See documentation at
michael@0 155 * https://developer.mozilla.org/En/NsIRequestObserver
michael@0 156 *
michael@0 157 * @param nsIRequest aRequest
michael@0 158 * @param nsISupports aContext
michael@0 159 */
michael@0 160 onStartRequest: function NRL_onStartRequest(aRequest)
michael@0 161 {
michael@0 162 this.request = aRequest;
michael@0 163 this._findOpenResponse();
michael@0 164 // Asynchronously wait for the data coming from the request.
michael@0 165 this.setAsyncListener(this.sink.inputStream, this);
michael@0 166 },
michael@0 167
michael@0 168 /**
michael@0 169 * Handle the onStopRequest by closing the sink output stream.
michael@0 170 *
michael@0 171 * For more documentation about nsIRequestObserver go to:
michael@0 172 * https://developer.mozilla.org/En/NsIRequestObserver
michael@0 173 */
michael@0 174 onStopRequest: function NRL_onStopRequest()
michael@0 175 {
michael@0 176 this._findOpenResponse();
michael@0 177 this.sink.outputStream.close();
michael@0 178 },
michael@0 179
michael@0 180 /**
michael@0 181 * Find the open response object associated to the current request. The
michael@0 182 * NetworkMonitor._httpResponseExaminer() method saves the response headers in
michael@0 183 * NetworkMonitor.openResponses. This method takes the data from the open
michael@0 184 * response object and puts it into the HTTP activity object, then sends it to
michael@0 185 * the remote Web Console instance.
michael@0 186 *
michael@0 187 * @private
michael@0 188 */
michael@0 189 _findOpenResponse: function NRL__findOpenResponse()
michael@0 190 {
michael@0 191 if (!this.owner || this._foundOpenResponse) {
michael@0 192 return;
michael@0 193 }
michael@0 194
michael@0 195 let openResponse = null;
michael@0 196
michael@0 197 for each (let item in this.owner.openResponses) {
michael@0 198 if (item.channel === this.httpActivity.channel) {
michael@0 199 openResponse = item;
michael@0 200 break;
michael@0 201 }
michael@0 202 }
michael@0 203
michael@0 204 if (!openResponse) {
michael@0 205 return;
michael@0 206 }
michael@0 207 this._foundOpenResponse = true;
michael@0 208
michael@0 209 delete this.owner.openResponses[openResponse.id];
michael@0 210
michael@0 211 this.httpActivity.owner.addResponseHeaders(openResponse.headers);
michael@0 212 this.httpActivity.owner.addResponseCookies(openResponse.cookies);
michael@0 213 },
michael@0 214
michael@0 215 /**
michael@0 216 * Clean up the response listener once the response input stream is closed.
michael@0 217 * This is called from onStopRequest() or from onInputStreamReady() when the
michael@0 218 * stream is closed.
michael@0 219 * @return void
michael@0 220 */
michael@0 221 onStreamClose: function NRL_onStreamClose()
michael@0 222 {
michael@0 223 if (!this.httpActivity) {
michael@0 224 return;
michael@0 225 }
michael@0 226 // Remove our listener from the request input stream.
michael@0 227 this.setAsyncListener(this.sink.inputStream, null);
michael@0 228
michael@0 229 this._findOpenResponse();
michael@0 230
michael@0 231 if (!this.httpActivity.discardResponseBody && this.receivedData.length) {
michael@0 232 this._onComplete(this.receivedData);
michael@0 233 }
michael@0 234 else if (!this.httpActivity.discardResponseBody &&
michael@0 235 this.httpActivity.responseStatus == 304) {
michael@0 236 // Response is cached, so we load it from cache.
michael@0 237 let charset = this.request.contentCharset || this.httpActivity.charset;
michael@0 238 NetworkHelper.loadFromCache(this.httpActivity.url, charset,
michael@0 239 this._onComplete.bind(this));
michael@0 240 }
michael@0 241 else {
michael@0 242 this._onComplete();
michael@0 243 }
michael@0 244 },
michael@0 245
michael@0 246 /**
michael@0 247 * Handler for when the response completes. This function cleans up the
michael@0 248 * response listener.
michael@0 249 *
michael@0 250 * @param string [aData]
michael@0 251 * Optional, the received data coming from the response listener or
michael@0 252 * from the cache.
michael@0 253 */
michael@0 254 _onComplete: function NRL__onComplete(aData)
michael@0 255 {
michael@0 256 let response = {
michael@0 257 mimeType: "",
michael@0 258 text: aData || "",
michael@0 259 };
michael@0 260
michael@0 261 response.size = response.text.length;
michael@0 262
michael@0 263 try {
michael@0 264 response.mimeType = this.request.contentType;
michael@0 265 }
michael@0 266 catch (ex) { }
michael@0 267
michael@0 268 if (!response.mimeType || !NetworkHelper.isTextMimeType(response.mimeType)) {
michael@0 269 response.encoding = "base64";
michael@0 270 response.text = btoa(response.text);
michael@0 271 }
michael@0 272
michael@0 273 if (response.mimeType && this.request.contentCharset) {
michael@0 274 response.mimeType += "; charset=" + this.request.contentCharset;
michael@0 275 }
michael@0 276
michael@0 277 this.receivedData = "";
michael@0 278
michael@0 279 this.httpActivity.owner.
michael@0 280 addResponseContent(response, this.httpActivity.discardResponseBody);
michael@0 281
michael@0 282 this.httpActivity.channel = null;
michael@0 283 this.httpActivity.owner = null;
michael@0 284 this.httpActivity = null;
michael@0 285 this.sink = null;
michael@0 286 this.inputStream = null;
michael@0 287 this.request = null;
michael@0 288 this.owner = null;
michael@0 289 },
michael@0 290
michael@0 291 /**
michael@0 292 * The nsIInputStreamCallback for when the request input stream is ready -
michael@0 293 * either it has more data or it is closed.
michael@0 294 *
michael@0 295 * @param nsIAsyncInputStream aStream
michael@0 296 * The sink input stream from which data is coming.
michael@0 297 * @returns void
michael@0 298 */
michael@0 299 onInputStreamReady: function NRL_onInputStreamReady(aStream)
michael@0 300 {
michael@0 301 if (!(aStream instanceof Ci.nsIAsyncInputStream) || !this.httpActivity) {
michael@0 302 return;
michael@0 303 }
michael@0 304
michael@0 305 let available = -1;
michael@0 306 try {
michael@0 307 // This may throw if the stream is closed normally or due to an error.
michael@0 308 available = aStream.available();
michael@0 309 }
michael@0 310 catch (ex) { }
michael@0 311
michael@0 312 if (available != -1) {
michael@0 313 if (available != 0) {
michael@0 314 // Note that passing 0 as the offset here is wrong, but the
michael@0 315 // onDataAvailable() method does not use the offset, so it does not
michael@0 316 // matter.
michael@0 317 this.onDataAvailable(this.request, null, aStream, 0, available);
michael@0 318 }
michael@0 319 this.setAsyncListener(aStream, this);
michael@0 320 }
michael@0 321 else {
michael@0 322 this.onStreamClose();
michael@0 323 }
michael@0 324 },
michael@0 325 }; // NetworkResponseListener.prototype
michael@0 326
michael@0 327
michael@0 328 /**
michael@0 329 * The network monitor uses the nsIHttpActivityDistributor to monitor network
michael@0 330 * requests. The nsIObserverService is also used for monitoring
michael@0 331 * http-on-examine-response notifications. All network request information is
michael@0 332 * routed to the remote Web Console.
michael@0 333 *
michael@0 334 * @constructor
michael@0 335 * @param object aFilters
michael@0 336 * Object with the filters to use for network requests:
michael@0 337 * - window (nsIDOMWindow): filter network requests by the associated
michael@0 338 * window object.
michael@0 339 * - appId (number): filter requests by the appId.
michael@0 340 * - topFrame (nsIDOMElement): filter requests by their topFrameElement.
michael@0 341 * Filters are optional. If any of these filters match the request is
michael@0 342 * logged (OR is applied). If no filter is provided then all requests are
michael@0 343 * logged.
michael@0 344 * @param object aOwner
michael@0 345 * The network monitor owner. This object needs to hold:
michael@0 346 * - onNetworkEvent(aRequestInfo, aChannel, aNetworkMonitor).
michael@0 347 * This method is invoked once for every new network request and it is
michael@0 348 * given the following arguments: the initial network request
michael@0 349 * information, and the channel. The third argument is the NetworkMonitor
michael@0 350 * instance.
michael@0 351 * onNetworkEvent() must return an object which holds several add*()
michael@0 352 * methods which are used to add further network request/response
michael@0 353 * information.
michael@0 354 */
michael@0 355 function NetworkMonitor(aFilters, aOwner)
michael@0 356 {
michael@0 357 if (aFilters) {
michael@0 358 this.window = aFilters.window;
michael@0 359 this.appId = aFilters.appId;
michael@0 360 this.topFrame = aFilters.topFrame;
michael@0 361 }
michael@0 362 if (!this.window && !this.appId && !this.topFrame) {
michael@0 363 this._logEverything = true;
michael@0 364 }
michael@0 365 this.owner = aOwner;
michael@0 366 this.openRequests = {};
michael@0 367 this.openResponses = {};
michael@0 368 this._httpResponseExaminer =
michael@0 369 DevToolsUtils.makeInfallible(this._httpResponseExaminer).bind(this);
michael@0 370 }
michael@0 371 exports.NetworkMonitor = NetworkMonitor;
michael@0 372
michael@0 373 NetworkMonitor.prototype = {
michael@0 374 _logEverything: false,
michael@0 375 window: null,
michael@0 376 appId: null,
michael@0 377 topFrame: null,
michael@0 378
michael@0 379 httpTransactionCodes: {
michael@0 380 0x5001: "REQUEST_HEADER",
michael@0 381 0x5002: "REQUEST_BODY_SENT",
michael@0 382 0x5003: "RESPONSE_START",
michael@0 383 0x5004: "RESPONSE_HEADER",
michael@0 384 0x5005: "RESPONSE_COMPLETE",
michael@0 385 0x5006: "TRANSACTION_CLOSE",
michael@0 386
michael@0 387 0x804b0003: "STATUS_RESOLVING",
michael@0 388 0x804b000b: "STATUS_RESOLVED",
michael@0 389 0x804b0007: "STATUS_CONNECTING_TO",
michael@0 390 0x804b0004: "STATUS_CONNECTED_TO",
michael@0 391 0x804b0005: "STATUS_SENDING_TO",
michael@0 392 0x804b000a: "STATUS_WAITING_FOR",
michael@0 393 0x804b0006: "STATUS_RECEIVING_FROM"
michael@0 394 },
michael@0 395
michael@0 396 // Network response bodies are piped through a buffer of the given size (in
michael@0 397 // bytes).
michael@0 398 responsePipeSegmentSize: null,
michael@0 399
michael@0 400 owner: null,
michael@0 401
michael@0 402 /**
michael@0 403 * Whether to save the bodies of network requests and responses. Disabled by
michael@0 404 * default to save memory.
michael@0 405 * @type boolean
michael@0 406 */
michael@0 407 saveRequestAndResponseBodies: false,
michael@0 408
michael@0 409 /**
michael@0 410 * Object that holds the HTTP activity objects for ongoing requests.
michael@0 411 */
michael@0 412 openRequests: null,
michael@0 413
michael@0 414 /**
michael@0 415 * Object that holds response headers coming from this._httpResponseExaminer.
michael@0 416 */
michael@0 417 openResponses: null,
michael@0 418
michael@0 419 /**
michael@0 420 * The network monitor initializer.
michael@0 421 */
michael@0 422 init: function NM_init()
michael@0 423 {
michael@0 424 this.responsePipeSegmentSize = Services.prefs
michael@0 425 .getIntPref("network.buffer.cache.size");
michael@0 426
michael@0 427 gActivityDistributor.addObserver(this);
michael@0 428
michael@0 429 if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
michael@0 430 Services.obs.addObserver(this._httpResponseExaminer,
michael@0 431 "http-on-examine-response", false);
michael@0 432 }
michael@0 433 },
michael@0 434
michael@0 435 /**
michael@0 436 * Observe notifications for the http-on-examine-response topic, coming from
michael@0 437 * the nsIObserverService.
michael@0 438 *
michael@0 439 * @private
michael@0 440 * @param nsIHttpChannel aSubject
michael@0 441 * @param string aTopic
michael@0 442 * @returns void
michael@0 443 */
michael@0 444 _httpResponseExaminer: function NM__httpResponseExaminer(aSubject, aTopic)
michael@0 445 {
michael@0 446 // The httpResponseExaminer is used to retrieve the uncached response
michael@0 447 // headers. The data retrieved is stored in openResponses. The
michael@0 448 // NetworkResponseListener is responsible with updating the httpActivity
michael@0 449 // object with the data from the new object in openResponses.
michael@0 450
michael@0 451 if (!this.owner || aTopic != "http-on-examine-response" ||
michael@0 452 !(aSubject instanceof Ci.nsIHttpChannel)) {
michael@0 453 return;
michael@0 454 }
michael@0 455
michael@0 456 let channel = aSubject.QueryInterface(Ci.nsIHttpChannel);
michael@0 457
michael@0 458 if (!this._matchRequest(channel)) {
michael@0 459 return;
michael@0 460 }
michael@0 461
michael@0 462 let response = {
michael@0 463 id: gSequenceId(),
michael@0 464 channel: channel,
michael@0 465 headers: [],
michael@0 466 cookies: [],
michael@0 467 };
michael@0 468
michael@0 469 let setCookieHeader = null;
michael@0 470
michael@0 471 channel.visitResponseHeaders({
michael@0 472 visitHeader: function NM__visitHeader(aName, aValue) {
michael@0 473 let lowerName = aName.toLowerCase();
michael@0 474 if (lowerName == "set-cookie") {
michael@0 475 setCookieHeader = aValue;
michael@0 476 }
michael@0 477 response.headers.push({ name: aName, value: aValue });
michael@0 478 }
michael@0 479 });
michael@0 480
michael@0 481 if (!response.headers.length) {
michael@0 482 return; // No need to continue.
michael@0 483 }
michael@0 484
michael@0 485 if (setCookieHeader) {
michael@0 486 response.cookies = NetworkHelper.parseSetCookieHeader(setCookieHeader);
michael@0 487 }
michael@0 488
michael@0 489 // Determine the HTTP version.
michael@0 490 let httpVersionMaj = {};
michael@0 491 let httpVersionMin = {};
michael@0 492
michael@0 493 channel.QueryInterface(Ci.nsIHttpChannelInternal);
michael@0 494 channel.getResponseVersion(httpVersionMaj, httpVersionMin);
michael@0 495
michael@0 496 response.status = channel.responseStatus;
michael@0 497 response.statusText = channel.responseStatusText;
michael@0 498 response.httpVersion = "HTTP/" + httpVersionMaj.value + "." +
michael@0 499 httpVersionMin.value;
michael@0 500
michael@0 501 this.openResponses[response.id] = response;
michael@0 502 },
michael@0 503
michael@0 504 /**
michael@0 505 * Begin observing HTTP traffic that originates inside the current tab.
michael@0 506 *
michael@0 507 * @see https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIHttpActivityObserver
michael@0 508 *
michael@0 509 * @param nsIHttpChannel aChannel
michael@0 510 * @param number aActivityType
michael@0 511 * @param number aActivitySubtype
michael@0 512 * @param number aTimestamp
michael@0 513 * @param number aExtraSizeData
michael@0 514 * @param string aExtraStringData
michael@0 515 */
michael@0 516 observeActivity: DevToolsUtils.makeInfallible(function NM_observeActivity(aChannel, aActivityType, aActivitySubtype, aTimestamp, aExtraSizeData, aExtraStringData)
michael@0 517 {
michael@0 518 if (!this.owner ||
michael@0 519 aActivityType != gActivityDistributor.ACTIVITY_TYPE_HTTP_TRANSACTION &&
michael@0 520 aActivityType != gActivityDistributor.ACTIVITY_TYPE_SOCKET_TRANSPORT) {
michael@0 521 return;
michael@0 522 }
michael@0 523
michael@0 524 if (!(aChannel instanceof Ci.nsIHttpChannel)) {
michael@0 525 return;
michael@0 526 }
michael@0 527
michael@0 528 aChannel = aChannel.QueryInterface(Ci.nsIHttpChannel);
michael@0 529
michael@0 530 if (aActivitySubtype ==
michael@0 531 gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_HEADER) {
michael@0 532 this._onRequestHeader(aChannel, aTimestamp, aExtraStringData);
michael@0 533 return;
michael@0 534 }
michael@0 535
michael@0 536 // Iterate over all currently ongoing requests. If aChannel can't
michael@0 537 // be found within them, then exit this function.
michael@0 538 let httpActivity = null;
michael@0 539 for each (let item in this.openRequests) {
michael@0 540 if (item.channel === aChannel) {
michael@0 541 httpActivity = item;
michael@0 542 break;
michael@0 543 }
michael@0 544 }
michael@0 545
michael@0 546 if (!httpActivity) {
michael@0 547 return;
michael@0 548 }
michael@0 549
michael@0 550 let transCodes = this.httpTransactionCodes;
michael@0 551
michael@0 552 // Store the time information for this activity subtype.
michael@0 553 if (aActivitySubtype in transCodes) {
michael@0 554 let stage = transCodes[aActivitySubtype];
michael@0 555 if (stage in httpActivity.timings) {
michael@0 556 httpActivity.timings[stage].last = aTimestamp;
michael@0 557 }
michael@0 558 else {
michael@0 559 httpActivity.timings[stage] = {
michael@0 560 first: aTimestamp,
michael@0 561 last: aTimestamp,
michael@0 562 };
michael@0 563 }
michael@0 564 }
michael@0 565
michael@0 566 switch (aActivitySubtype) {
michael@0 567 case gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_BODY_SENT:
michael@0 568 this._onRequestBodySent(httpActivity);
michael@0 569 break;
michael@0 570 case gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER:
michael@0 571 this._onResponseHeader(httpActivity, aExtraStringData);
michael@0 572 break;
michael@0 573 case gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE:
michael@0 574 this._onTransactionClose(httpActivity);
michael@0 575 break;
michael@0 576 default:
michael@0 577 break;
michael@0 578 }
michael@0 579 }),
michael@0 580
michael@0 581 /**
michael@0 582 * Check if a given network request should be logged by this network monitor
michael@0 583 * instance based on the current filters.
michael@0 584 *
michael@0 585 * @private
michael@0 586 * @param nsIHttpChannel aChannel
michael@0 587 * Request to check.
michael@0 588 * @return boolean
michael@0 589 * True if the network request should be logged, false otherwise.
michael@0 590 */
michael@0 591 _matchRequest: function NM__matchRequest(aChannel)
michael@0 592 {
michael@0 593 if (this._logEverything) {
michael@0 594 return true;
michael@0 595 }
michael@0 596
michael@0 597 if (this.window) {
michael@0 598 let win = NetworkHelper.getWindowForRequest(aChannel);
michael@0 599 if (win && win.top === this.window) {
michael@0 600 return true;
michael@0 601 }
michael@0 602 }
michael@0 603
michael@0 604 if (this.topFrame) {
michael@0 605 let topFrame = NetworkHelper.getTopFrameForRequest(aChannel);
michael@0 606 if (topFrame && topFrame === this.topFrame) {
michael@0 607 return true;
michael@0 608 }
michael@0 609 }
michael@0 610
michael@0 611 if (this.appId) {
michael@0 612 let appId = NetworkHelper.getAppIdForRequest(aChannel);
michael@0 613 if (appId && appId == this.appId) {
michael@0 614 return true;
michael@0 615 }
michael@0 616 }
michael@0 617
michael@0 618 return false;
michael@0 619 },
michael@0 620
michael@0 621 /**
michael@0 622 * Handler for ACTIVITY_SUBTYPE_REQUEST_HEADER. When a request starts the
michael@0 623 * headers are sent to the server. This method creates the |httpActivity|
michael@0 624 * object where we store the request and response information that is
michael@0 625 * collected through its lifetime.
michael@0 626 *
michael@0 627 * @private
michael@0 628 * @param nsIHttpChannel aChannel
michael@0 629 * @param number aTimestamp
michael@0 630 * @param string aExtraStringData
michael@0 631 * @return void
michael@0 632 */
michael@0 633 _onRequestHeader:
michael@0 634 function NM__onRequestHeader(aChannel, aTimestamp, aExtraStringData)
michael@0 635 {
michael@0 636 if (!this._matchRequest(aChannel)) {
michael@0 637 return;
michael@0 638 }
michael@0 639
michael@0 640 let win = NetworkHelper.getWindowForRequest(aChannel);
michael@0 641 let httpActivity = this.createActivityObject(aChannel);
michael@0 642
michael@0 643 // see NM__onRequestBodySent()
michael@0 644 httpActivity.charset = win ? win.document.characterSet : null;
michael@0 645 httpActivity.private = win ? PrivateBrowsingUtils.isWindowPrivate(win) : false;
michael@0 646
michael@0 647 httpActivity.timings.REQUEST_HEADER = {
michael@0 648 first: aTimestamp,
michael@0 649 last: aTimestamp
michael@0 650 };
michael@0 651
michael@0 652 let httpVersionMaj = {};
michael@0 653 let httpVersionMin = {};
michael@0 654 let event = {};
michael@0 655 event.startedDateTime = new Date(Math.round(aTimestamp / 1000)).toISOString();
michael@0 656 event.headersSize = aExtraStringData.length;
michael@0 657 event.method = aChannel.requestMethod;
michael@0 658 event.url = aChannel.URI.spec;
michael@0 659 event.private = httpActivity.private;
michael@0 660
michael@0 661 // Determine if this is an XHR request.
michael@0 662 try {
michael@0 663 let callbacks = aChannel.notificationCallbacks;
michael@0 664 let xhrRequest = callbacks ? callbacks.getInterface(Ci.nsIXMLHttpRequest) : null;
michael@0 665 httpActivity.isXHR = event.isXHR = !!xhrRequest;
michael@0 666 } catch (e) {
michael@0 667 httpActivity.isXHR = event.isXHR = false;
michael@0 668 }
michael@0 669
michael@0 670 // Determine the HTTP version.
michael@0 671 aChannel.QueryInterface(Ci.nsIHttpChannelInternal);
michael@0 672 aChannel.getRequestVersion(httpVersionMaj, httpVersionMin);
michael@0 673
michael@0 674 event.httpVersion = "HTTP/" + httpVersionMaj.value + "." +
michael@0 675 httpVersionMin.value;
michael@0 676
michael@0 677 event.discardRequestBody = !this.saveRequestAndResponseBodies;
michael@0 678 event.discardResponseBody = !this.saveRequestAndResponseBodies;
michael@0 679
michael@0 680 let headers = [];
michael@0 681 let cookies = [];
michael@0 682 let cookieHeader = null;
michael@0 683
michael@0 684 // Copy the request header data.
michael@0 685 aChannel.visitRequestHeaders({
michael@0 686 visitHeader: function NM__visitHeader(aName, aValue)
michael@0 687 {
michael@0 688 if (aName == "Cookie") {
michael@0 689 cookieHeader = aValue;
michael@0 690 }
michael@0 691 headers.push({ name: aName, value: aValue });
michael@0 692 }
michael@0 693 });
michael@0 694
michael@0 695 if (cookieHeader) {
michael@0 696 cookies = NetworkHelper.parseCookieHeader(cookieHeader);
michael@0 697 }
michael@0 698
michael@0 699 httpActivity.owner = this.owner.onNetworkEvent(event, aChannel, this);
michael@0 700
michael@0 701 this._setupResponseListener(httpActivity);
michael@0 702
michael@0 703 this.openRequests[httpActivity.id] = httpActivity;
michael@0 704
michael@0 705 httpActivity.owner.addRequestHeaders(headers);
michael@0 706 httpActivity.owner.addRequestCookies(cookies);
michael@0 707 },
michael@0 708
michael@0 709 /**
michael@0 710 * Create the empty HTTP activity object. This object is used for storing all
michael@0 711 * the request and response information.
michael@0 712 *
michael@0 713 * This is a HAR-like object. Conformance to the spec is not guaranteed at
michael@0 714 * this point.
michael@0 715 *
michael@0 716 * TODO: Bug 708717 - Add support for network log export to HAR
michael@0 717 *
michael@0 718 * @see http://www.softwareishard.com/blog/har-12-spec
michael@0 719 * @param nsIHttpChannel aChannel
michael@0 720 * The HTTP channel for which the HTTP activity object is created.
michael@0 721 * @return object
michael@0 722 * The new HTTP activity object.
michael@0 723 */
michael@0 724 createActivityObject: function NM_createActivityObject(aChannel)
michael@0 725 {
michael@0 726 return {
michael@0 727 id: gSequenceId(),
michael@0 728 channel: aChannel,
michael@0 729 charset: null, // see NM__onRequestHeader()
michael@0 730 url: aChannel.URI.spec,
michael@0 731 discardRequestBody: !this.saveRequestAndResponseBodies,
michael@0 732 discardResponseBody: !this.saveRequestAndResponseBodies,
michael@0 733 timings: {}, // internal timing information, see NM_observeActivity()
michael@0 734 responseStatus: null, // see NM__onResponseHeader()
michael@0 735 owner: null, // the activity owner which is notified when changes happen
michael@0 736 };
michael@0 737 },
michael@0 738
michael@0 739 /**
michael@0 740 * Setup the network response listener for the given HTTP activity. The
michael@0 741 * NetworkResponseListener is responsible for storing the response body.
michael@0 742 *
michael@0 743 * @private
michael@0 744 * @param object aHttpActivity
michael@0 745 * The HTTP activity object we are tracking.
michael@0 746 */
michael@0 747 _setupResponseListener: function NM__setupResponseListener(aHttpActivity)
michael@0 748 {
michael@0 749 let channel = aHttpActivity.channel;
michael@0 750 channel.QueryInterface(Ci.nsITraceableChannel);
michael@0 751
michael@0 752 // The response will be written into the outputStream of this pipe.
michael@0 753 // This allows us to buffer the data we are receiving and read it
michael@0 754 // asynchronously.
michael@0 755 // Both ends of the pipe must be blocking.
michael@0 756 let sink = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
michael@0 757
michael@0 758 // The streams need to be blocking because this is required by the
michael@0 759 // stream tee.
michael@0 760 sink.init(false, false, this.responsePipeSegmentSize, PR_UINT32_MAX, null);
michael@0 761
michael@0 762 // Add listener for the response body.
michael@0 763 let newListener = new NetworkResponseListener(this, aHttpActivity);
michael@0 764
michael@0 765 // Remember the input stream, so it isn't released by GC.
michael@0 766 newListener.inputStream = sink.inputStream;
michael@0 767 newListener.sink = sink;
michael@0 768
michael@0 769 let tee = Cc["@mozilla.org/network/stream-listener-tee;1"].
michael@0 770 createInstance(Ci.nsIStreamListenerTee);
michael@0 771
michael@0 772 let originalListener = channel.setNewListener(tee);
michael@0 773
michael@0 774 tee.init(originalListener, sink.outputStream, newListener);
michael@0 775 },
michael@0 776
michael@0 777 /**
michael@0 778 * Handler for ACTIVITY_SUBTYPE_REQUEST_BODY_SENT. The request body is logged
michael@0 779 * here.
michael@0 780 *
michael@0 781 * @private
michael@0 782 * @param object aHttpActivity
michael@0 783 * The HTTP activity object we are working with.
michael@0 784 */
michael@0 785 _onRequestBodySent: function NM__onRequestBodySent(aHttpActivity)
michael@0 786 {
michael@0 787 if (aHttpActivity.discardRequestBody) {
michael@0 788 return;
michael@0 789 }
michael@0 790
michael@0 791 let sentBody = NetworkHelper.
michael@0 792 readPostTextFromRequest(aHttpActivity.channel,
michael@0 793 aHttpActivity.charset);
michael@0 794
michael@0 795 if (!sentBody && this.window &&
michael@0 796 aHttpActivity.url == this.window.location.href) {
michael@0 797 // If the request URL is the same as the current page URL, then
michael@0 798 // we can try to get the posted text from the page directly.
michael@0 799 // This check is necessary as otherwise the
michael@0 800 // NetworkHelper.readPostTextFromPageViaWebNav()
michael@0 801 // function is called for image requests as well but these
michael@0 802 // are not web pages and as such don't store the posted text
michael@0 803 // in the cache of the webpage.
michael@0 804 let webNav = this.window.QueryInterface(Ci.nsIInterfaceRequestor).
michael@0 805 getInterface(Ci.nsIWebNavigation);
michael@0 806 sentBody = NetworkHelper.
michael@0 807 readPostTextFromPageViaWebNav(webNav, aHttpActivity.charset);
michael@0 808 }
michael@0 809
michael@0 810 if (sentBody) {
michael@0 811 aHttpActivity.owner.addRequestPostData({ text: sentBody });
michael@0 812 }
michael@0 813 },
michael@0 814
michael@0 815 /**
michael@0 816 * Handler for ACTIVITY_SUBTYPE_RESPONSE_HEADER. This method stores
michael@0 817 * information about the response headers.
michael@0 818 *
michael@0 819 * @private
michael@0 820 * @param object aHttpActivity
michael@0 821 * The HTTP activity object we are working with.
michael@0 822 * @param string aExtraStringData
michael@0 823 * The uncached response headers.
michael@0 824 */
michael@0 825 _onResponseHeader:
michael@0 826 function NM__onResponseHeader(aHttpActivity, aExtraStringData)
michael@0 827 {
michael@0 828 // aExtraStringData contains the uncached response headers. The first line
michael@0 829 // contains the response status (e.g. HTTP/1.1 200 OK).
michael@0 830 //
michael@0 831 // Note: The response header is not saved here. Calling the
michael@0 832 // channel.visitResponseHeaders() methood at this point sometimes causes an
michael@0 833 // NS_ERROR_NOT_AVAILABLE exception.
michael@0 834 //
michael@0 835 // We could parse aExtraStringData to get the headers and their values, but
michael@0 836 // that is not trivial to do in an accurate manner. Hence, we save the
michael@0 837 // response headers in this._httpResponseExaminer().
michael@0 838
michael@0 839 let headers = aExtraStringData.split(/\r\n|\n|\r/);
michael@0 840 let statusLine = headers.shift();
michael@0 841 let statusLineArray = statusLine.split(" ");
michael@0 842
michael@0 843 let response = {};
michael@0 844 response.httpVersion = statusLineArray.shift();
michael@0 845 response.status = statusLineArray.shift();
michael@0 846 response.statusText = statusLineArray.join(" ");
michael@0 847 response.headersSize = aExtraStringData.length;
michael@0 848
michael@0 849 aHttpActivity.responseStatus = response.status;
michael@0 850
michael@0 851 // Discard the response body for known response statuses.
michael@0 852 switch (parseInt(response.status)) {
michael@0 853 case HTTP_MOVED_PERMANENTLY:
michael@0 854 case HTTP_FOUND:
michael@0 855 case HTTP_SEE_OTHER:
michael@0 856 case HTTP_TEMPORARY_REDIRECT:
michael@0 857 aHttpActivity.discardResponseBody = true;
michael@0 858 break;
michael@0 859 }
michael@0 860
michael@0 861 response.discardResponseBody = aHttpActivity.discardResponseBody;
michael@0 862
michael@0 863 aHttpActivity.owner.addResponseStart(response);
michael@0 864 },
michael@0 865
michael@0 866 /**
michael@0 867 * Handler for ACTIVITY_SUBTYPE_TRANSACTION_CLOSE. This method updates the HAR
michael@0 868 * timing information on the HTTP activity object and clears the request
michael@0 869 * from the list of known open requests.
michael@0 870 *
michael@0 871 * @private
michael@0 872 * @param object aHttpActivity
michael@0 873 * The HTTP activity object we work with.
michael@0 874 */
michael@0 875 _onTransactionClose: function NM__onTransactionClose(aHttpActivity)
michael@0 876 {
michael@0 877 let result = this._setupHarTimings(aHttpActivity);
michael@0 878 aHttpActivity.owner.addEventTimings(result.total, result.timings);
michael@0 879 delete this.openRequests[aHttpActivity.id];
michael@0 880 },
michael@0 881
michael@0 882 /**
michael@0 883 * Update the HTTP activity object to include timing information as in the HAR
michael@0 884 * spec. The HTTP activity object holds the raw timing information in
michael@0 885 * |timings| - these are timings stored for each activity notification. The
michael@0 886 * HAR timing information is constructed based on these lower level data.
michael@0 887 *
michael@0 888 * @param object aHttpActivity
michael@0 889 * The HTTP activity object we are working with.
michael@0 890 * @return object
michael@0 891 * This object holds two properties:
michael@0 892 * - total - the total time for all of the request and response.
michael@0 893 * - timings - the HAR timings object.
michael@0 894 */
michael@0 895 _setupHarTimings: function NM__setupHarTimings(aHttpActivity)
michael@0 896 {
michael@0 897 let timings = aHttpActivity.timings;
michael@0 898 let harTimings = {};
michael@0 899
michael@0 900 // Not clear how we can determine "blocked" time.
michael@0 901 harTimings.blocked = -1;
michael@0 902
michael@0 903 // DNS timing information is available only in when the DNS record is not
michael@0 904 // cached.
michael@0 905 harTimings.dns = timings.STATUS_RESOLVING && timings.STATUS_RESOLVED ?
michael@0 906 timings.STATUS_RESOLVED.last -
michael@0 907 timings.STATUS_RESOLVING.first : -1;
michael@0 908
michael@0 909 if (timings.STATUS_CONNECTING_TO && timings.STATUS_CONNECTED_TO) {
michael@0 910 harTimings.connect = timings.STATUS_CONNECTED_TO.last -
michael@0 911 timings.STATUS_CONNECTING_TO.first;
michael@0 912 }
michael@0 913 else if (timings.STATUS_SENDING_TO) {
michael@0 914 harTimings.connect = timings.STATUS_SENDING_TO.first -
michael@0 915 timings.REQUEST_HEADER.first;
michael@0 916 }
michael@0 917 else {
michael@0 918 harTimings.connect = -1;
michael@0 919 }
michael@0 920
michael@0 921 if ((timings.STATUS_WAITING_FOR || timings.STATUS_RECEIVING_FROM) &&
michael@0 922 (timings.STATUS_CONNECTED_TO || timings.STATUS_SENDING_TO)) {
michael@0 923 harTimings.send = (timings.STATUS_WAITING_FOR ||
michael@0 924 timings.STATUS_RECEIVING_FROM).first -
michael@0 925 (timings.STATUS_CONNECTED_TO ||
michael@0 926 timings.STATUS_SENDING_TO).last;
michael@0 927 }
michael@0 928 else {
michael@0 929 harTimings.send = -1;
michael@0 930 }
michael@0 931
michael@0 932 if (timings.RESPONSE_START) {
michael@0 933 harTimings.wait = timings.RESPONSE_START.first -
michael@0 934 (timings.REQUEST_BODY_SENT ||
michael@0 935 timings.STATUS_SENDING_TO).last;
michael@0 936 }
michael@0 937 else {
michael@0 938 harTimings.wait = -1;
michael@0 939 }
michael@0 940
michael@0 941 if (timings.RESPONSE_START && timings.RESPONSE_COMPLETE) {
michael@0 942 harTimings.receive = timings.RESPONSE_COMPLETE.last -
michael@0 943 timings.RESPONSE_START.first;
michael@0 944 }
michael@0 945 else {
michael@0 946 harTimings.receive = -1;
michael@0 947 }
michael@0 948
michael@0 949 let totalTime = 0;
michael@0 950 for (let timing in harTimings) {
michael@0 951 let time = Math.max(Math.round(harTimings[timing] / 1000), -1);
michael@0 952 harTimings[timing] = time;
michael@0 953 if (time > -1) {
michael@0 954 totalTime += time;
michael@0 955 }
michael@0 956 }
michael@0 957
michael@0 958 return {
michael@0 959 total: totalTime,
michael@0 960 timings: harTimings,
michael@0 961 };
michael@0 962 },
michael@0 963
michael@0 964 /**
michael@0 965 * Suspend Web Console activity. This is called when all Web Consoles are
michael@0 966 * closed.
michael@0 967 */
michael@0 968 destroy: function NM_destroy()
michael@0 969 {
michael@0 970 if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
michael@0 971 Services.obs.removeObserver(this._httpResponseExaminer,
michael@0 972 "http-on-examine-response");
michael@0 973 }
michael@0 974
michael@0 975 gActivityDistributor.removeObserver(this);
michael@0 976
michael@0 977 this.openRequests = {};
michael@0 978 this.openResponses = {};
michael@0 979 this.owner = null;
michael@0 980 this.window = null;
michael@0 981 this.topFrame = null;
michael@0 982 },
michael@0 983 }; // NetworkMonitor.prototype
michael@0 984
michael@0 985
michael@0 986 /**
michael@0 987 * The NetworkMonitorChild is used to proxy all of the network activity of the
michael@0 988 * child app process from the main process. The child WebConsoleActor creates an
michael@0 989 * instance of this object.
michael@0 990 *
michael@0 991 * Network requests for apps happen in the main process. As such,
michael@0 992 * a NetworkMonitor instance is used by the WebappsActor in the main process to
michael@0 993 * log the network requests for this child process.
michael@0 994 *
michael@0 995 * The main process creates NetworkEventActorProxy instances per request. These
michael@0 996 * send the data to this object using the nsIMessageManager. Here we proxy the
michael@0 997 * data to the WebConsoleActor or to a NetworkEventActor.
michael@0 998 *
michael@0 999 * @constructor
michael@0 1000 * @param number appId
michael@0 1001 * The web appId of the child process.
michael@0 1002 * @param nsIMessageManager messageManager
michael@0 1003 * The nsIMessageManager to use to communicate with the parent process.
michael@0 1004 * @param string connID
michael@0 1005 * The connection ID to use for send messages to the parent process.
michael@0 1006 * @param object owner
michael@0 1007 * The WebConsoleActor that is listening for the network requests.
michael@0 1008 */
michael@0 1009 function NetworkMonitorChild(appId, messageManager, connID, owner) {
michael@0 1010 this.appId = appId;
michael@0 1011 this.connID = connID;
michael@0 1012 this.owner = owner;
michael@0 1013 this._messageManager = messageManager;
michael@0 1014 this._onNewEvent = this._onNewEvent.bind(this);
michael@0 1015 this._onUpdateEvent = this._onUpdateEvent.bind(this);
michael@0 1016 this._netEvents = new Map();
michael@0 1017 }
michael@0 1018 exports.NetworkMonitorChild = NetworkMonitorChild;
michael@0 1019
michael@0 1020 NetworkMonitorChild.prototype = {
michael@0 1021 appId: null,
michael@0 1022 owner: null,
michael@0 1023 _netEvents: null,
michael@0 1024 _saveRequestAndResponseBodies: false,
michael@0 1025
michael@0 1026 get saveRequestAndResponseBodies() {
michael@0 1027 return this._saveRequestAndResponseBodies;
michael@0 1028 },
michael@0 1029
michael@0 1030 set saveRequestAndResponseBodies(val) {
michael@0 1031 this._saveRequestAndResponseBodies = val;
michael@0 1032
michael@0 1033 this._messageManager.sendAsyncMessage("debug:netmonitor:" + this.connID, {
michael@0 1034 appId: this.appId,
michael@0 1035 action: "setPreferences",
michael@0 1036 preferences: {
michael@0 1037 saveRequestAndResponseBodies: this._saveRequestAndResponseBodies,
michael@0 1038 },
michael@0 1039 });
michael@0 1040 },
michael@0 1041
michael@0 1042 init: function() {
michael@0 1043 let mm = this._messageManager;
michael@0 1044 mm.addMessageListener("debug:netmonitor:" + this.connID + ":newEvent",
michael@0 1045 this._onNewEvent);
michael@0 1046 mm.addMessageListener("debug:netmonitor:" + this.connID + ":updateEvent",
michael@0 1047 this._onUpdateEvent);
michael@0 1048 mm.sendAsyncMessage("debug:netmonitor:" + this.connID, {
michael@0 1049 appId: this.appId,
michael@0 1050 action: "start",
michael@0 1051 });
michael@0 1052 },
michael@0 1053
michael@0 1054 _onNewEvent: DevToolsUtils.makeInfallible(function _onNewEvent(msg) {
michael@0 1055 let {id, event} = msg.data;
michael@0 1056 let actor = this.owner.onNetworkEvent(event);
michael@0 1057 this._netEvents.set(id, Cu.getWeakReference(actor));
michael@0 1058 }),
michael@0 1059
michael@0 1060 _onUpdateEvent: DevToolsUtils.makeInfallible(function _onUpdateEvent(msg) {
michael@0 1061 let {id, method, args} = msg.data;
michael@0 1062 let weakActor = this._netEvents.get(id);
michael@0 1063 let actor = weakActor ? weakActor.get() : null;
michael@0 1064 if (!actor) {
michael@0 1065 Cu.reportError("Received debug:netmonitor:updateEvent for unknown event ID: " + id);
michael@0 1066 return;
michael@0 1067 }
michael@0 1068 if (!(method in actor)) {
michael@0 1069 Cu.reportError("Received debug:netmonitor:updateEvent unsupported method: " + method);
michael@0 1070 return;
michael@0 1071 }
michael@0 1072 actor[method].apply(actor, args);
michael@0 1073 }),
michael@0 1074
michael@0 1075 destroy: function() {
michael@0 1076 let mm = this._messageManager;
michael@0 1077 mm.removeMessageListener("debug:netmonitor:" + this.connID + ":newEvent",
michael@0 1078 this._onNewEvent);
michael@0 1079 mm.removeMessageListener("debug:netmonitor:" + this.connID + ":updateEvent",
michael@0 1080 this._onUpdateEvent);
michael@0 1081 mm.sendAsyncMessage("debug:netmonitor:" + this.connID, {
michael@0 1082 action: "disconnect",
michael@0 1083 });
michael@0 1084 this._netEvents.clear();
michael@0 1085 this._messageManager = null;
michael@0 1086 this.owner = null;
michael@0 1087 },
michael@0 1088 }; // NetworkMonitorChild.prototype
michael@0 1089
michael@0 1090 /**
michael@0 1091 * The NetworkEventActorProxy is used to send network request information from
michael@0 1092 * the main process to the child app process. One proxy is used per request.
michael@0 1093 * Similarly, one NetworkEventActor in the child app process is used per
michael@0 1094 * request. The client receives all network logs from the child actors.
michael@0 1095 *
michael@0 1096 * The child process has a NetworkMonitorChild instance that is listening for
michael@0 1097 * all network logging from the main process. The net monitor shim is used to
michael@0 1098 * proxy the data to the WebConsoleActor instance of the child process.
michael@0 1099 *
michael@0 1100 * @constructor
michael@0 1101 * @param nsIMessageManager messageManager
michael@0 1102 * The message manager for the child app process. This is used for
michael@0 1103 * communication with the NetworkMonitorChild instance of the process.
michael@0 1104 * @param string connID
michael@0 1105 * The connection ID to use to send messages to the child process.
michael@0 1106 */
michael@0 1107 function NetworkEventActorProxy(messageManager, connID) {
michael@0 1108 this.id = gSequenceId();
michael@0 1109 this.connID = connID;
michael@0 1110 this.messageManager = messageManager;
michael@0 1111 }
michael@0 1112 exports.NetworkEventActorProxy = NetworkEventActorProxy;
michael@0 1113
michael@0 1114 NetworkEventActorProxy.methodFactory = function(method) {
michael@0 1115 return DevToolsUtils.makeInfallible(function() {
michael@0 1116 let args = Array.slice(arguments);
michael@0 1117 let mm = this.messageManager;
michael@0 1118 mm.sendAsyncMessage("debug:netmonitor:" + this.connID + ":updateEvent", {
michael@0 1119 id: this.id,
michael@0 1120 method: method,
michael@0 1121 args: args,
michael@0 1122 });
michael@0 1123 }, "NetworkEventActorProxy." + method);
michael@0 1124 };
michael@0 1125
michael@0 1126 NetworkEventActorProxy.prototype = {
michael@0 1127 /**
michael@0 1128 * Initialize the network event. This method sends the network request event
michael@0 1129 * to the content process.
michael@0 1130 *
michael@0 1131 * @param object event
michael@0 1132 * Object describing the network request.
michael@0 1133 * @return object
michael@0 1134 * This object.
michael@0 1135 */
michael@0 1136 init: DevToolsUtils.makeInfallible(function(event)
michael@0 1137 {
michael@0 1138 let mm = this.messageManager;
michael@0 1139 mm.sendAsyncMessage("debug:netmonitor:" + this.connID + ":newEvent", {
michael@0 1140 id: this.id,
michael@0 1141 event: event,
michael@0 1142 });
michael@0 1143 return this;
michael@0 1144 }),
michael@0 1145 };
michael@0 1146
michael@0 1147 (function() {
michael@0 1148 // Listeners for new network event data coming from the NetworkMonitor.
michael@0 1149 let methods = ["addRequestHeaders", "addRequestCookies", "addRequestPostData",
michael@0 1150 "addResponseStart", "addResponseHeaders", "addResponseCookies",
michael@0 1151 "addResponseContent", "addEventTimings"];
michael@0 1152 let factory = NetworkEventActorProxy.methodFactory;
michael@0 1153 for (let method of methods) {
michael@0 1154 NetworkEventActorProxy.prototype[method] = factory(method);
michael@0 1155 }
michael@0 1156 })();
michael@0 1157
michael@0 1158
michael@0 1159 /**
michael@0 1160 * The NetworkMonitor manager used by the Webapps actor in the main process.
michael@0 1161 * This object uses the message manager to listen for requests from the child
michael@0 1162 * process to start/stop the network monitor.
michael@0 1163 *
michael@0 1164 * @constructor
michael@0 1165 * @param nsIDOMElement frame
michael@0 1166 * The browser frame to work with (mozbrowser).
michael@0 1167 * @param string id
michael@0 1168 * Instance identifier to use for messages.
michael@0 1169 */
michael@0 1170 function NetworkMonitorManager(frame, id)
michael@0 1171 {
michael@0 1172 this.id = id;
michael@0 1173 let mm = frame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager;
michael@0 1174 this.messageManager = mm;
michael@0 1175 this.frame = frame;
michael@0 1176 this.onNetMonitorMessage = this.onNetMonitorMessage.bind(this);
michael@0 1177 this.onNetworkEvent = this.onNetworkEvent.bind(this);
michael@0 1178
michael@0 1179 mm.addMessageListener("debug:netmonitor:" + id, this.onNetMonitorMessage);
michael@0 1180 }
michael@0 1181 exports.NetworkMonitorManager = NetworkMonitorManager;
michael@0 1182
michael@0 1183 NetworkMonitorManager.prototype = {
michael@0 1184 netMonitor: null,
michael@0 1185 frame: null,
michael@0 1186 messageManager: null,
michael@0 1187
michael@0 1188 /**
michael@0 1189 * Handler for "debug:monitor" messages received through the message manager
michael@0 1190 * from the content process.
michael@0 1191 *
michael@0 1192 * @param object msg
michael@0 1193 * Message from the content.
michael@0 1194 */
michael@0 1195 onNetMonitorMessage: DevToolsUtils.makeInfallible(function _onNetMonitorMessage(msg) {
michael@0 1196 let { action, appId } = msg.json;
michael@0 1197 // Pipe network monitor data from parent to child via the message manager.
michael@0 1198 switch (action) {
michael@0 1199 case "start":
michael@0 1200 if (!this.netMonitor) {
michael@0 1201 this.netMonitor = new NetworkMonitor({
michael@0 1202 topFrame: this.frame,
michael@0 1203 appId: appId,
michael@0 1204 }, this);
michael@0 1205 this.netMonitor.init();
michael@0 1206 }
michael@0 1207 break;
michael@0 1208
michael@0 1209 case "setPreferences": {
michael@0 1210 let {preferences} = msg.json;
michael@0 1211 for (let key of Object.keys(preferences)) {
michael@0 1212 if (key == "saveRequestAndResponseBodies" && this.netMonitor) {
michael@0 1213 this.netMonitor.saveRequestAndResponseBodies = preferences[key];
michael@0 1214 }
michael@0 1215 }
michael@0 1216 break;
michael@0 1217 }
michael@0 1218
michael@0 1219 case "stop":
michael@0 1220 if (this.netMonitor) {
michael@0 1221 this.netMonitor.destroy();
michael@0 1222 this.netMonitor = null;
michael@0 1223 }
michael@0 1224 break;
michael@0 1225
michael@0 1226 case "disconnect":
michael@0 1227 this.destroy();
michael@0 1228 break;
michael@0 1229 }
michael@0 1230 }),
michael@0 1231
michael@0 1232 /**
michael@0 1233 * Handler for new network requests. This method is invoked by the current
michael@0 1234 * NetworkMonitor instance.
michael@0 1235 *
michael@0 1236 * @param object event
michael@0 1237 * Object describing the network request.
michael@0 1238 * @return object
michael@0 1239 * A NetworkEventActorProxy instance which is notified when further
michael@0 1240 * data about the request is available.
michael@0 1241 */
michael@0 1242 onNetworkEvent: DevToolsUtils.makeInfallible(function _onNetworkEvent(event) {
michael@0 1243 return new NetworkEventActorProxy(this.messageManager, this.id).init(event);
michael@0 1244 }),
michael@0 1245
michael@0 1246 destroy: function()
michael@0 1247 {
michael@0 1248 if (this.messageManager) {
michael@0 1249 this.messageManager.removeMessageListener("debug:netmonitor:" + this.id,
michael@0 1250 this.onNetMonitorMessage);
michael@0 1251 }
michael@0 1252 this.messageManager = null;
michael@0 1253 this.filters = null;
michael@0 1254
michael@0 1255 if (this.netMonitor) {
michael@0 1256 this.netMonitor.destroy();
michael@0 1257 this.netMonitor = null;
michael@0 1258 }
michael@0 1259 },
michael@0 1260 }; // NetworkMonitorManager.prototype
michael@0 1261
michael@0 1262
michael@0 1263 /**
michael@0 1264 * A WebProgressListener that listens for location changes.
michael@0 1265 *
michael@0 1266 * This progress listener is used to track file loads and other kinds of
michael@0 1267 * location changes.
michael@0 1268 *
michael@0 1269 * @constructor
michael@0 1270 * @param object aWindow
michael@0 1271 * The window for which we need to track location changes.
michael@0 1272 * @param object aOwner
michael@0 1273 * The listener owner which needs to implement two methods:
michael@0 1274 * - onFileActivity(aFileURI)
michael@0 1275 * - onLocationChange(aState, aTabURI, aPageTitle)
michael@0 1276 */
michael@0 1277 function ConsoleProgressListener(aWindow, aOwner)
michael@0 1278 {
michael@0 1279 this.window = aWindow;
michael@0 1280 this.owner = aOwner;
michael@0 1281 }
michael@0 1282 exports.ConsoleProgressListener = ConsoleProgressListener;
michael@0 1283
michael@0 1284 ConsoleProgressListener.prototype = {
michael@0 1285 /**
michael@0 1286 * Constant used for startMonitor()/stopMonitor() that tells you want to
michael@0 1287 * monitor file loads.
michael@0 1288 */
michael@0 1289 MONITOR_FILE_ACTIVITY: 1,
michael@0 1290
michael@0 1291 /**
michael@0 1292 * Constant used for startMonitor()/stopMonitor() that tells you want to
michael@0 1293 * monitor page location changes.
michael@0 1294 */
michael@0 1295 MONITOR_LOCATION_CHANGE: 2,
michael@0 1296
michael@0 1297 /**
michael@0 1298 * Tells if you want to monitor file activity.
michael@0 1299 * @private
michael@0 1300 * @type boolean
michael@0 1301 */
michael@0 1302 _fileActivity: false,
michael@0 1303
michael@0 1304 /**
michael@0 1305 * Tells if you want to monitor location changes.
michael@0 1306 * @private
michael@0 1307 * @type boolean
michael@0 1308 */
michael@0 1309 _locationChange: false,
michael@0 1310
michael@0 1311 /**
michael@0 1312 * Tells if the console progress listener is initialized or not.
michael@0 1313 * @private
michael@0 1314 * @type boolean
michael@0 1315 */
michael@0 1316 _initialized: false,
michael@0 1317
michael@0 1318 _webProgress: null,
michael@0 1319
michael@0 1320 QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
michael@0 1321 Ci.nsISupportsWeakReference]),
michael@0 1322
michael@0 1323 /**
michael@0 1324 * Initialize the ConsoleProgressListener.
michael@0 1325 * @private
michael@0 1326 */
michael@0 1327 _init: function CPL__init()
michael@0 1328 {
michael@0 1329 if (this._initialized) {
michael@0 1330 return;
michael@0 1331 }
michael@0 1332
michael@0 1333 this._webProgress = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 1334 .getInterface(Ci.nsIWebNavigation)
michael@0 1335 .QueryInterface(Ci.nsIWebProgress);
michael@0 1336 this._webProgress.addProgressListener(this,
michael@0 1337 Ci.nsIWebProgress.NOTIFY_STATE_ALL);
michael@0 1338
michael@0 1339 this._initialized = true;
michael@0 1340 },
michael@0 1341
michael@0 1342 /**
michael@0 1343 * Start a monitor/tracker related to the current nsIWebProgressListener
michael@0 1344 * instance.
michael@0 1345 *
michael@0 1346 * @param number aMonitor
michael@0 1347 * Tells what you want to track. Available constants:
michael@0 1348 * - this.MONITOR_FILE_ACTIVITY
michael@0 1349 * Track file loads.
michael@0 1350 * - this.MONITOR_LOCATION_CHANGE
michael@0 1351 * Track location changes for the top window.
michael@0 1352 */
michael@0 1353 startMonitor: function CPL_startMonitor(aMonitor)
michael@0 1354 {
michael@0 1355 switch (aMonitor) {
michael@0 1356 case this.MONITOR_FILE_ACTIVITY:
michael@0 1357 this._fileActivity = true;
michael@0 1358 break;
michael@0 1359 case this.MONITOR_LOCATION_CHANGE:
michael@0 1360 this._locationChange = true;
michael@0 1361 break;
michael@0 1362 default:
michael@0 1363 throw new Error("ConsoleProgressListener: unknown monitor type " +
michael@0 1364 aMonitor + "!");
michael@0 1365 }
michael@0 1366 this._init();
michael@0 1367 },
michael@0 1368
michael@0 1369 /**
michael@0 1370 * Stop a monitor.
michael@0 1371 *
michael@0 1372 * @param number aMonitor
michael@0 1373 * Tells what you want to stop tracking. See this.startMonitor() for
michael@0 1374 * the list of constants.
michael@0 1375 */
michael@0 1376 stopMonitor: function CPL_stopMonitor(aMonitor)
michael@0 1377 {
michael@0 1378 switch (aMonitor) {
michael@0 1379 case this.MONITOR_FILE_ACTIVITY:
michael@0 1380 this._fileActivity = false;
michael@0 1381 break;
michael@0 1382 case this.MONITOR_LOCATION_CHANGE:
michael@0 1383 this._locationChange = false;
michael@0 1384 break;
michael@0 1385 default:
michael@0 1386 throw new Error("ConsoleProgressListener: unknown monitor type " +
michael@0 1387 aMonitor + "!");
michael@0 1388 }
michael@0 1389
michael@0 1390 if (!this._fileActivity && !this._locationChange) {
michael@0 1391 this.destroy();
michael@0 1392 }
michael@0 1393 },
michael@0 1394
michael@0 1395 onStateChange:
michael@0 1396 function CPL_onStateChange(aProgress, aRequest, aState, aStatus)
michael@0 1397 {
michael@0 1398 if (!this.owner) {
michael@0 1399 return;
michael@0 1400 }
michael@0 1401
michael@0 1402 if (this._fileActivity) {
michael@0 1403 this._checkFileActivity(aProgress, aRequest, aState, aStatus);
michael@0 1404 }
michael@0 1405
michael@0 1406 if (this._locationChange) {
michael@0 1407 this._checkLocationChange(aProgress, aRequest, aState, aStatus);
michael@0 1408 }
michael@0 1409 },
michael@0 1410
michael@0 1411 /**
michael@0 1412 * Check if there is any file load, given the arguments of
michael@0 1413 * nsIWebProgressListener.onStateChange. If the state change tells that a file
michael@0 1414 * URI has been loaded, then the remote Web Console instance is notified.
michael@0 1415 * @private
michael@0 1416 */
michael@0 1417 _checkFileActivity:
michael@0 1418 function CPL__checkFileActivity(aProgress, aRequest, aState, aStatus)
michael@0 1419 {
michael@0 1420 if (!(aState & Ci.nsIWebProgressListener.STATE_START)) {
michael@0 1421 return;
michael@0 1422 }
michael@0 1423
michael@0 1424 let uri = null;
michael@0 1425 if (aRequest instanceof Ci.imgIRequest) {
michael@0 1426 let imgIRequest = aRequest.QueryInterface(Ci.imgIRequest);
michael@0 1427 uri = imgIRequest.URI;
michael@0 1428 }
michael@0 1429 else if (aRequest instanceof Ci.nsIChannel) {
michael@0 1430 let nsIChannel = aRequest.QueryInterface(Ci.nsIChannel);
michael@0 1431 uri = nsIChannel.URI;
michael@0 1432 }
michael@0 1433
michael@0 1434 if (!uri || !uri.schemeIs("file") && !uri.schemeIs("ftp")) {
michael@0 1435 return;
michael@0 1436 }
michael@0 1437
michael@0 1438 this.owner.onFileActivity(uri.spec);
michael@0 1439 },
michael@0 1440
michael@0 1441 /**
michael@0 1442 * Check if the current window.top location is changing, given the arguments
michael@0 1443 * of nsIWebProgressListener.onStateChange. If that is the case, the remote
michael@0 1444 * Web Console instance is notified.
michael@0 1445 * @private
michael@0 1446 */
michael@0 1447 _checkLocationChange:
michael@0 1448 function CPL__checkLocationChange(aProgress, aRequest, aState, aStatus)
michael@0 1449 {
michael@0 1450 let isStart = aState & Ci.nsIWebProgressListener.STATE_START;
michael@0 1451 let isStop = aState & Ci.nsIWebProgressListener.STATE_STOP;
michael@0 1452 let isNetwork = aState & Ci.nsIWebProgressListener.STATE_IS_NETWORK;
michael@0 1453 let isWindow = aState & Ci.nsIWebProgressListener.STATE_IS_WINDOW;
michael@0 1454
michael@0 1455 // Skip non-interesting states.
michael@0 1456 if (!isNetwork || !isWindow || aProgress.DOMWindow != this.window) {
michael@0 1457 return;
michael@0 1458 }
michael@0 1459
michael@0 1460 if (isStart && aRequest instanceof Ci.nsIChannel) {
michael@0 1461 this.owner.onLocationChange("start", aRequest.URI.spec, "");
michael@0 1462 }
michael@0 1463 else if (isStop) {
michael@0 1464 this.owner.onLocationChange("stop", this.window.location.href,
michael@0 1465 this.window.document.title);
michael@0 1466 }
michael@0 1467 },
michael@0 1468
michael@0 1469 onLocationChange: function() {},
michael@0 1470 onStatusChange: function() {},
michael@0 1471 onProgressChange: function() {},
michael@0 1472 onSecurityChange: function() {},
michael@0 1473
michael@0 1474 /**
michael@0 1475 * Destroy the ConsoleProgressListener.
michael@0 1476 */
michael@0 1477 destroy: function CPL_destroy()
michael@0 1478 {
michael@0 1479 if (!this._initialized) {
michael@0 1480 return;
michael@0 1481 }
michael@0 1482
michael@0 1483 this._initialized = false;
michael@0 1484 this._fileActivity = false;
michael@0 1485 this._locationChange = false;
michael@0 1486
michael@0 1487 try {
michael@0 1488 this._webProgress.removeProgressListener(this);
michael@0 1489 }
michael@0 1490 catch (ex) {
michael@0 1491 // This can throw during browser shutdown.
michael@0 1492 }
michael@0 1493
michael@0 1494 this._webProgress = null;
michael@0 1495 this.window = null;
michael@0 1496 this.owner = null;
michael@0 1497 },
michael@0 1498 }; // ConsoleProgressListener.prototype
michael@0 1499
michael@0 1500 function gSequenceId() { return gSequenceId.n++; }
michael@0 1501 gSequenceId.n = 1;

mercurial