From 731ba68d2d0ee28dfd2ed0804e164c40e8754a27 Mon Sep 17 00:00:00 2001 From: markt Date: Fri, 1 Oct 2010 10:21:58 +0000 Subject: [PATCH] Fix https://issues.apache.org/bugzilla/show_bug.cgi?id=49860 Add support for trailing headers git-svn-id: https://svn.apache.org/repos/asf/tomcat/trunk@1003461 13f79535-47bb-0310-9956-ffa450edef68 --- .../coyote/http11/filters/ChunkedInputFilter.java | 171 ++++++++++++++++++++- .../http11/filters/TestChunkedInputFilter.java | 134 ++++++++++++++++ webapps/docs/changelog.xml | 4 + 3 files changed, 303 insertions(+), 6 deletions(-) create mode 100644 test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java diff --git a/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java b/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java index 996c0776d..b3a6bfb66 100644 --- a/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java +++ b/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java @@ -17,6 +17,7 @@ package org.apache.coyote.http11.filters; +import java.io.EOFException; import java.io.IOException; import org.apache.coyote.InputBuffer; @@ -25,6 +26,8 @@ import org.apache.coyote.http11.Constants; import org.apache.coyote.http11.InputFilter; import org.apache.tomcat.util.buf.ByteChunk; import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.http.MimeHeaders; /** * Chunked input filter. Parses chunked data according to @@ -95,12 +98,19 @@ public class ChunkedInputFilter implements InputFilter { */ protected boolean endChunk = false; + /** * Flag set to true if the next call to doRead() must parse a CRLF pair * before doing anything else. */ protected boolean needCRLFParse = false; + + /** + * Request being parsed. + */ + private Request request; + // ------------------------------------------------------------- Properties @@ -177,7 +187,7 @@ public class ChunkedInputFilter implements InputFilter { */ @Override public void setRequest(Request request) { - // NOOP: Request isn't used so ignore it + this.request = request; } @@ -357,14 +367,163 @@ public class ChunkedInputFilter implements InputFilter { /** * Parse end chunk data. - * FIXME: Handle trailers */ - protected boolean parseEndChunk() - throws IOException { - - return parseCRLF(); // FIXME + protected void parseEndChunk() throws IOException { + // Handle option trailer headers + while (parseHeader()) { + // Loop until we run out of headers + } } + + @SuppressWarnings("null") // headerValue cannot be null + private boolean parseHeader() throws IOException { + + MimeHeaders headers = request.getMimeHeaders(); + byte chr = 0; + while (true) { + // Read new bytes if needed + if (pos >= lastValid) { + if (readBytes() <0) + throw new EOFException("Unexpected end of stream whilst reading trailer headers for chunked request"); + } + + chr = buf[pos]; + + if ((chr == Constants.CR) || (chr == Constants.LF)) { + if (chr == Constants.LF) { + pos++; + return false; + } + } else { + break; + } + + pos++; + + } + + // Mark the current buffer position + int start = pos; + + // + // Reading the header name + // Header name is always US-ASCII + // + + boolean colon = false; + MessageBytes headerValue = null; + + while (!colon) { + + // Read new bytes if needed + if (pos >= lastValid) { + if (readBytes() <0) + throw new EOFException("Unexpected end of stream whilst reading trailer headers for chunked request"); + } + + if (buf[pos] == Constants.COLON) { + colon = true; + headerValue = headers.addValue(buf, start, pos - start); + } + chr = buf[pos]; + if ((chr >= Constants.A) && (chr <= Constants.Z)) { + buf[pos] = (byte) (chr - Constants.LC_OFFSET); + } + + pos++; + + } + + // Mark the current buffer position + start = pos; + int realPos = pos; + + // + // Reading the header value (which can be spanned over multiple lines) + // + + boolean eol = false; + boolean validLine = true; + + while (validLine) { + + boolean space = true; + + // Skipping spaces + while (space) { + + // Read new bytes if needed + if (pos >= lastValid) { + if (readBytes() <0) + throw new EOFException("Unexpected end of stream whilst reading trailer headers for chunked request"); + } + + if ((buf[pos] == Constants.SP) || (buf[pos] == Constants.HT)) { + pos++; + } else { + space = false; + } + + } + + int lastSignificantChar = realPos; + + // Reading bytes until the end of the line + while (!eol) { + + // Read new bytes if needed + if (pos >= lastValid) { + if (readBytes() <0) + throw new EOFException("Unexpected end of stream whilst reading trailer headers for chunked request"); + } + + if (buf[pos] == Constants.CR) { + // Skip + } else if (buf[pos] == Constants.LF) { + eol = true; + } else if (buf[pos] == Constants.SP) { + buf[realPos] = buf[pos]; + realPos++; + } else { + buf[realPos] = buf[pos]; + realPos++; + lastSignificantChar = realPos; + } + + pos++; + + } + + realPos = lastSignificantChar; + + // Checking the first character of the new line. If the character + // is a LWS, then it's a multiline header + + // Read new bytes if needed + if (pos >= lastValid) { + if (readBytes() <0) + throw new EOFException("Unexpected end of stream whilst reading trailer headers for chunked request"); + } + + chr = buf[pos]; + if ((chr != Constants.SP) && (chr != Constants.HT)) { + validLine = false; + } else { + eol = false; + // Copying one extra space in the buffer (since there must + // be at least one space inserted between the lines) + buf[realPos] = chr; + realPos++; + } + + } + + // Set the header value + headerValue.setBytes(buf, start, realPos - start); + + return true; + } } diff --git a/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java b/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java new file mode 100644 index 000000000..0d7fae9b8 --- /dev/null +++ b/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java @@ -0,0 +1,134 @@ +package org.apache.coyote.http11.filters; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestChunkedInputFilter extends TomcatBaseTest { + + public void testTrailingHeaders() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // Must have a real docBase - just use temp + Context ctx = + tomcat.addContext("", System.getProperty("java.io.tmpdir")); + + Tomcat.addServlet(ctx, "servlet", new EchoHeaderServlet()); + ctx.addServletMapping("/", "servlet"); + + tomcat.start(); + + String request = + "POST /echo-params.jsp HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: any" + SimpleHttpClient.CRLF + + "Transfer-encoding: chunked" + SimpleHttpClient.CRLF + + "Content-Type: application/x-www-form-urlencoded" + + SimpleHttpClient.CRLF + + "Connection: close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + + "3" + SimpleHttpClient.CRLF + + "a=0" + SimpleHttpClient.CRLF + + "4" + SimpleHttpClient.CRLF + + "&b=1" + SimpleHttpClient.CRLF + + "0" + SimpleHttpClient.CRLF + + "x-trailer: TestTestTest" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF; + + TrailerClient client = new TrailerClient(); + client.setPort(getPort()); + client.setRequest(new String[] {request}); + + client.connect(); + client.processRequest(); + assertEquals("null7TestTestTest", client.getResponseBody()); + } + + public void testNoTrailingHeaders() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // Must have a real docBase - just use temp + Context ctx = + tomcat.addContext("", System.getProperty("java.io.tmpdir")); + + Tomcat.addServlet(ctx, "servlet", new EchoHeaderServlet()); + ctx.addServletMapping("/", "servlet"); + + tomcat.start(); + + String request = + "POST /echo-params.jsp HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: any" + SimpleHttpClient.CRLF + + "Transfer-encoding: chunked" + SimpleHttpClient.CRLF + + "Content-Type: application/x-www-form-urlencoded" + + SimpleHttpClient.CRLF + + "Connection: close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + + "3" + SimpleHttpClient.CRLF + + "a=0" + SimpleHttpClient.CRLF + + "4" + SimpleHttpClient.CRLF + + "&b=1" + SimpleHttpClient.CRLF + + "0" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF; + + TrailerClient client = new TrailerClient(); + client.setPort(getPort()); + client.setRequest(new String[] {request}); + + client.connect(); + client.processRequest(); + assertEquals("null7null", client.getResponseBody()); + } + + private static class EchoHeaderServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + PrintWriter pw = resp.getWriter(); + // Header not visible yet, body not processed + String value = req.getHeader("x-trailer"); + if (value == null) { + value = "null"; + } + pw.write(value); + + // Read the body - quick and dirty + InputStream is = req.getInputStream(); + int count = 0; + while (is.read() > -1) { + count++; + } + + pw.write(Integer.valueOf(count).toString()); + + // Header should be visible now + value = req.getHeader("x-trailer"); + if (value == null) { + value = "null"; + } + pw.write(value); + } + } + + private static class TrailerClient extends SimpleHttpClient { + + @Override + public boolean isResponseBodyOK() { + return getResponseBody().contains("TestTestTest"); + } + } +} diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 2b8095e02..fb838e5cb 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -230,6 +230,10 @@ Various refactorings to reduce code duplication and unnecessary code in the connectors. (markt) + + 49860: Add support for trailing headers in chunked HTTP + requests. (markt) + -- 2.11.0