1 /* See license.txt for terms of usage */
  2 
  3 // ************************************************************************************************
  4 // Constants
  5 
  6 const CLASS_ID = Components.ID("{5AAEB534-FA57-488d-9A73-20C258FC7BDB}");
  7 const CLASS_NAME = "Firebug Channel Listener";
  8 const CONTRACT_ID = "@joehewitt.com/firebug-channel-listener;1";
  9 
 10 const Cc = Components.classes;
 11 const Ci = Components.interfaces;
 12 const Cr = Components.results;
 13 
 14 var FBTrace = {DBG_FAKE: "fake"};
 15 
 16 // ************************************************************************************************
 17 // ChannelListener implementation
 18 
 19 /**
 20  * This object implements nsIStreamListener interface and is intended to monitor all network
 21  * channels (nsIHttpChannel). A new instance of this object is created and registered an HTTP
 22  * channel. See Firebug.TabCacheModel.onExamineResponse method.
 23  */
 24 function ChannelListener()
 25 {
 26     this.wrappedJSObject = this;
 27 
 28     this.window = null;
 29     this.request = null;
 30 
 31     this.endOfLine = false;
 32     this.ignore = false;
 33 
 34     // The original channel listener (see nsITraceableChannel for more).
 35     this.listener = null;
 36 
 37     // The proxy listener is used to send events to possible listeners (e.g. net panel)
 38     // and properly pass the request object through nsIStreamListner to chrome-window space.
 39     this.proxyListener = null;
 40 
 41     // The response will be written into the outputStream of this pipe.
 42     // Both ends of the pipe must be blocking. Initialized in TabCacheModel.registerStreamListener.
 43     this.sink = null;
 44 
 45     if (FBTrace.DBG_FAKE)  // cause the detrace to remove this statement and check for cached tracer
 46     {
 47         FBTrace = Cc["@joehewitt.com/firebug-trace-service;1"].getService(Ci.nsISupports)
 48             .wrappedJSObject.getTracer("extensions.firebug");
 49     }
 50 }
 51 
 52 ChannelListener.prototype =
 53 {
 54     setAsyncListener: function(request, stream, listener)
 55     {
 56         try
 57         {
 58             // xxxHonza: is there any other way how to find out the stream is closed?
 59             // Throws NS_BASE_STREAM_CLOSED if the stream is closed normally or at end-of-file.
 60             var available = stream.available();
 61         }
 62         catch (err)
 63         {
 64             if (err.name == "NS_BASE_STREAM_CLOSED")
 65             {
 66                 if (FBTrace.DBG_CACHE)
 67                     FBTrace.sysout("tabCache.ChannelListener.setAsyncListener; " +
 68                         "Don't set, the stream is closed.");
 69                 return;
 70             }
 71 
 72             if (FBTrace.DBG_CACHE || FBTrace.DBG_ERRORS)
 73                 FBTrace.sysout("tabCache.ChannelListener.setAsyncListener; EXCEPTION " +
 74                     safeGetName(request), err);
 75             return;
 76         }
 77 
 78         try
 79         {
 80             // Asynchronously wait for the stream to be readable or closed.
 81             stream.asyncWait(listener, 0, 0, null);
 82         }
 83         catch (err)
 84         {
 85             if (FBTrace.DBG_CACHE || FBTrace.DBG_ERRORS)
 86                 FBTrace.sysout("tabCache.ChannelListener.setAsyncListener; EXCEPTION " +
 87                     safeGetName(request), err);
 88         }
 89     },
 90 
 91     onCollectData: function(request, context, inputStream, offset, count)
 92     {
 93         if (this.ignore)
 94             return;
 95 
 96         try
 97         {
 98             if (this.sink)
 99             {
100                 var bis = CCIN("@mozilla.org/binaryinputstream;1", "nsIBinaryInputStream");
101                 bis.setInputStream(inputStream);
102                 var data = bis.readBytes(count);
103             }
104             else
105             {
106                 var binaryInputStream = CCIN("@mozilla.org/binaryinputstream;1", "nsIBinaryInputStream");
107                 var storageStream = CCIN("@mozilla.org/storagestream;1", "nsIStorageStream");
108                 var binaryOutputStream = CCIN("@mozilla.org/binaryoutputstream;1", "nsIBinaryOutputStream");
109 
110                 binaryInputStream.setInputStream(inputStream);
111                 storageStream.init(8192, count, null);
112                 binaryOutputStream.setOutputStream(storageStream.getOutputStream(0));
113 
114                 var data = binaryInputStream.readBytes(count);
115                 binaryOutputStream.writeBytes(data, count);
116             }
117 
118             // Avoid creating additional empty line if response comes in more pieces
119             // and the split is made just between "\r" and "\n" (Win line-end).
120             // So, if the response starts with "\n" while the previous part ended with "\r",
121             // remove the first character.
122             if (this.endOfLine && data.length && data[0] == "\n")
123                 data = data.substring(1);
124 
125             if (data.length)
126                 this.endOfLine = data[data.length-1] == "\r";
127 
128             // Store received data into the cache as they come. If the method returns
129             // false, the rest of the response is ignored (not cached). This is used
130             // to limit size of a cached response.
131             if (!context.sourceCache.storePartialResponse(request, data, this.window))
132                 this.ignore = true;
133 
134             // Let other listeners use the stream.
135             if (storageStream)
136                 return storageStream.newInputStream(0);
137         }
138         catch (err)
139         {
140             if (FBTrace.DBG_CACHE || FBTrace.DBG_ERRORS)
141                 FBTrace.sysout("tabCache.ChannelListener.onCollectData EXCEPTION\n", err);
142         }
143 
144         return null;
145     },
146 
147     /* nsIStreamListener */
148     onDataAvailable: function(request, requestContext, inputStream, offset, count)
149     {
150         try
151         {
152             // Use wrappedJSObject to bypass IDL definition that doesn't return any value.
153             var newStream = this.proxyListener.wrappedJSObject.onDataAvailable(request, requestContext,
154                 inputStream, offset, count);
155 
156             if (newStream)
157                 inputStream = newStream;
158 
159             var context = this.getContext(this.window);
160             if (context)
161             {
162                 newStream = this.onCollectData(request, context, inputStream, offset, count);
163                 if (newStream)
164                     inputStream = newStream;
165             }
166         }
167         catch (err)
168         {
169             if (FBTrace.DBG_CACHE || FBTrace.DBG_ERRORS)
170                 FBTrace.sysout("tabCache.ChannelListener.onDataAvailable onCollectData FAILS " +
171                     "(" + offset + ", " + count + ") EXCEPTION: " +
172                     safeGetName(request), err);
173         }
174 
175         if (this.listener)
176         {
177             try  // https://bugzilla.mozilla.org/show_bug.cgi?id=492534
178             {
179                 this.listener.onDataAvailable(request, requestContext, inputStream, offset, count);
180             }
181             catch(exc)
182             {
183                 if (FBTrace.DBG_CACHE)
184                     FBTrace.sysout("tabCache.ChannelListener.onDataAvailable cancelling request at " +
185                     "(" + offset + ", " + count + ") EXCEPTION: " +
186                     safeGetName(request), exc);
187 
188                 request.cancel(exc.result);
189             }
190         }
191     },
192 
193     onStartRequest: function(request, requestContext)
194     {
195         try
196         {
197             this.request = request.QueryInterface(Ci.nsIHttpChannel);
198 
199             if (FBTrace.DBG_CACHE)
200                 FBTrace.sysout("tabCache.ChannelListener.onStartRequest; " +
201                     request.contentType + ", " + safeGetName(request));
202 
203             // Pass to the proxy only if the associated context exists (the window is not unloaded)
204             var context = this.getContext(this.window);
205             if (context)
206             {
207                 // Due to #489317, the check whether this response should be cached
208                 // must be done here (the content type is not valid before calling
209                 // onStartRequest). Let's ignore the response if it should not be cached.
210                 this.ignore = !this.shouldCacheRequest(request);
211 
212                 // Notify proxy listener.
213                 this.proxyListener.onStartRequest(request, requestContext);
214 
215                 // Listen for incoming data.
216                 if (!this.ignore && this.sink)
217                     this.setAsyncListener(request, this.sink.inputStream, this);
218             }
219         }
220         catch (err)
221         {
222             if (FBTrace.DBG_CACHE || FBTrace.DBG_ERRORS)
223                 FBTrace.sysout("tabCache.ChannelListener.onStartRequest EXCEPTION\n", err);
224         }
225 
226         if (this.listener)
227         {
228             try  // https://bugzilla.mozilla.org/show_bug.cgi?id=492534
229             {
230                 this.listener.onStartRequest(request, requestContext);
231             }
232             catch(exc)
233             {
234                 if (FBTrace.DBG_CACHE)
235                     FBTrace.sysout("tabCache.ChannelListener.onStartRequest cancelling request " +
236                     "EXCEPTION: " + safeGetName(request), exc);
237 
238                 request.cancel(exc.result);
239             }
240         }
241     },
242 
243     onStopRequest: function(request, requestContext, statusCode)
244     {
245         try
246         {
247             var context = this.getContext(this.window);
248             if (context)
249                 this.proxyListener.onStopRequest(request, requestContext, statusCode);
250         }
251         catch (err)
252         {
253             if (FBTrace.DBG_CACHE || FBTrace.DBG_ERRORS)
254                 FBTrace.sysout("tabCache.ChannelListener.onStopRequest EXCEPTION\n", err);
255         }
256 
257         if (this.listener)
258             this.listener.onStopRequest(request, requestContext, statusCode);
259     },
260 
261     /* nsITraceableChannel */
262     setNewListener: function(listener)
263     {
264         this.proxyListener = listener;
265         return null;
266     },
267 
268     /* nsIInputStreamCallback */
269     onInputStreamReady : function(stream)
270     {
271         try
272         {
273             if (FBTrace.DBG_CACHE)
274                 FBTrace.sysout("tabCache.ChannelListener.onInputStreamReady " +
275                     safeGetName(this.request));
276 
277             if (stream instanceof Ci.nsIAsyncInputStream)
278             {
279                 try
280                 {
281                     this.onDataAvailable(this.request, null, stream, 0, stream.available());
282                 }
283                 catch (err)
284                 {
285                     // stream.available throws an exception if the stream is closed,
286                     // which is ok, since this callback can be called even in this
287                     // situations.
288                 }
289 
290                 // Listen for further incoming data.
291                 if (!this.ignore)
292                     this.setAsyncListener(this.request, stream, this);
293             }
294         }
295         catch (err)
296         {
297             if (FBTrace.DBG_CACHE || FBTrace.DBG_ERRORS)
298                 FBTrace.sysout("tabCache.ChannelListener.onInputStreamReady EXCEPTION " +
299                     safeGetName(this.request), err);
300         }
301     },
302 
303     /* nsISupports */
304     QueryInterface: function(iid)
305     {
306         if (iid.equals(Ci.nsIStreamListener) ||
307             iid.equals(Ci.nsIInputStreamCallback) ||
308             iid.equals(Ci.nsISupportsWeakReference) ||
309             iid.equals(Ci.nsITraceableChannel) ||
310             iid.equals(Ci.nsISupports))
311         {
312             return this;
313         }
314 
315         throw Components.results.NS_NOINTERFACE;
316     },
317 
318     getContext: function(win)
319     {
320         // This must be overridden in tabCache. This scope doesn't have an
321         // access to TabWatcher and its getContextByWindow method.
322         return null;
323     }
324 }
325 
326 // ************************************************************************************************
327 
328 function safeGetName(request)
329 {
330     try
331     {
332         return request.name;
333     }
334     catch (exc)
335     {
336         return null;
337     }
338 }
339 
340 function CCIN(cName, ifaceName)
341 {
342     return Cc[cName].createInstance(Ci[ifaceName]);
343 }
344 
345 // ************************************************************************************************
346 // Service factory
347 
348 var ListenerFactory =
349 {
350     createInstance: function (outer, iid)
351     {
352         if (outer != null)
353             throw Cr.NS_ERROR_NO_AGGREGATION;
354 
355         if (iid.equals(Ci.nsISupports) ||
356             iid.equals(Ci.nsIStreamListener))
357         {
358             var listener = new ChannelListener();
359 
360             if (FBTrace.DBG_CACHE)
361                 FBTrace.sysout("tabCache.ListenerFactory.createInstance; ");
362 
363             return listener.QueryInterface(iid);
364         }
365 
366         throw Cr.NS_ERROR_NO_INTERFACE;
367     },
368 
369     QueryInterface: function(iid)
370     {
371         if (iid.equals(Ci.nsISupports) ||
372             iid.equals(Ci.nsISupportsWeakReference) ||
373             iid.equals(Ci.nsIFactory))
374             return this;
375 
376         throw Cr.NS_ERROR_NO_INTERFACE;
377     }
378 };
379 
380 // ************************************************************************************************
381 // Module implementation
382 
383 var ListenerModule =
384 {
385     registerSelf: function(compMgr, fileSpec, location, type)
386     {
387         compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar);
388         compMgr.registerFactoryLocation(CLASS_ID, CLASS_NAME,
389             CONTRACT_ID, fileSpec, location, type);
390     },
391 
392     unregisterSelf: function(compMgr, fileSpec, location)
393     {
394         compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar);
395         compMgr.unregisterFactoryLocation(CLASS_ID, location);
396     },
397 
398     getClassObject: function(compMgr, cid, iid)
399     {
400         if (!iid.equals(Ci.nsIFactory))
401             throw Cr.NS_ERROR_NOT_IMPLEMENTED;
402 
403         if (cid.equals(CLASS_ID))
404             return ListenerFactory;
405 
406         throw Cr.NS_ERROR_NO_INTERFACE;
407     },
408 
409     canUnload: function(compMgr)
410     {
411         return true;
412     }
413 };
414 
415 // ************************************************************************************************
416 
417 function NSGetModule(compMgr, fileSpec)
418 {
419     return ListenerModule;
420 }
421