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