From ec05603f792336feb2681bf1bf84a23d7dc109fc Mon Sep 17 00:00:00 2001 From: Jan Odvarko Date: Mon, 25 Aug 2008 13:21:28 -0400 Subject: [PATCH] Bug 430155. New nsHttpChannel interface to allow examination of HTTP data before it is passed to the channel's creator. r=biesi, sr=bzbarsky --- netwerk/base/public/Makefile.in | 1 + netwerk/base/public/nsITraceableChannel.idl | 67 ++++++++++ netwerk/protocol/http/src/nsHttpChannel.cpp | 59 +++++++++ netwerk/protocol/http/src/nsHttpChannel.h | 4 + netwerk/test/unit/test_traceable_channel.js | 135 ++++++++++++++++++++ 5 files changed, 266 insertions(+) create mode 100644 netwerk/base/public/nsITraceableChannel.idl create mode 100644 netwerk/test/unit/test_traceable_channel.js diff --git a/netwerk/base/public/Makefile.in b/netwerk/base/public/Makefile.in index 7fe717374e3..eee3a78e5c6 100644 --- a/netwerk/base/public/Makefile.in +++ b/netwerk/base/public/Makefile.in @@ -58,6 +58,7 @@ SDK_XPIDLSRCS = \ nsIFileURL.idl \ nsIUploadChannel.idl \ nsIUnicharStreamListener.idl \ + nsITraceableChannel.idl \ $(NULL) XPIDLSRCS = \ diff --git a/netwerk/base/public/nsITraceableChannel.idl b/netwerk/base/public/nsITraceableChannel.idl new file mode 100644 index 00000000000..2ab1476fd08 --- /dev/null +++ b/netwerk/base/public/nsITraceableChannel.idl @@ -0,0 +1,67 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla. + * + * The Initial Developer of the Original Code is + * Jan Wrobel + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Jan Odvarko + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#include "nsISupports.idl" + +interface nsIStreamListener; + +/** + * A channel implementing this interface allows one to intercept its data by + * inserting intermediate stream listeners. + */ +[scriptable, uuid(68167b0b-ef34-4d79-a09a-8045f7c5140e)] +interface nsITraceableChannel : nsISupports +{ + /* + * Replace the channel's listener with a new one, and return the listener + * the channel used to have. The new listener intercepts OnStartRequest, + * OnDataAvailable and OnStopRequest calls and must pass them to + * the original listener after examination. If multiple callers replace + * the channel's listener, a chain of listeners is created. + * The caller of setNewListener has no way to control at which place + * in the chain its listener is placed. + * + * Note: The caller of setNewListener must not delay passing + * OnStartRequest to the original listener. + * + * Note2: A channel may restrict when the listener can be replaced. + * It is not recommended to allow listener replacement after OnStartRequest + * has been called. + */ + nsIStreamListener setNewListener(in nsIStreamListener aListener); +}; diff --git a/netwerk/protocol/http/src/nsHttpChannel.cpp b/netwerk/protocol/http/src/nsHttpChannel.cpp index f75c5e3f20b..afaedfc6cd2 100644 --- a/netwerk/protocol/http/src/nsHttpChannel.cpp +++ b/netwerk/protocol/http/src/nsHttpChannel.cpp @@ -24,6 +24,8 @@ * Darin Fisher (original author) * Christian Biesinger * Google Inc. + * Jan Wrobel + * Jan Odvarko * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or @@ -122,6 +124,7 @@ nsHttpChannel::nsHttpChannel() , mResuming(PR_FALSE) , mInitedCacheEntry(PR_FALSE) , mCacheForOfflineUse(PR_FALSE) + , mTracingEnabled(PR_TRUE) { LOG(("Creating nsHttpChannel @%x\n", this)); @@ -697,6 +700,8 @@ CallTypeSniffers(void *aClosure, const PRUint8 *aData, PRUint32 aCount) nsresult nsHttpChannel::CallOnStartRequest() { + mTracingEnabled = PR_FALSE; + if (mResponseHead && mResponseHead->ContentType().IsEmpty()) { if (!mContentTypeHint.IsEmpty()) mResponseHead->SetContentType(mContentTypeHint); @@ -3387,6 +3392,7 @@ NS_INTERFACE_MAP_BEGIN(nsHttpChannel) NS_INTERFACE_MAP_ENTRY(nsISupportsPriority) NS_INTERFACE_MAP_ENTRY(nsIProtocolProxyCallback) NS_INTERFACE_MAP_ENTRY(nsIProxiedChannel) + NS_INTERFACE_MAP_ENTRY(nsITraceableChannel) NS_INTERFACE_MAP_END_INHERITING(nsHashPropertyBag) //----------------------------------------------------------------------------- @@ -5014,3 +5020,56 @@ nsHttpChannel::nsContentEncodings::PrepareForNext(void) mReady = PR_TRUE; return NS_OK; } + +//----------------------------------------------------------------------------- +// nsStreamListenerWrapper +//----------------------------------------------------------------------------- + +// Wrapper class to make replacement of nsHttpChannel's listener +// from JavaScript possible. It is workaround for bug 433711. +class nsStreamListenerWrapper : public nsIStreamListener +{ +public: + nsStreamListenerWrapper(nsIStreamListener *listener); + + NS_DECL_ISUPPORTS + NS_FORWARD_NSIREQUESTOBSERVER(mListener->) + NS_FORWARD_NSISTREAMLISTENER(mListener->) + +private: + ~nsStreamListenerWrapper() {} + nsCOMPtr mListener; +}; + +nsStreamListenerWrapper::nsStreamListenerWrapper(nsIStreamListener *listener) + : mListener(listener) +{ + NS_ASSERTION(mListener, "no stream listener specified"); +} + +NS_IMPL_ISUPPORTS2(nsStreamListenerWrapper, + nsIStreamListener, + nsIRequestObserver) + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsITraceableChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::SetNewListener(nsIStreamListener *aListener, nsIStreamListener **_retval) +{ + if (!mTracingEnabled) + return NS_ERROR_FAILURE; + + NS_ENSURE_ARG_POINTER(aListener); + + nsCOMPtr wrapper = + new nsStreamListenerWrapper(mListener); + + if (!wrapper) + return NS_ERROR_OUT_OF_MEMORY; + + wrapper.forget(_retval); + mListener = aListener; + return NS_OK; +} diff --git a/netwerk/protocol/http/src/nsHttpChannel.h b/netwerk/protocol/http/src/nsHttpChannel.h index 2148f482699..58a4229ebc0 100644 --- a/netwerk/protocol/http/src/nsHttpChannel.h +++ b/netwerk/protocol/http/src/nsHttpChannel.h @@ -80,6 +80,7 @@ #include "nsIProtocolProxyCallback.h" #include "nsICancelable.h" #include "nsIProxiedChannel.h" +#include "nsITraceableChannel.h" class nsHttpResponseHead; class nsAHttpConnection; @@ -103,6 +104,7 @@ class nsHttpChannel : public nsHashPropertyBag , public nsISupportsPriority , public nsIProtocolProxyCallback , public nsIProxiedChannel + , public nsITraceableChannel { public: NS_DECL_ISUPPORTS_INHERITED @@ -121,6 +123,7 @@ public: NS_DECL_NSISUPPORTSPRIORITY NS_DECL_NSIPROTOCOLPROXYCALLBACK NS_DECL_NSIPROXIEDCHANNEL + NS_DECL_NSITRACEABLECHANNEL nsHttpChannel(); virtual ~nsHttpChannel(); @@ -306,6 +309,7 @@ private: PRUint32 mResuming : 1; PRUint32 mInitedCacheEntry : 1; PRUint32 mCacheForOfflineUse : 1; + PRUint32 mTracingEnabled : 1; class nsContentEncodings : public nsIUTF8StringEnumerator { diff --git a/netwerk/test/unit/test_traceable_channel.js b/netwerk/test/unit/test_traceable_channel.js new file mode 100644 index 00000000000..fa22061a5f4 --- /dev/null +++ b/netwerk/test/unit/test_traceable_channel.js @@ -0,0 +1,135 @@ +// Test nsITraceableChannel interface. +// Replace original listener with TracingListener that modifies body of HTTP +// response. Make sure that body received by original channel's listener +// is correctly modified. + +do_import_script("netwerk/test/httpserver/httpd.js"); + +var httpserver = null; +var originalBody = "original http response body"; +var replacedBody = "replaced http response body"; + +function TracingListener() {} + +TracingListener.prototype = { + + // Replace received response body. + onDataAvailable: function(request, context, inputStream, + offset, count) { + dump("*** tracing listener onDataAvailable\n"); + var binaryInputStream = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Components.interfaces.nsIBinaryInputStream); + binaryInputStream.setInputStream(inputStream); + + var data = binaryInputStream.readBytes(count); + var origBody = originalBody.substr(offset, count); + do_check_eq(origBody, data); + + var storageStream = Cc["@mozilla.org/storagestream;1"]. + createInstance(Components.interfaces.nsIStorageStream); + var binaryOutputStream = Cc["@mozilla.org/binaryoutputstream;1"]. + createInstance(Components.interfaces.nsIBinaryOutputStream); + + storageStream.init(8192, 100, null); + binaryOutputStream.setOutputStream(storageStream.getOutputStream(0)); + + var newBody = replacedBody.substr(offset, count); + binaryOutputStream.writeBytes(newBody, newBody.length); + + this.listener.onDataAvailable(request, context, + storageStream.newInputStream(0), 0, + replacedBody.length); + }, + + onStartRequest: function(request, context) { + this.listener.onStartRequest(request, context); + + // Make sure listener can't be replaced after OnStartRequest was called. + request.QueryInterface(Components.interfaces.nsITraceableChannel); + try { + var newListener = new TracingListener(); + newListener.listener = request.setNewListener(newListener); + } catch(e) { + return; // OK + } + do_throw("replaced channel's listener during onStartRequest."); + }, + + onStopRequest: function(request, context, statusCode) { + this.listener.onStopRequest(request, context, statusCode); + httpserver.stop(); + do_test_finished(); + }, + + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsIStreamListener) || + iid.equals(Components.interfaces.nsIRequestObserver) || + iid.equals(Components.interfaces.nsISupports) + ) + return this; + throw Components.results.NS_NOINTERFACE; + }, + + listener: null +} + + +function HttpResponseExaminer() {} + +HttpResponseExaminer.prototype = { + register: function() { + Cc["@mozilla.org/observer-service;1"]. + getService(Components.interfaces.nsIObserverService). + addObserver(this, "http-on-examine-response", true); + }, + + // Replace channel's listener. + observe: function(subject, topic, data) { + try { + subject.QueryInterface(Components.interfaces.nsITraceableChannel); + var newListener = new TracingListener(); + newListener.listener = subject.setNewListener(newListener); + } catch(e) { + do_throw("can't replace listener" + e); + } + }, + + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsIObserver) || + iid.equals(Components.interfaces.nsISupportsWeakReference) || + iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_NOINTERFACE; + } +} + +function test_handler(metadata, response) { + response.setHeader("Content-Type", "text/html", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(originalBody, originalBody.length); +} + +function make_channel(url) { + var ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + return ios.newChannel(url, null, null). + QueryInterface(Components.interfaces.nsIHttpChannel); +} + +// Check if received body is correctly modified. +function get_data(request, input, ctx) { + do_check_eq(replacedBody, input); +} + +function run_test() { + var observer = new HttpResponseExaminer(); + observer.register(); + + httpserver = new nsHttpServer(); + httpserver.registerPathHandler("/testdir", test_handler); + httpserver.start(4444); + + var channel = make_channel("http://localhost:4444/testdir"); + channel.asyncOpen(new ChannelListener(get_data), null); + do_test_pending(); +}