| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (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.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| 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 static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNull; |
| import static org.junit.Assert.assertTrue; |
| |
| import org.junit.Test; |
| |
| 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 { |
| |
| private static final String LF = "\n"; |
| private static final int EXT_SIZE_LIMIT = 10; |
| |
| @Test |
| public void testChunkHeaderCRLF() throws Exception { |
| doTestChunkingCRLF(true, true, true, true, true, true); |
| } |
| |
| @Test |
| public void testChunkHeaderLF() throws Exception { |
| doTestChunkingCRLF(false, true, true, true, true, false); |
| } |
| |
| @Test |
| public void testChunkCRLF() throws Exception { |
| doTestChunkingCRLF(true, true, true, true, true, true); |
| } |
| |
| @Test |
| public void testChunkLF() throws Exception { |
| doTestChunkingCRLF(true, false, true, true, true, false); |
| } |
| |
| @Test |
| public void testFirstTrailingHeadersCRLF() throws Exception { |
| doTestChunkingCRLF(true, true, true, true, true, true); |
| } |
| |
| @Test |
| public void testFirstTrailingHeadersLF() throws Exception { |
| doTestChunkingCRLF(true, true, false, true, true, true); |
| } |
| |
| @Test |
| public void testSecondTrailingHeadersCRLF() throws Exception { |
| doTestChunkingCRLF(true, true, true, true, true, true); |
| } |
| |
| @Test |
| public void testSecondTrailingHeadersLF() throws Exception { |
| doTestChunkingCRLF(true, true, true, false, true, true); |
| } |
| |
| @Test |
| public void testEndCRLF() throws Exception { |
| doTestChunkingCRLF(true, true, true, true, true, true); |
| } |
| |
| @Test |
| public void testEndLF() throws Exception { |
| doTestChunkingCRLF(true, true, true, true, false, false); |
| } |
| |
| private void doTestChunkingCRLF(boolean chunkHeaderUsesCRLF, |
| boolean chunkUsesCRLF, boolean firstheaderUsesCRLF, |
| boolean secondheaderUsesCRLF, boolean endUsesCRLF, |
| boolean expectPass) throws Exception { |
| |
| // Setup Tomcat instance |
| Tomcat tomcat = getTomcatInstance(); |
| |
| // No file system docBase required |
| Context ctx = tomcat.addContext("", null); |
| |
| // Configure allowed trailer headers |
| tomcat.getConnector().setProperty("allowedTrailerHeaders", "X-Trailer1,X-Trailer2"); |
| |
| EchoHeaderServlet servlet = new EchoHeaderServlet(expectPass); |
| Tomcat.addServlet(ctx, "servlet", servlet); |
| ctx.addServletMappingDecoded("/", "servlet"); |
| |
| tomcat.start(); |
| |
| String[] request = new String[]{ |
| "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" + (chunkHeaderUsesCRLF ? SimpleHttpClient.CRLF : LF) + |
| "a=0" + (chunkUsesCRLF ? SimpleHttpClient.CRLF : LF) + |
| "4" + SimpleHttpClient.CRLF + |
| "&b=1" + SimpleHttpClient.CRLF + |
| "0" + SimpleHttpClient.CRLF + |
| "x-trailer1: Test", "Value1" + |
| (firstheaderUsesCRLF ? SimpleHttpClient.CRLF : LF) + |
| "x-trailer2: TestValue2" + |
| (secondheaderUsesCRLF ? SimpleHttpClient.CRLF : LF) + |
| (endUsesCRLF ? SimpleHttpClient.CRLF : LF) }; |
| |
| TrailerClient client = |
| new TrailerClient(tomcat.getConnector().getLocalPort()); |
| client.setRequest(request); |
| |
| client.connect(); |
| Exception processException = null; |
| try { |
| client.processRequest(); |
| } catch (Exception e) { |
| // Socket was probably closed before client had a chance to read |
| // response |
| processException = e; |
| } |
| |
| if (expectPass) { |
| assertTrue(client.isResponse200()); |
| assertEquals("nullnull7TestValue1TestValue2", |
| client.getResponseBody()); |
| assertNull(processException); |
| assertFalse(servlet.getExceptionDuringRead()); |
| } else { |
| if (processException == null) { |
| assertTrue(client.getResponseLine(), client.isResponse500()); |
| } else { |
| // Use fall-back for checking the error occurred |
| assertTrue(servlet.getExceptionDuringRead()); |
| } |
| } |
| } |
| |
| @Test |
| public void testTrailingHeadersSizeLimit() throws Exception { |
| // Setup Tomcat instance |
| Tomcat tomcat = getTomcatInstance(); |
| |
| // No file system docBase required |
| Context ctx = tomcat.addContext("", null); |
| |
| Tomcat.addServlet(ctx, "servlet", new EchoHeaderServlet(false)); |
| ctx.addServletMappingDecoded("/", "servlet"); |
| |
| // Limit the size of the trailing header |
| tomcat.getConnector().setProperty("maxTrailerSize", "10"); |
| tomcat.start(); |
| |
| String[] request = new String[]{ |
| "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: Test" + SimpleHttpClient.CRLF + |
| SimpleHttpClient.CRLF }; |
| |
| TrailerClient client = |
| new TrailerClient(tomcat.getConnector().getLocalPort()); |
| client.setRequest(request); |
| |
| client.connect(); |
| client.processRequest(); |
| // Expected to fail because the trailers are longer |
| // than the set limit of 10 bytes |
| assertTrue(client.isResponse500()); |
| } |
| |
| |
| @Test |
| public void testExtensionSizeLimitOneBelow() throws Exception { |
| doTestExtensionSizeLimit(EXT_SIZE_LIMIT - 1, true); |
| } |
| |
| |
| @Test |
| public void testExtensionSizeLimitExact() throws Exception { |
| doTestExtensionSizeLimit(EXT_SIZE_LIMIT, true); |
| } |
| |
| |
| @Test |
| public void testExtensionSizeLimitOneOver() throws Exception { |
| doTestExtensionSizeLimit(EXT_SIZE_LIMIT + 1, false); |
| } |
| |
| |
| private void doTestExtensionSizeLimit(int len, boolean ok) throws Exception { |
| // Setup Tomcat instance |
| Tomcat tomcat = getTomcatInstance(); |
| |
| tomcat.getConnector().setProperty( |
| "maxExtensionSize", Integer.toString(EXT_SIZE_LIMIT)); |
| |
| // No file system docBase required |
| Context ctx = tomcat.addContext("", null); |
| |
| Tomcat.addServlet(ctx, "servlet", new EchoHeaderServlet(ok)); |
| ctx.addServletMappingDecoded("/", "servlet"); |
| |
| tomcat.start(); |
| |
| String extName = ";foo="; |
| StringBuilder extValue = new StringBuilder(len); |
| for (int i = 0; i < (len - extName.length()); i++) { |
| extValue.append("x"); |
| } |
| |
| String[] request = new String[]{ |
| "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" + extName + extValue.toString() + SimpleHttpClient.CRLF + |
| "a=0" + SimpleHttpClient.CRLF + |
| "4" + SimpleHttpClient.CRLF + |
| "&b=1" + SimpleHttpClient.CRLF + |
| "0" + SimpleHttpClient.CRLF + |
| SimpleHttpClient.CRLF }; |
| |
| TrailerClient client = |
| new TrailerClient(tomcat.getConnector().getLocalPort()); |
| client.setRequest(request); |
| |
| client.connect(); |
| client.processRequest(); |
| |
| if (ok) { |
| assertTrue(client.isResponse200()); |
| } else { |
| assertTrue(client.isResponse500()); |
| } |
| } |
| |
| @Test |
| public void testNoTrailingHeaders() throws Exception { |
| // Setup Tomcat instance |
| Tomcat tomcat = getTomcatInstance(); |
| |
| // No file system docBase required |
| Context ctx = tomcat.addContext("", null); |
| |
| Tomcat.addServlet(ctx, "servlet", new EchoHeaderServlet(true)); |
| ctx.addServletMappingDecoded("/", "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(tomcat.getConnector().getLocalPort()); |
| client.setRequest(new String[] {request}); |
| |
| client.connect(); |
| client.processRequest(); |
| assertEquals("nullnull7nullnull", client.getResponseBody()); |
| } |
| |
| @Test |
| public void testChunkSizeZero() throws Exception { |
| doTestChunkSize(true, true, "", 10, 0); |
| } |
| |
| @Test |
| public void testChunkSizeAbsent() throws Exception { |
| doTestChunkSize(false, false, SimpleHttpClient.CRLF, 10, 0); |
| } |
| |
| @Test |
| public void testChunkSizeTwentyFive() throws Exception { |
| doTestChunkSize(true, true, "19" + SimpleHttpClient.CRLF |
| + "Hello World!Hello World!!" + SimpleHttpClient.CRLF, 40, 25); |
| } |
| |
| @Test |
| public void testChunkSizeEightDigit() throws Exception { |
| doTestChunkSize(true, true, "0000000C" + SimpleHttpClient.CRLF |
| + "Hello World!" + SimpleHttpClient.CRLF, 20, 12); |
| } |
| |
| @Test |
| public void testChunkSizeNineDigit() throws Exception { |
| doTestChunkSize(false, false, "00000000C" + SimpleHttpClient.CRLF |
| + "Hello World!" + SimpleHttpClient.CRLF, 20, 12); |
| } |
| |
| @Test |
| public void testChunkSizeLong() throws Exception { |
| doTestChunkSize(true, false, "7fFFffFF" + SimpleHttpClient.CRLF |
| + "Hello World!" + SimpleHttpClient.CRLF, 10, 10); |
| } |
| |
| @Test |
| public void testChunkSizeIntegerMinValue() throws Exception { |
| doTestChunkSize(false, false, "80000000" + SimpleHttpClient.CRLF |
| + "Hello World!" + SimpleHttpClient.CRLF, 10, 10); |
| } |
| |
| @Test |
| public void testChunkSizeMinusOne() throws Exception { |
| doTestChunkSize(false, false, "ffffffff" + SimpleHttpClient.CRLF |
| + "Hello World!" + SimpleHttpClient.CRLF, 10, 10); |
| } |
| |
| /** |
| * @param expectPass |
| * If the servlet is expected to process the request |
| * @param expectReadWholeBody |
| * If the servlet is expected to fully read the body and reliably |
| * deliver a response |
| * @param chunks |
| * Text of chunks |
| * @param readLimit |
| * Do not read more than this many bytes |
| * @param expectReadCount |
| * Expected count of read bytes |
| * @throws Exception |
| * Unexpected |
| */ |
| private void doTestChunkSize(boolean expectPass, |
| boolean expectReadWholeBody, String chunks, int readLimit, |
| int expectReadCount) throws Exception { |
| // Setup Tomcat instance |
| Tomcat tomcat = getTomcatInstance(); |
| |
| // No file system docBase required |
| Context ctx = tomcat.addContext("", null); |
| |
| BodyReadServlet servlet = new BodyReadServlet(expectPass, readLimit); |
| Tomcat.addServlet(ctx, "servlet", servlet); |
| ctx.addServletMappingDecoded("/", "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: text/plain" + SimpleHttpClient.CRLF; |
| if (expectPass) { |
| request += "Connection: close" + SimpleHttpClient.CRLF; |
| } |
| request += SimpleHttpClient.CRLF + chunks + "0" + SimpleHttpClient.CRLF |
| + SimpleHttpClient.CRLF; |
| |
| TrailerClient client = new TrailerClient(tomcat.getConnector() |
| .getLocalPort()); |
| client.setRequest(new String[] { request }); |
| |
| Exception processException = null; |
| client.connect(); |
| try { |
| client.processRequest(); |
| } catch (Exception e) { |
| // Socket was probably closed before client had a chance to read |
| // response |
| processException = e; |
| } |
| if (expectPass) { |
| if (expectReadWholeBody) { |
| assertNull(processException); |
| } |
| if (processException == null) { |
| assertTrue(client.getResponseLine(), client.isResponse200()); |
| assertEquals(String.valueOf(expectReadCount), |
| client.getResponseBody()); |
| } |
| assertEquals(expectReadCount, servlet.getCountRead()); |
| } else { |
| if (processException == null) { |
| assertTrue(client.getResponseLine(), client.isResponse500()); |
| } |
| assertEquals(0, servlet.getCountRead()); |
| assertTrue(servlet.getExceptionDuringRead()); |
| } |
| } |
| |
| private static class EchoHeaderServlet extends HttpServlet { |
| private static final long serialVersionUID = 1L; |
| |
| private boolean exceptionDuringRead = false; |
| |
| private final boolean expectPass; |
| |
| public EchoHeaderServlet(boolean expectPass) { |
| this.expectPass = expectPass; |
| } |
| |
| @Override |
| protected void doPost(HttpServletRequest req, HttpServletResponse resp) |
| throws ServletException, IOException { |
| resp.setContentType("text/plain"); |
| PrintWriter pw = resp.getWriter(); |
| // Headers not visible yet, body not processed |
| dumpHeader("x-trailer1", req, pw); |
| dumpHeader("x-trailer2", req, pw); |
| |
| // Read the body - quick and dirty |
| InputStream is = req.getInputStream(); |
| int count = 0; |
| try { |
| while (is.read() > -1) { |
| count++; |
| } |
| } catch (IOException ioe) { |
| exceptionDuringRead = true; |
| if (!expectPass) { // as expected |
| log(ioe.toString()); |
| resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); |
| return; |
| } |
| throw ioe; |
| } |
| |
| pw.write(Integer.valueOf(count).toString()); |
| |
| // Headers should be visible now |
| dumpHeader("x-trailer1", req, pw); |
| dumpHeader("x-trailer2", req, pw); |
| } |
| |
| public boolean getExceptionDuringRead() { |
| return exceptionDuringRead; |
| } |
| |
| private void dumpHeader(String headerName, HttpServletRequest req, |
| PrintWriter pw) { |
| String value = req.getHeader(headerName); |
| if (value == null) { |
| value = "null"; |
| } |
| pw.write(value); |
| } |
| } |
| |
| private static class BodyReadServlet extends HttpServlet { |
| private static final long serialVersionUID = 1L; |
| |
| private boolean exceptionDuringRead = false; |
| private int countRead = 0; |
| private final boolean expectPass; |
| private final int readLimit; |
| |
| public BodyReadServlet(boolean expectPass, int readLimit) { |
| this.expectPass = expectPass; |
| this.readLimit = readLimit; |
| } |
| |
| @Override |
| protected void doPost(HttpServletRequest req, HttpServletResponse resp) |
| throws ServletException, IOException { |
| resp.setContentType("text/plain"); |
| PrintWriter pw = resp.getWriter(); |
| |
| // Read the body - quick and dirty |
| InputStream is = req.getInputStream(); |
| try { |
| while (is.read() > -1 && countRead < readLimit) { |
| countRead++; |
| } |
| } catch (IOException ioe) { |
| exceptionDuringRead = true; |
| if (!expectPass) { // as expected |
| log(ioe.toString()); |
| resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); |
| return; |
| } |
| throw ioe; |
| } |
| |
| pw.write(Integer.valueOf(countRead).toString()); |
| } |
| |
| public boolean getExceptionDuringRead() { |
| return exceptionDuringRead; |
| } |
| |
| public int getCountRead() { |
| return countRead; |
| } |
| } |
| |
| private static class TrailerClient extends SimpleHttpClient { |
| |
| public TrailerClient(int port) { |
| setPort(port); |
| } |
| |
| @Override |
| public boolean isResponseBodyOK() { |
| return getResponseBody().contains("TestTestTest"); |
| } |
| } |
| } |