1 /* See license.txt for terms of usage */
  2 
  3 define([
  4     "firebug/lib/xpcom",
  5     "firebug/lib/trace",
  6     "firebug/lib/http"
  7 ],
  8 function(Xpcom, FBTrace, Http) {
  9 
 10 // ********************************************************************************************* //
 11 // Constants
 12 
 13 const Cc = Components.classes;
 14 const Ci = Components.interfaces;
 15 const Cr = Components.results;
 16 const Cu = Components.utils;
 17 
 18 const PrefService = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
 19 var redirectionLimit = PrefService.getIntPref("network.http.redirection-limit");
 20 
 21 // ********************************************************************************************* //
 22 // ChannelListener implementation
 23 
 24 /**
 25  * This object implements nsIStreamListener interface and is intended to monitor all network
 26  * channels (nsIHttpChannel). A new instance of this object is created and registered an HTTP
 27  * channel. See Firebug.TabCacheModel.onExamineResponse method.
 28  */
 29 function ChannelListener(win, request, listener)
 30 /** lends ChannelListener */
 31 {
 32     this.window = win;
 33     this.request = request;
 34     this.proxyListener = listener;
 35 
 36     this.endOfLine = false;
 37     this.ignore = false;
 38 
 39     // The original channel listener (see nsITraceableChannel for more).
 40     this.listener = null;
 41 
 42     // The response will be written into the outputStream of this pipe.
 43     // Both ends of the pipe must be blocking.
 44     this.sink = Xpcom.CCIN("@mozilla.org/pipe;1", "nsIPipe");
 45     this.sink.init(false, false, 0x20000, 0x4000, null);
 46 
 47     // Remember the input stream, so it isn't released by GC.
 48     // See issue 2788 for more details.
 49     this.inputStream = this.sink.inputStream;
 50 
 51     this.downloadCounter = 0;
 52 
 53     // Add tee listener into the chain of request stream listeners so, the chain
 54     // doesn't include a JS code. This way all exceptions are propertly distributed
 55     // (#515051).
 56     var tee = Xpcom.CCIN("@mozilla.org/network/stream-listener-tee;1", "nsIStreamListenerTee");
 57     tee = tee.QueryInterface(Ci.nsIStreamListenerTee);
 58 
 59     var originalListener = request.setNewListener(tee);
 60     tee.init(originalListener, this.sink.outputStream, this);
 61 }
 62 
 63 ChannelListener.prototype =
 64 {
 65     setAsyncListener: function(request, stream, listener)
 66     {
 67         try
 68         {
 69             // xxxHonza: is there any other way how to find out the stream is closed?
 70             // Throws NS_BASE_STREAM_CLOSED if the stream is closed normally or at end-of-file.
 71             var available = stream.available();
 72         }
 73         catch (err)
 74         {
 75             if (err.name == "NS_BASE_STREAM_CLOSED")
 76             {
 77                 if (FBTrace.DBG_CACHE)
 78                     FBTrace.sysout("ChannelListener.setAsyncListener; " +
 79                         "Don't set, the stream is closed.");
 80                 return;
 81             }
 82 
 83             if (FBTrace.DBG_CACHE || FBTrace.DBG_ERRORS)
 84                 FBTrace.sysout("ChannelListener.setAsyncListener; EXCEPTION " +
 85                     Http.safeGetRequestName(request), err);
 86             return;
 87         }
 88 
 89         try
 90         {
 91             // Asynchronously wait for the stream to be readable or closed.
 92             stream.asyncWait(listener, 0, 0, null);
 93         }
 94         catch (err)
 95         {
 96             if (FBTrace.DBG_CACHE || FBTrace.DBG_ERRORS)
 97                 FBTrace.sysout("ChannelListener.setAsyncListener; EXCEPTION " +
 98                     Http.safeGetRequestName(request), err);
 99         }
100     },
101 
102     onCollectData: function(request, context, inputStream, offset, count)
103     {
104         if (FBTrace.DBG_CACHE && this.ignore)
105             FBTrace.sysout("ChannelListener.onCollectData; IGNORE stopping further onCollectData");
106 
107         try
108         {
109             if (this.sink)
110             {
111                 var bis = Xpcom.CCIN("@mozilla.org/binaryinputstream;1", "nsIBinaryInputStream");
112                 bis.setInputStream(inputStream);
113                 var data = bis.readBytes(count);
114 
115                 // Data from the pipe has been consumed (to avoid mem leaks) so, we can end now.
116                 if (this.ignore)
117                     return;
118             }
119             else
120             {
121                 // In this case, we don't need to read the data.
122                 if (this.ignore)
123                     return;
124 
125                 var binaryInputStream =
126                     Xpcom.CCIN("@mozilla.org/binaryinputstream;1", "nsIBinaryInputStream");
127                 var storageStream =
128                     Xpcom.CCIN("@mozilla.org/storagestream;1", "nsIStorageStream");
129                 var binaryOutputStream =
130                     Xpcom.CCIN("@mozilla.org/binaryoutputstream;1", "nsIBinaryOutputStream");
131 
132                 binaryInputStream.setInputStream(inputStream);
133                 storageStream.init(8192, count, null);
134                 binaryOutputStream.setOutputStream(storageStream.getOutputStream(0));
135 
136                 var data = binaryInputStream.readBytes(count);
137                 binaryOutputStream.writeBytes(data, count);
138             }
139 
140             // Avoid creating additional empty line if response comes in more pieces
141             // and the split is made just between "\r" and "\n" (Win line-end).
142             // So, if the response starts with "\n" while the previous part ended with "\r",
143             // remove the first character.
144             if (this.endOfLine && data.length && data[0] == "\n")
145                 data = data.substring(1);
146 
147             if (data.length)
148                 this.endOfLine = data[data.length-1] == "\r";
149 
150             // If the method returns false, the rest of the response is ignored (not cached).
151             // This is used to limit size of a cached response.
152             if (!this.proxyListener.onCollectData(request, data, offset))
153             {
154                 this.ignore = true;
155             }
156 
157             // Let other listeners use the stream.
158             if (storageStream)
159                 return storageStream.newInputStream(0);
160         }
161         catch (err)
162         {
163             if (FBTrace.DBG_CACHE || FBTrace.DBG_ERRORS)
164                 FBTrace.sysout("ChannelListener.onCollectData EXCEPTION\n", err);
165         }
166 
167         return null;
168     },
169 
170     /* nsIStreamListener */
171     onDataAvailable: function(request, requestContext, inputStream, offset, count)
172     {
173         try
174         {
175             // Force a garbage collection cycle, see:
176             // https://bugzilla.mozilla.org/show_bug.cgi?id=638075
177             this.downloadCounter += count;
178             if (this.downloadCounter > (1024*1024*2))
179             {
180                 this.downloadCounter = 0;
181                 Cu.forceGC();
182             }
183 
184             var newStream = this.proxyListener.onDataAvailable(request, requestContext,
185                 inputStream, offset, count);
186 
187             if (newStream)
188                 inputStream = newStream;
189 
190             newStream = this.onCollectData(request, null, inputStream, offset, count);
191             if (newStream)
192                 inputStream = newStream;
193         }
194         catch (err)
195         {
196             if (FBTrace.DBG_CACHE || FBTrace.DBG_ERRORS)
197                 FBTrace.sysout("ChannelListener.onDataAvailable onCollectData FAILS " +
198                     "(" + offset + ", " + count + ") EXCEPTION: " +
199                     Http.safeGetRequestName(request), err);
200         }
201 
202         if (this.listener)
203         {
204             try  // https://bugzilla.mozilla.org/show_bug.cgi?id=492534
205             {
206                 this.listener.onDataAvailable(request, requestContext, inputStream, offset, count);
207             }
208             catch(exc)
209             {
210                 if (FBTrace.DBG_CACHE)
211                     FBTrace.sysout("ChannelListener.onDataAvailable canceling request at " +
212                         "(" + offset + ", " + count + ") EXCEPTION: " +
213                         Http.safeGetRequestName(request), exc);
214 
215                 request.cancel(exc.result);
216             }
217         }
218     },
219 
220     onStartRequest: function(request, requestContext)
221     {
222         try
223         {
224             this.request = request.QueryInterface(Ci.nsIHttpChannel);
225 
226             if (FBTrace.DBG_CACHE)
227                 FBTrace.sysout("ChannelListener.onStartRequest; " +
228                     this.request.contentType + ", " + Http.safeGetRequestName(this.request));
229 
230             // Don't register listener twice (redirects, see also bug529536).
231             // xxxHonza: I don't know any way how to find out that a listener
232             // has been already registered for the channel. So, use the redirection limit
233             // to see that the channel has been redirected and so, listener is there.
234             if (request.redirectionLimit < redirectionLimit)
235             {
236                 if (FBTrace.DBG_CACHE)
237                     FBTrace.sysout("ChannelListener.onStartRequest; redirected request " +
238                         request.redirectionLimit + " (max=" + redirectionLimit + ")");
239                 return;
240             }
241 
242             // Due to #489317, the check whether this response should be cached
243             // must be done here (the content type is not valid before calling
244             // onStartRequest). Let's ignore the response if it should not be cached.
245             this.ignore = !this.proxyListener.shouldCacheRequest(request);
246 
247             // Notify proxy listener.
248             this.proxyListener.onStartRequest(request, requestContext);
249 
250             // Listen for incoming data.
251             if (FBTrace.DBG_CACHE && !this.sink)
252                 FBTrace.sysout("ChannelListener.onStartRequest NO SINK stopping setAsyncListener");
253 
254             if (FBTrace.DBG_CACHE && this.ignore && this.sink)
255                 FBTrace.sysout("ChannelListener.onStartRequest IGNORE(shouldCacheRequest) " +
256                     "stopping setAsyncListener");
257 
258             // Even if the response is marked as ignored we need to read the sink
259             // to avoid mem leaks.
260             if (this.sink)
261                 this.setAsyncListener(request, this.sink.inputStream, this);
262         }
263         catch (err)
264         {
265             if (FBTrace.DBG_CACHE || FBTrace.DBG_ERRORS)
266                 FBTrace.sysout("ChannelListener.onStartRequest EXCEPTION\n", err);
267         }
268 
269         if (this.listener)
270         {
271             try  // https://bugzilla.mozilla.org/show_bug.cgi?id=492534
272             {
273                 this.listener.onStartRequest(request, requestContext);
274             }
275             catch(exc)
276             {
277                 if (FBTrace.DBG_CACHE)
278                     FBTrace.sysout("ChannelListener.onStartRequest canceling request " +
279                     "EXCEPTION: " + Http.safeGetRequestName(request), exc);
280 
281                 request.cancel(exc.result);
282             }
283         }
284     },
285 
286     onStopRequest: function(request, requestContext, statusCode)
287     {
288         try
289         {
290             if (FBTrace.DBG_CACHE)
291                 FBTrace.sysout("ChannelListener.onStopRequest; " +
292                     request.contentType + ", " + Http.safeGetRequestName(request));
293 
294             this.proxyListener.onStopRequest(request, requestContext, statusCode);
295         }
296         catch (err)
297         {
298             if (FBTrace.DBG_CACHE || FBTrace.DBG_ERRORS)
299                 FBTrace.sysout("ChannelListener.onStopRequest EXCEPTION\n", err);
300         }
301 
302         // The request body has been downloaded. Remove the listener (the last parameter
303         // is null) since it's not needed now.
304         if (this.sink)
305             this.setAsyncListener(request, this.sink.inputStream, null);
306 
307         if (this.listener)
308             this.listener.onStopRequest(request, requestContext, statusCode);
309     },
310 
311     /* nsITraceableChannel */
312     setNewListener: function(listener)
313     {
314         this.proxyListener = listener;
315         return null;
316     },
317 
318     /* nsIInputStreamCallback */
319     onInputStreamReady : function(stream)
320     {
321         try
322         {
323             if (FBTrace.DBG_CACHE)
324                 FBTrace.sysout("ChannelListener.onInputStreamReady " +
325                     Http.safeGetRequestName(this.request));
326 
327             if (stream instanceof Ci.nsIAsyncInputStream)
328             {
329                 try
330                 {
331                     var offset = stream.tell();
332                     var available = stream.available();
333                     this.onDataAvailable(this.request, null, stream, offset, available);
334                 }
335                 catch (err)
336                 {
337                     // stream.available throws an exception if the stream is closed,
338                     // which is ok, since this callback can be called even in this
339                     // situations.
340                     if (FBTrace.DBG_CACHE)
341                         FBTrace.sysout("ChannelListener.onInputStreamReady EXCEPTION calling onDataAvailable: " +
342                             Http.safeGetRequestName(this.request), err);
343                 }
344 
345                 // Listen for further incoming data.
346                 if (FBTrace.DBG_CACHE && this.ignore)
347                     FBTrace.sysout("ChannelListener.onInputStreamReady IGNORE stopping setAsyncListener");
348 
349                 this.setAsyncListener(this.request, stream, this);
350             }
351             else
352             {
353                 if (FBTrace.DBG_CACHE)
354                     FBTrace.sysout("ChannelListener.onInputStreamReady NOT a nsIAsyncInputStream",stream);
355             }
356         }
357         catch (err)
358         {
359             if (FBTrace.DBG_CACHE || FBTrace.DBG_ERRORS)
360                 FBTrace.sysout("ChannelListener.onInputStreamReady EXCEPTION " +
361                     Http.safeGetRequestName(this.request), err);
362         }
363     },
364 
365     /* nsISupports */
366     QueryInterface: function(iid)
367     {
368         if (iid.equals(Ci.nsIStreamListener) ||
369             iid.equals(Ci.nsIInputStreamCallback) ||
370             iid.equals(Ci.nsISupportsWeakReference) ||
371             iid.equals(Ci.nsITraceableChannel) ||
372             iid.equals(Ci.nsISupports))
373         {
374             return this;
375         }
376 
377         throw Components.results.NS_NOINTERFACE;
378     },
379 }
380 
381 // ********************************************************************************************* //
382 
383 var HttpResponseObserver =
384 {
385     register: function(win, request, listener)
386     {
387         if (request instanceof Ci.nsITraceableChannel)
388             return new ChannelListener(win, request, listener);
389 
390         return null;
391     }
392 }
393 
394 return HttpResponseObserver;
395 
396 // ********************************************************************************************* //
397 });
398