This is an automated email from the ASF dual-hosted git repository. markt pushed a commit to branch 9.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/9.0.x by this push: new 1d0e79ad72 Fix regression in reading chunked request bodies 1d0e79ad72 is described below commit 1d0e79ad723ec181a3650c5d9c9ad9dd62178333 Author: Mark Thomas <ma...@apache.org> AuthorDate: Wed Aug 14 10:53:04 2024 +0100 Fix regression in reading chunked request bodies available() was returning a non-zero value when there was no data to read. In some circumstances this could cause a blocking read to block waiting for more data rather than return the data it had already received. --- .../coyote/http11/filters/ChunkedInputFilter.java | 10 ++ .../http11/filters/TestChunkedInputFilter.java | 115 +++++++++++++++++++-- webapps/docs/changelog.xml | 11 ++ 3 files changed, 130 insertions(+), 6 deletions(-) diff --git a/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java b/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java index 36389576d4..a967ef65a8 100644 --- a/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java +++ b/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java @@ -198,6 +198,16 @@ public class ChunkedInputFilter implements InputFilter, ApplicationBufferHandler if (readChunk != null) { available = readChunk.remaining(); } + + // Handle some edge cases + if (available == 1 && parseState == ParseState.CHUNK_BODY_CRLF) { + // Either just the CR or just the LF are left in the buffer. There is no data to read. + available = 0; + } else if (available == 2 && !crFound && parseState == ParseState.CHUNK_BODY_CRLF) { + // Just CRLF is left in the buffer. There is no data to read. + available = 0; + } + if (available == 0) { // No data buffered here. Try the next filter in the chain. return buffer.available(); diff --git a/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java b/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java index 9a0d41eff6..6f9e906080 100644 --- a/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java +++ b/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java @@ -16,9 +16,13 @@ */ package org.apache.coyote.http11.filters; +import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -36,7 +40,6 @@ import org.apache.catalina.startup.TomcatBaseTest; public class TestChunkedInputFilter extends TomcatBaseTest { - private static final String LF = "\n"; private static final int EXT_SIZE_LIMIT = 10; @Test @@ -117,16 +120,16 @@ public class TestChunkedInputFilter extends TomcatBaseTest { SimpleHttpClient.CRLF + "Connection: close" + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF + - "3" + (chunkHeaderUsesCRLF ? SimpleHttpClient.CRLF : LF) + - "a=0" + (chunkUsesCRLF ? SimpleHttpClient.CRLF : LF) + + "3" + (chunkHeaderUsesCRLF ? SimpleHttpClient.CRLF : SimpleHttpClient.LF) + + "a=0" + (chunkUsesCRLF ? SimpleHttpClient.CRLF : SimpleHttpClient.LF) + "4" + SimpleHttpClient.CRLF + "&b=1" + SimpleHttpClient.CRLF + "0" + SimpleHttpClient.CRLF + "x-trailer1: Test", "Value1" + - (firstheaderUsesCRLF ? SimpleHttpClient.CRLF : LF) + + (firstheaderUsesCRLF ? SimpleHttpClient.CRLF : SimpleHttpClient.LF) + "x-trailer2: TestValue2" + - (secondheaderUsesCRLF ? SimpleHttpClient.CRLF : LF) + - (endUsesCRLF ? SimpleHttpClient.CRLF : LF) }; + (secondheaderUsesCRLF ? SimpleHttpClient.CRLF : SimpleHttpClient.LF) + + (endUsesCRLF ? SimpleHttpClient.CRLF : SimpleHttpClient.LF) }; TrailerClient client = new TrailerClient(tomcat.getConnector().getLocalPort()); @@ -777,4 +780,104 @@ public class TestChunkedInputFilter extends TomcatBaseTest { Assert.assertNull(client.getResponseLine()); } } + + + private static class BodyReadLineServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + int lineCount = 0; + int pauseCount = 0; + + // Read the body one line at a time. There should be ~1s between reads. + try (InputStream is = req.getInputStream(); + InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8); + BufferedReader br = new BufferedReader(isr)) { + long lastRead = 0; + while (br.readLine() != null) { + long thisRead = System.nanoTime(); + if (lineCount > 0) { + /* + * After the first line, look for a pause of at least 800ms between reads. + */ + if ((thisRead - lastRead) > TimeUnit.MILLISECONDS.toNanos(800)) { + pauseCount++; + } + } + lastRead = thisRead; + lineCount++; + } + } + + resp.setContentType("text/plain"); + PrintWriter pw = resp.getWriter(); + pw.write(Integer.toString(lineCount) + "," + Integer.toString(pauseCount)); + } + } + + + private static class ReadLineClient extends SimpleHttpClient { + + ReadLineClient(int port) { + setPort(port); + } + + @Override + public boolean isResponseBodyOK() { + return getResponseBody().equals("5"); + } + } + + + @Test + public void testChunkedSplitWithReader() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + BodyReadLineServlet servlet = new BodyReadLineServlet(); + Tomcat.addServlet(ctx, "servlet", servlet); + ctx.addServletMappingDecoded("/test", "servlet"); + + tomcat.getConnector().setProperty("connectionTimeout", "300000"); + tomcat.start(); + + String[] request = new String[]{ + "POST /test 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 + + "7" + SimpleHttpClient.CRLF + + "DATA01\n" + SimpleHttpClient.CRLF, + "7" + SimpleHttpClient.CRLF + + "DATA02\n" + SimpleHttpClient.CRLF, + "7" + SimpleHttpClient.CRLF + + // Split the CRLF between writes + "DATA03\n" + SimpleHttpClient.CR, + SimpleHttpClient.LF + + "7" + SimpleHttpClient.CRLF + + "DATA04\n" + SimpleHttpClient.CRLF, + "7" + SimpleHttpClient.CRLF + + "DATA05\n" + SimpleHttpClient.CRLF + + "0" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF}; + + ReadLineClient client = new ReadLineClient(tomcat.getConnector().getLocalPort()); + client.setRequest(request); + + client.connect(300000,300000); + client.processRequest(); + Assert.assertTrue(client.isResponse200()); + /* + * Output is "<lines read>,<pauses observer>" so there should be 5 lines read with a pause between each. + */ + Assert.assertEquals("5,4", client.getResponseBody()); + } } diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 85e6ab0c8c..2f3f85194b 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -105,6 +105,17 @@ issues do not "pop up" wrt. others). --> <section name="Tomcat 9.0.94 (remm)" rtext="in development"> + <subsection name="Coyote"> + <changelog> + <fix> + Correct a regression in the fix for non-blocking reads of chunked + request bodies that caused <code>InputStream.available()</code> to + return a non-zero value when there was no data to read. In some + circumstances this could cause a blocking read to block waiting for more + data rather than return the data it had already received. (markt) + </fix> + </changelog> + </subsection> <subsection name="Jasper"> <changelog> <fix> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org