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