blob: daea90590b57305962927d5522d78742ad883ce6 [file] [log] [blame]
/*
* 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");
}
}
}