-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1 All,
After becoming intrigued over Youssef's recent post (see the "Record and simulate a web app" thread from the 18th), I wrote come code. Unfortunately, I wasn't able to get a complete response captured because of the timing of various events during the request (in summary: the HttpProcessor does some stuff at the last minute that we can't capture using application code). So, I've given up on writing the response-capture part of the code, at least for now. Since I wrote it, I figured I'd publish it, and here is as good a place as any (attached, hopefully). I'm sure there are some folks on the list who would like to see how request wrappers should be written and you can even see a few other neat things like: - - how to wrap a servlet input stream and reader - - how to make a copy of streaming data - - how to figure out which request parameters are GET versus POST I'd love comments if anyone has them. Enjoy! - -chris -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.9 (MingW32) Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org iEYEARECAAYFAkme2ZcACgkQ9CaO5/Lv0PB8QQCgwGJhkc5IPr9sccpeLJZbQ7mL hyUAnikwihV2dZsG4B1LpRA6Iz4ZV7yW =Kug3 -----END PGP SIGNATURE-----
// // License: // Copyright (c) 2009 Christopher Schultz // Free to use for any purpose for no fee. No guarantees. Credits and shout-outs are appreciated. // package net.christopherschultz.http; import java.io.IOException; import java.io.PrintStream; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; // RequestWrapper import java.io.InputStream; import java.io.OutputStream; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.Writer; import java.io.StringWriter; import java.io.BufferedReader; import java.io.UnsupportedEncodingException; import java.nio.CharBuffer; import java.util.Enumeration; import java.util.Iterator; import java.util.Map; import java.util.HashMap; import java.net.URLEncoder; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.ServletInputStream; import javax.servlet.ServletOutputStream; /** * HttpConversationRecorderFilter * * Dumps the complete HTTP request to stdout after the request has * been processed. * * This should be the first filter in your filter chain so you can * capture all of the request body. * * @author Chris Schultz (ch...@christopherschultz.net) * @version 2009-02-20 */ public class HttpConversationRecorderFilter implements Filter { private static final String DEFAULT_CHARSET = "ISO-8859-1"; private static final String DEFAULT_URI_CHARSET = "UTF-8"; private String _defaultCharset = DEFAULT_CHARSET; private String _uriCharset = DEFAULT_URI_CHARSET; public void init(FilterConfig config) { String charset = config.getInitParameter("default-charset"); if(null != charset) _defaultCharset = charset; charset = config.getInitParameter("default-uri-charset"); if(null != charset) _uriCharset = charset; } public void destroy() { } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if(request instanceof HttpServletRequest && response instanceof HttpServletResponse) { Throwable ex = null; HttpRequestRecorderWrapper req; req = wrapRequest((HttpServletRequest)request); try { chain.doFilter(req, response); } catch (Exception e) { // Save the exception for later. We still want to record // the request and possibly part of the response. // // Don't worry about catching java.lang.Error. ex = e; } // TODO: Write the request to a file? req.dumpRequest(System.out); req.free(); // Re-throw any exception that might have occurred if(null != ex) { if(ex instanceof ServletException) throw (ServletException)ex; if(ex instanceof IOException) throw (IOException)ex; if(ex instanceof RuntimeException) throw (RuntimeException)ex; if(ex instanceof Error) throw (Error)ex; // Should never get here throw new ServletException("Unexpected exception", ex); } } else { chain.doFilter(request, response); } } private HttpRequestRecorderWrapper wrapRequest(HttpServletRequest request) { return new HttpRequestRecorderWrapper(request); } private class HttpRequestRecorderWrapper extends HttpServletRequestWrapper { private ServletTeeInputStream _inputStream; private ByteArrayOutputStream _inputStreamBuffer; private BufferedTeeReader _inputReader; private StringWriter _inputReaderBuffer; private boolean _usingReader; public HttpRequestRecorderWrapper(HttpServletRequest request) { super(request); } public boolean usingReader() { return _usingReader; } public BufferedReader getReader() throws IOException { if(null == _inputReader) { BufferedReader in = super.getReader(); _inputReaderBuffer = new StringWriter(); _inputReader = new BufferedTeeReader(in, _inputReaderBuffer, true); _usingReader = true; } return _inputReader; } public ServletInputStream getInputStream() throws IOException { if(null == _inputStream) { ServletInputStream in = super.getInputStream(); _inputStreamBuffer = new ByteArrayOutputStream(); _inputStream = new ServletTeeInputStream(in, _inputStreamBuffer, true); } return _inputStream; } public byte[] getRequestBody() { if(usingReader()) { String charset = getCharacterEncoding(); if(null == charset) charset = _defaultCharset; try { return _inputReaderBuffer.toString().getBytes(charset); } catch (UnsupportedEncodingException uee) { throw new InternalError("Unrecognized character set: " + charset); } } else { return _inputStreamBuffer.toByteArray(); } } public void free() { _inputStream = null; _inputReader = null; if(null != _inputStreamBuffer) { _inputStreamBuffer.reset(); // empty buffer _inputStreamBuffer = null; } if(null != _inputReaderBuffer) { _inputReaderBuffer.getBuffer().setLength(0); // empty buffer _inputReaderBuffer = null; } } public void dumpRequest(PrintStream out) throws IOException { out.println("======> dumping HTTP request <============="); // // Wow, the first line of an HTTP request is tough to reconstruct // out.print(getMethod()); out.print(" "); out.print(getRequestURI()); String query = getQueryString(); if(null != query) { out.print("?"); out.print(query); } String protocol = getProtocol(); if(null != protocol) { out.print(" "); out.print(protocol); } out.println(); // // Now, dump all the headers // for(Enumeration i=getHeaderNames(); i.hasMoreElements(); ) { String headerName = (String)i.nextElement(); out.print(headerName); out.print(": "); for(Enumeration j = getHeaders(headerName); j.hasMoreElements(); ) { out.print((String)j.nextElement()); if(j.hasMoreElements()) out.print(","); } out.println(); } // Print the end-of-header newline out.println(); // // If Content-Type is application/x-www-form-urlencoded // then we have to reconstruct the parameters from the parameter // map. // // Trigger the fetching of request parameters getParameter(""); // See Servlet Specification SRV.3.1.1 for details. if("POST".equals(getMethod()) && "application/x-www-form-urlencoded".equals(getContentType())) { // In this case, the servlet container is supposed to // consume the request body and parse it as POST form data. // // We will need to reconstruct the POST string using all // parameters read plus the query string to figure out which // came from where. String postQueryString = RequestUtils.getPostQueryString(this, _uriCharset); out.print(postQueryString); } else { // // Consume any remaining request body // if(usingReader()) { int leftover = 0; // Stealthily avoid reading from a closed reader if(!_inputReader.isClosed()) { try { while(-1 != _inputReader.read()) leftover++; _inputReader.close(); } catch (IOException ioe) { System.err.println("WARN: reader already closed?"); } } if(0 < leftover) System.out.println("====> Filter read " + leftover + " unread characters from the request reader"); } else { int leftover = 0; // Stealthily avoid reading from a closed reader if(!_inputStream.isClosed()) { try { while(-1 != _inputStream.read()) leftover++; _inputStream.close(); } catch (IOException ioe) { System.err.println("WARN: stream already closed? " + ioe); } } if(0 < leftover) System.out.println("====> Filter read " + leftover + " unread bytes from the request stream"); } // // Dump the request body // byte[] body = getRequestBody(); String charset = getCharacterEncoding(); if(null == charset) charset = _defaultCharset; if(null != body && 0 < body.length) out.write(body); } out.flush(); } } private static class ServletTeeInputStream extends ServletInputStream { private InputStream _in; private OutputStream _out; private boolean _close; private boolean _isClosed = false; public ServletTeeInputStream(InputStream in, OutputStream out, boolean close) { _in = in; _out = out; _close = close; } public int available() throws IOException { return _in.available(); } public void close() throws IOException { try { _in.close(); } finally { if(_close) _out.close(); } _isClosed = true; } public void mark(int readlimit) { _in.mark(readlimit); } public boolean markSupported() { return _in.markSupported(); } public void reset() throws IOException { _in.reset(); } public int read() throws IOException { int ch = _in.read(); if(-1 != ch) _out.write(ch); return ch; } public int read(byte[] b) throws IOException { return this.read(b, 0, b.length); } public int read(byte[] b, int off, int len) throws IOException { int n = _in.read(b,off,len); if(-1 != n) _out.write(b, off, n); return n; } public long skip(long want) throws IOException { long total = 0; byte[] buffer = new byte[1024]; while(0 < want) { int n; int wantThisTime = (1024 < want ? 1024 : (int)want); if(-1 != (n = _in.read(buffer, 0, wantThisTime))) { _out.write(buffer, 0, n); total += n; want -= n; } } return total; } public boolean isClosed() { return _isClosed; } } public static class BufferedTeeReader extends BufferedReader { private Writer _out; private boolean _close; private boolean _isClosed = false; // TODO: Allow this to be overridden private String _lineSeparator = System.getProperty("line.separator"); public BufferedTeeReader(BufferedReader in, Writer out, boolean close) { super(in); _out = out; _close = close; } public int read() throws IOException { int c = super.read(); if(-1 != c) _out.write(c); return c; } public int read(char[] buffer) throws IOException { // Calling super.read(buffer) results in // this.read(buffer, 0, buffer.length) being called, // which will double the amount of output generated. // int n = super.read(buffer); // // if(-1 != n) // _out.write(buffer, 0, n); // // return n; return this.read(buffer, 0, buffer.length); } public int read(char[] buffer, int off, int len) throws IOException { int n = super.read(buffer, off, len); if(-1 != n) _out.write(buffer, off, n); return n; } public int read(CharBuffer buffer) throws IOException { int n = super.read(buffer); if(-1 != n) _out.append(buffer, 0, n); return n; } public String readLine() throws IOException { String line = super.readLine(); if(null != line) { _out.write(line); _out.write(_lineSeparator); } return line; } public long skip(long want) throws IOException { long total = 0; char[] buffer = new char[1024]; while(0 < want) { int n; int wantThisTime = (1024 < want ? 1024 : (int)want); if(-1 != (n = super.read(buffer, 0, wantThisTime))) { _out.write(buffer, 0, n); total += n; want -= n; } } return total; } public boolean ready() throws IOException { return super.ready(); } // TODO: return false? public boolean markSupported() { return super.markSupported(); } public void mark(int readAheadLimit) throws IOException { super.mark(readAheadLimit); } public void reset() throws IOException { super.reset(); } public void close() throws IOException { try { super.close(); } finally { if(_close) _out.close(); } _isClosed = true; } public boolean isClosed() { return _isClosed; } } public static class RequestUtils { public static String getPostQueryString(HttpServletRequest req) { try { return getPostQueryString(req, "UTF-8"); } catch (UnsupportedEncodingException uee) { throw new InternalError("UTF-8 charset is suddenly unsupported"); } } public static String getPostQueryString(HttpServletRequest req, String uriCharset) throws UnsupportedEncodingException { if(!("POST".equals(req.getMethod()) && "application/x-www-form-urlencoded".equals(req.getContentType()))) throw new IllegalArgumentException("Request must be both POST and application/x-www-urlencoded"); StringBuffer sb = new StringBuffer(); Map queryParameterCounts = countQueryParameters(req.getQueryString()); boolean first = true; for(Iterator i=req.getParameterMap().entrySet().iterator(); i.hasNext(); ) { Map.Entry entry = (Map.Entry)i.next(); String paramName = (String)entry.getKey(); String[] values = (String[])entry.getValue(); int skip = 0; Integer queryCount = (Integer)queryParameterCounts .get(paramName); if(null != queryCount) skip += queryCount.intValue(); if(values.length > skip) { for(int j=skip; j<values.length; ++j) { if(first) first = false; else sb.append('&'); sb.append(URLEncoder.encode(paramName, uriCharset)) .append('=') .append(URLEncoder.encode(values[j], uriCharset)); } } } return sb.toString(); } private static Map countQueryParameters(String query) { HashMap counts = new HashMap(); if(null != query) { String[] paramNames = query.split("=[^&]*(&)?"); for(int i=0; i<paramNames.length; ++i) { Integer count = (Integer)counts.get(paramNames[i]); if(null == count) count = new Integer(1); else count = new Integer(count.intValue() + 1); counts.put(paramNames[i], count); } } return counts; } } }
--------------------------------------------------------------------- To unsubscribe, e-mail: users-unsubscr...@tomcat.apache.org For additional commands, e-mail: users-h...@tomcat.apache.org