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