/*
* 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;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import static org.apache.catalina.startup.SimpleHttpClient.CRLF;
import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.connector.ResponseFacade;
import org.apache.catalina.startup.SimpleHttpClient;
import org.apache.catalina.startup.TesterServlet;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
import org.apache.tomcat.util.buf.B2CConverter;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
import org.apache.tomcat.util.http.parser.TokenList;
public class TestHttp11Processor extends TomcatBaseTest {
@Test
public void testResponseWithErrorChunked() throws Exception {
Tomcat tomcat = getTomcatInstance();
// This setting means the connection will be closed at the end of the
// request
Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1"));
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add protected servlet
Tomcat.addServlet(ctx, "ChunkedResponseWithErrorServlet", new ResponseWithErrorServlet(true));
ctx.addServletMappingDecoded("/*", "ChunkedResponseWithErrorServlet");
tomcat.start();
// @formatter:off
String request =
"GET /anything HTTP/1.1" + CRLF +
"Host: any" + CRLF +
CRLF;
// @formatter:on
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
// Expected response is a 200 response followed by an incomplete chunked
// body.
Assert.assertTrue(client.isResponse200());
// Should use chunked encoding
String transferEncoding = null;
for (String header : client.getResponseHeaders()) {
if (header.startsWith("Transfer-Encoding:")) {
transferEncoding = header.substring(18).trim();
}
}
Assert.assertEquals("chunked", transferEncoding);
// There should not be an end chunk
Assert.assertFalse(client.getResponseBody().endsWith("0"));
// The last portion of text should be there
Assert.assertTrue(client.getResponseBody().endsWith("line03" + CRLF));
}
private static class ResponseWithErrorServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final boolean useChunks;
ResponseWithErrorServlet(boolean useChunks) {
this.useChunks = useChunks;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/plain");
resp.setCharacterEncoding("UTF-8");
if (!useChunks) {
// Longer than it needs to be because response will fail before
// it is complete
resp.setContentLength(100);
}
PrintWriter pw = resp.getWriter();
pw.print("line01");
pw.flush();
resp.flushBuffer();
pw.print("line02");
pw.flush();
resp.flushBuffer();
pw.print("line03");
// Now throw a RuntimeException to end this request
throw new ServletException("Deliberate failure");
}
}
@Test
public void testWithUnknownExpectation() throws Exception {
getTomcatInstanceTestWebapp(false, true);
// @formatter:off
String request =
"POST /echo-params.jsp HTTP/1.1" + CRLF +
"Host: any" + CRLF +
"Expect: unknown" + CRLF +
CRLF;
// @formatter:on
Client client = new Client(getPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
Assert.assertTrue(client.isResponse417());
}
@Test
public void testWithTEVoid() throws Exception {
getTomcatInstanceTestWebapp(false, true);
// @formatter:off
String request =
"POST /echo-params.jsp HTTP/1.1" + CRLF +
"Host: any" + CRLF +
"Transfer-encoding: void" + CRLF +
"Content-Length: 9" + CRLF +
SimpleHttpClient.HTTP_HEADER_CONTENT_TYPE_FORM_URL_ENCODING +
CRLF +
"test=data";
// @formatter:on
Client client = new Client(getPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
Assert.assertTrue(client.isResponse501());
}
@Test
public void testWithTEBuffered() throws Exception {
getTomcatInstanceTestWebapp(false, true);
// @formatter:off
String request =
"POST /echo-params.jsp HTTP/1.1" + CRLF +
"Host: any" + CRLF +
"Transfer-encoding: buffered" + CRLF +
"Content-Length: 9" + CRLF +
SimpleHttpClient.HTTP_HEADER_CONTENT_TYPE_FORM_URL_ENCODING +
CRLF +
"test=data";
// @formatter:on
Client client = new Client(getPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
Assert.assertTrue(client.isResponse501());
}
@Test
public void testWithTEChunked() throws Exception {
doTestWithTEChunked(false);
}
@Test
public void testWithTEChunkedWithCL() throws Exception {
// Should be ignored
doTestWithTEChunked(true);
}
private void doTestWithTEChunked(boolean withCL) throws Exception {
getTomcatInstanceTestWebapp(false, true);
// @formatter:off
String request =
"POST /test/echo-params.jsp HTTP/1.1" + CRLF +
"Host: any" + CRLF +
(withCL ? "Content-length: 1" + CRLF : "") +
"Transfer-encoding: chunked" + CRLF +
SimpleHttpClient.HTTP_HEADER_CONTENT_TYPE_FORM_URL_ENCODING +
"Connection: close" + CRLF +
CRLF +
"9" + CRLF +
"test=data" + CRLF +
"0" + CRLF +
CRLF;
// @formatter:on
Client client = new Client(getPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
Assert.assertTrue(client.isResponse200());
Assert.assertTrue(client.getResponseBody().contains("test - data"));
}
@Test
public void testWithTESavedRequest() throws Exception {
getTomcatInstanceTestWebapp(false, true);
// @formatter:off
String request =
"POST /echo-params.jsp HTTP/1.1" + CRLF +
"Host: any" + CRLF +
"Transfer-encoding: savedrequest" + CRLF +
"Content-Length: 9" + CRLF +
SimpleHttpClient.HTTP_HEADER_CONTENT_TYPE_FORM_URL_ENCODING +
CRLF +
"test=data";
// @formatter:on
Client client = new Client(getPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
Assert.assertTrue(client.isResponse501());
}
@Test
public void testWithTEUnsupported() throws Exception {
getTomcatInstanceTestWebapp(false, true);
// @formatter:off
String request =
"POST /echo-params.jsp HTTP/1.1" + CRLF +
"Host: any" + CRLF +
"Transfer-encoding: unsupported" + CRLF +
"Content-Length: 9" + CRLF +
SimpleHttpClient.HTTP_HEADER_CONTENT_TYPE_FORM_URL_ENCODING +
CRLF +
"test=data";
// @formatter:on
Client client = new Client(getPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
Assert.assertTrue(client.isResponse501());
}
@Test
public void testPipelining() throws Exception {
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add protected servlet
Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet());
ctx.addServletMappingDecoded("/foo", "TesterServlet");
tomcat.start();
String requestPart1 = "GET /foo HTTP/1.1" + CRLF;
String requestPart2 = "Host: any" + CRLF + CRLF;
final Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { requestPart1, requestPart2 });
client.setRequestPause(1000);
client.setUseContentLength(true);
client.connect();
Runnable send = new Runnable() {
@Override
public void run() {
try {
client.sendRequest();
client.sendRequest();
} catch (InterruptedException | IOException e) {
throw new RuntimeException(e);
}
}
};
Thread t = new Thread(send);
t.start();
// Sleep for 1500 ms which should mean the all of request 1 has been
// sent and half of request 2
Thread.sleep(1500);
// Now read the first response
client.readResponse(true);
Assert.assertFalse(client.isResponse50x());
Assert.assertTrue(client.isResponse200());
Assert.assertEquals("OK", client.getResponseBody());
// Read the second response. No need to sleep, read will block until
// there is data to process
client.readResponse(true);
Assert.assertFalse(client.isResponse50x());
Assert.assertTrue(client.isResponse200());
Assert.assertEquals("OK", client.getResponseBody());
}
@Test
public void testPipeliningBug64974() throws Exception {
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add protected servlet
Wrapper w = Tomcat.addServlet(ctx, "servlet", new Bug64974Servlet());
w.setAsyncSupported(true);
ctx.addServletMappingDecoded("/foo", "servlet");
tomcat.start();
// @formatter:off
String request =
"GET /foo HTTP/1.1" + CRLF +
"Host: any" + CRLF +
CRLF +
"GET /foo HTTP/1.1" + CRLF +
"Host: any" + CRLF +
CRLF;
// @formatter:on
final Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.setUseContentLength(true);
client.connect();
client.sendRequest();
// Now read the first response
client.readResponse(true);
Assert.assertFalse(client.isResponse50x());
Assert.assertTrue(client.isResponse200());
Assert.assertEquals("OK", client.getResponseBody());
// Read the second response. No need to sleep, read will block until
// there is data to process
client.readResponse(true);
Assert.assertFalse(client.isResponse50x());
Assert.assertTrue(client.isResponse200());
Assert.assertEquals("OK", client.getResponseBody());
}
@Test
public void testChunking11NoContentLength() throws Exception {
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = getProgrammaticRootContext();
Tomcat.addServlet(ctx, "NoContentLengthFlushingServlet", new NoContentLengthFlushingServlet());
ctx.addServletMappingDecoded("/test", "NoContentLengthFlushingServlet");
tomcat.start();
ByteChunk responseBody = new ByteChunk();
Map<String, List<String>> responseHeaders = new HashMap<>();
int rc = getUrl("http://localhost:" + getPort() + "/test", responseBody, responseHeaders);
Assert.assertEquals(HttpServletResponse.SC_OK, rc);
String transferEncoding = getSingleHeader("Transfer-Encoding", responseHeaders);
Assert.assertEquals("chunked", transferEncoding);
}
@Test
public void testNoChunking11NoContentLengthConnectionClose() throws Exception {
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = getProgrammaticRootContext();
Tomcat.addServlet(ctx, "NoContentLengthConnectionCloseFlushingServlet",
new NoContentLengthConnectionCloseFlushingServlet());
ctx.addServletMappingDecoded("/test", "NoContentLengthConnectionCloseFlushingServlet");
tomcat.start();
ByteChunk responseBody = new ByteChunk();
Map<String, List<String>> responseHeaders = new HashMap<>();
int rc = getUrl("http://localhost:" + getPort() + "/test", responseBody, responseHeaders);
Assert.assertEquals(HttpServletResponse.SC_OK, rc);
String connection = getSingleHeader("Connection", responseHeaders);
Assert.assertEquals("close", connection);
Assert.assertFalse(responseHeaders.containsKey("Transfer-Encoding"));
Assert.assertEquals("OK", responseBody.toString());
}
@Test
public void testBug53677a() throws Exception {
doTestBug53677(false);
}
@Test
public void testBug53677b() throws Exception {
doTestBug53677(true);
}
private void doTestBug53677(boolean flush) throws Exception {
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = getProgrammaticRootContext();
Tomcat.addServlet(ctx, "LargeHeaderServlet", new LargeHeaderServlet(flush));
ctx.addServletMappingDecoded("/test", "LargeHeaderServlet");
tomcat.start();
ByteChunk responseBody = new ByteChunk();
int rc = getUrl("http://localhost:" + getPort() + "/test", responseBody, null);
Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc);
if (responseBody.getLength() > 0) {
// It will be >0 if the standard error page handling has been
// triggered
Assert.assertFalse(responseBody.toString().contains("FAIL"));
}
}
private static CountDownLatch bug55772Latch1 = new CountDownLatch(1);
private static CountDownLatch bug55772Latch2 = new CountDownLatch(1);
private static CountDownLatch bug55772Latch3 = new CountDownLatch(1);
private static boolean bug55772IsSecondRequest = false;
private static boolean bug55772RequestStateLeaked = false;
@Test
public void testBug55772() throws Exception {
Tomcat tomcat = getTomcatInstance();
Assert.assertTrue(tomcat.getConnector().setProperty("processorCache", "1"));
Assert.assertTrue(tomcat.getConnector().setProperty("maxThreads", "1"));
// No file system docBase required
Context ctx = getProgrammaticRootContext();
Tomcat.addServlet(ctx, "async", new Bug55772Servlet());
ctx.addServletMappingDecoded("/*", "async");
tomcat.start();
String request1 = "GET /async?1 HTTP/1.1 " + "Host: localhost:" + getPort() + " " +
"Connection: keep-alive " + "Cache-Control: max-age=0 " +
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 " +
"User-Agent: Request1 " + "Accept-Encoding: gzip,deflate,sdch " +
"Accept-Language: en-US,en;q=0.8,fr;q=0.6,es;q=0.4 " +
"Cookie: something.that.should.not.leak=true " + " ";
String request2 = "GET /async?2 HTTP/1.1 " + "Host: localhost:" + getPort() + " " +
"Connection: keep-alive " + "Cache-Control: max-age=0 " +
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 " +
"User-Agent: Request2 " + "Accept-Encoding: gzip,deflate,sdch " +
"Accept-Language: en-US,en;q=0.8,fr;q=0.6,es;q=0.4 " + " ";
try (Socket connection = new Socket("localhost", getPort())) {
connection.setSoLinger(true, 0);
Writer writer = new OutputStreamWriter(connection.getOutputStream(), StandardCharsets.US_ASCII);
writer.write(request1);
writer.flush();
bug55772Latch1.await();
connection.close();
}
bug55772Latch2.await();
bug55772IsSecondRequest = true;
try (Socket connection = new Socket("localhost", getPort())) {
connection.setSoLinger(true, 0);
Writer writer = new OutputStreamWriter(connection.getOutputStream(), B2CConverter.getCharset("US-ASCII"));
writer.write(request2);
writer.flush();
connection.getInputStream().read();
}
bug55772Latch3.await();
if (bug55772RequestStateLeaked) {
Assert.fail("State leaked between requests!");
}
}
// https://bz.apache.org/bugzilla/show_bug.cgi?id=57324
@Test
public void testNon2xxResponseWithExpectation() throws Exception {
doTestNon2xxResponseAndExpectation(true);
}
@Test
public void testNon2xxResponseWithoutExpectation() throws Exception {
doTestNon2xxResponseAndExpectation(false);
}
private void doTestNon2xxResponseAndExpectation(boolean useExpectation) throws Exception {
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = getProgrammaticRootContext();
Tomcat.addServlet(ctx, "echo", new EchoBodyServlet());
ctx.addServletMappingDecoded("/echo", "echo");
SecurityCollection collection = new SecurityCollection("All", "");
collection.addPatternDecoded("/*");
SecurityConstraint constraint = new SecurityConstraint();
constraint.addAuthRole("Any");
constraint.addCollection(collection);
ctx.addConstraint(constraint);
tomcat.start();
// @formatter:off
String request =
"POST /echo HTTP/1.1" + CRLF +
"Host: localhost:" + getPort() + CRLF +
"Content-Length: 10" + CRLF +
(useExpectation ? "Expect: 100-continue" + CRLF : "") +
CRLF +
"HelloWorld";
// @formatter:on
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.setUseContentLength(true);
client.connect();
client.processRequest();
Assert.assertTrue(client.isResponse403());
String connectionHeaderValue = null;
for (String header : client.getResponseHeaders()) {
if (header.startsWith("Connection:")) {
connectionHeaderValue = header.substring(header.indexOf(':') + 1).trim();
break;
}
}
if (useExpectation) {
List<String> connectionHeaders = new ArrayList<>();
TokenList.parseTokenList(new StringReader(connectionHeaderValue), connectionHeaders);
Assert.assertEquals(1, connectionHeaders.size());
Assert.assertEquals("close", connectionHeaders.get(0));
} else {
Assert.assertNull(connectionHeaderValue);
}
}
private static class Bug55772Servlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
if (bug55772IsSecondRequest) {
Cookie[] cookies = req.getCookies();
if (cookies != null && cookies.length > 0) {
for (Cookie cookie : req.getCookies()) {
if (cookie.getName().equalsIgnoreCase("something.that.should.not.leak")) {
bug55772RequestStateLeaked = true;
break;
}
}
}
bug55772Latch3.countDown();
} else {
req.getCookies(); // We have to do this so Tomcat will actually parse the cookies from the request
}
req.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.TRUE);
AsyncContext asyncContext = req.startAsync();
asyncContext.setTimeout(5000);
bug55772Latch1.countDown();
PrintWriter writer = asyncContext.getResponse().getWriter();
writer.print(' ');
writer.flush();
bug55772Latch2.countDown();
}
}
private static final class LargeHeaderServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
boolean flush = false;
LargeHeaderServlet(boolean flush) {
this.flush = flush;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String largeValue = CharBuffer.allocate(10000).toString().replace('\0', 'x');
resp.setHeader("x-Test", largeValue);
if (flush) {
resp.flushBuffer();
}
resp.setContentType("text/plain");
resp.getWriter().print("FAIL");
}
}
// flushes with no content-length set
// should result in chunking on HTTP 1.1
private static final class NoContentLengthFlushingServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setStatus(HttpServletResponse.SC_OK);
resp.setContentType("text/plain");
resp.getWriter().write("OK");
resp.flushBuffer();
}
}
// flushes with no content-length set but sets Connection: close header
// should no result in chunking on HTTP 1.1
private static final class NoContentLengthConnectionCloseFlushingServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setStatus(HttpServletResponse.SC_OK);
resp.setContentType("text/event-stream");
resp.addHeader("Connection", "close");
resp.flushBuffer();
resp.getWriter().write("OK");
resp.flushBuffer();
}
}
private static final class Client extends SimpleHttpClient {
Client(int port) {
setPort(port);
}
@Override
public boolean isResponseBodyOK() {
return getResponseBody().contains("test - data");
}
}
/*
* Partially read chunked input is not swallowed when it is read during async processing.
*/
@Test
public void testBug57621a() throws Exception {
doTestBug57621(true);
}
@Test
public void testBug57621b() throws Exception {
doTestBug57621(false);
}
private void doTestBug57621(boolean delayAsyncThread) throws Exception {
Tomcat tomcat = getTomcatInstance();
Context root = getProgrammaticRootContext();
Wrapper w = Tomcat.addServlet(root, "Bug57621", new Bug57621Servlet(delayAsyncThread));
w.setAsyncSupported(true);
root.addServletMappingDecoded("/test", "Bug57621");
tomcat.start();
Bug57621Client client = new Bug57621Client();
client.setPort(tomcat.getConnector().getLocalPort());
client.setUseContentLength(true);
client.connect();
client.doRequest();
Assert.assertTrue(client.getResponseLine(), client.isResponse200());
Assert.assertTrue(client.isResponseBodyOK());
// Do the request again to ensure that the remaining body was swallowed
client.resetResponse();
client.processRequest();
Assert.assertTrue(client.isResponse200());
Assert.assertTrue(client.isResponseBodyOK());
client.disconnect();
}
private static class Bug57621Servlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final boolean delayAsyncThread;
Bug57621Servlet(boolean delayAsyncThread) {
this.delayAsyncThread = delayAsyncThread;
}
@Override
protected void doPut(HttpServletRequest req, final HttpServletResponse resp)
throws ServletException, IOException {
final AsyncContext ac = req.startAsync();
ac.start(new Runnable() {
@Override
public void run() {
if (delayAsyncThread) {
// Makes the difference between calling complete before
// the request body is received or after.
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
resp.setContentType("text/plain");
resp.setCharacterEncoding("UTF-8");
try {
resp.getWriter().print("OK");
} catch (IOException ignore) {
// Should never happen. Test will fail if it does.
}
ac.complete();
}
});
}
}
private static class Bug57621Client extends SimpleHttpClient {
private Exception doRequest() {
try {
String[] request = new String[2];
request[0] = "PUT http://localhost:8080/test HTTP/1.1" + CRLF + "Host: localhost:8080" + CRLF +
"Transfer-encoding: chunked" + CRLF + CRLF + "2" + CRLF + "OK";
request[1] = CRLF + "0" + CRLF + CRLF;
setRequest(request);
processRequest(); // blocks until response has been read
} catch (Exception e) {
return e;
}
return null;
}
@Override
public boolean isResponseBodyOK() {
if (getResponseBody() == null) {
return false;
}
if (!getResponseBody().contains("OK")) {
return false;
}
return true;
}
}
@Test
public void testBug59310() throws Exception {
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = getProgrammaticRootContext();
Tomcat.addServlet(ctx, "Bug59310", new Bug59310Servlet());
ctx.addServletMappingDecoded("/test", "Bug59310");
tomcat.start();
ByteChunk getBody = new ByteChunk();
Map<String, List<String>> getHeaders = new HashMap<>();
int getStatus = getUrl("http://localhost:" + getPort() + "/test", getBody, getHeaders);
ByteChunk headBody = new ByteChunk();
Map<String, List<String>> headHeaders = new HashMap<>();
int headStatus = getUrl("http://localhost:" + getPort() + "/test", headBody, headHeaders);
Assert.assertEquals(HttpServletResponse.SC_OK, getStatus);
Assert.assertEquals(HttpServletResponse.SC_OK, headStatus);
Assert.assertEquals(2, getBody.getLength());
Assert.assertEquals(2, headBody.getLength());
if (getHeaders.containsKey("Content-Length")) {
Assert.assertEquals(getHeaders.get("Content-Length"), headHeaders.get("Content-Length"));
} else {
Assert.assertFalse(headHeaders.containsKey("Content-Length"));
}
}
private static class Bug59310Servlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().print("OK");
}
@Override
protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().print("OK");
}
}
/*
* Tests what happens if a request is completed during a dispatch but the request body has not been fully read.
*/
@Test
public void testRequestBodySwallowing() throws Exception {
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = getProgrammaticRootContext();
DispatchingServlet servlet = new DispatchingServlet();
Wrapper w = Tomcat.addServlet(ctx, "Test", servlet);
w.setAsyncSupported(true);
ctx.addServletMappingDecoded("/test", "Test");
tomcat.start();
// Hand-craft the client so we have complete control over the timing
SocketAddress addr = new InetSocketAddress("localhost", getPort());
Socket socket = new Socket();
socket.setSoTimeout(300000);
socket.connect(addr, 300000);
OutputStream os = socket.getOutputStream();
Writer writer = new OutputStreamWriter(os, "ISO-8859-1");
InputStream is = socket.getInputStream();
Reader r = new InputStreamReader(is, "ISO-8859-1");
BufferedReader reader = new BufferedReader(r);
// Write the headers
writer.write("POST /test HTTP/1.1 ");
writer.write("Host: localhost:8080 ");
writer.write("Transfer-Encoding: chunked ");
writer.write(" ");
writer.flush();
validateResponse(reader);
// Write the request body
writer.write("2 ");
writer.write("AB ");
writer.write("0 ");
writer.write(" ");
writer.flush();
// Write the 2nd request
writer.write("POST /test HTTP/1.1 ");
writer.write("Host: localhost:8080 ");
writer.write("Transfer-Encoding: chunked ");
writer.write(" ");
writer.flush();
// Read the 2nd response
validateResponse(reader);
// Write the 2nd request body
writer.write("2 ");
writer.write("AB ");
writer.write("0 ");
writer.write(" ");
writer.flush();
// Done
socket.close();
}
private void validateResponse(BufferedReader reader) throws IOException {
// First line has the response code and should always be 200
String line = reader.readLine();
Assert.assertEquals("HTTP/1.1 200 ", line);
while (!"OK".equals(line)) {
line = reader.readLine();
}
}
private static class DispatchingServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
if (DispatcherType.ASYNC.equals(req.getDispatcherType())) {
resp.setContentType("text/plain");
resp.setCharacterEncoding("UTF-8");
resp.getWriter().write("OK ");
} else {
req.startAsync().dispatch();
}
}
}
@Test
public void testBug61086() throws Exception {
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = getProgrammaticRootContext();
Bug61086Servlet servlet = new Bug61086Servlet();
Tomcat.addServlet(ctx, "Test", servlet);
ctx.addServletMappingDecoded("/test", "Test");
tomcat.start();
ByteChunk responseBody = new ByteChunk();
Map<String, List<String>> responseHeaders = new HashMap<>();
int rc = getUrl("http://localhost:" + getPort() + "/test", responseBody, responseHeaders);
Assert.assertEquals(HttpServletResponse.SC_RESET_CONTENT, rc);
String contentLength = getSingleHeader("Content-Length", responseHeaders);
Assert.assertEquals("0", contentLength);
Assert.assertTrue(responseBody.getLength() == 0);
}
private static final class Bug61086Servlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setStatus(205);
}
}
/*
* Multiple, different Host headers
*/
@Test
public void testMultipleHostHeader01() throws Exception {
Tomcat tomcat = getTomcatInstance();
// This setting means the connection will be closed at the end of the
// request
Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1"));
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add servlet
Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet());
ctx.addServletMappingDecoded("/foo", "TesterServlet");
tomcat.start();
// @formatter:off
String request =
"GET /foo HTTP/1.1" + CRLF +
"Host: a" + CRLF +
"Host: b" + CRLF +
CRLF;
// @formatter:on
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
// Expected response is a 400 response.
Assert.assertTrue(client.isResponse400());
}
/*
* Multiple instances of the same Host header
*/
@Test
public void testMultipleHostHeader02() throws Exception {
Tomcat tomcat = getTomcatInstance();
// This setting means the connection will be closed at the end of the
// request
Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1"));
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add servlet
Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet());
ctx.addServletMappingDecoded("/foo", "TesterServlet");
tomcat.start();
// @formatter:off
String request =
"GET /foo HTTP/1.1" + CRLF +
"Host: a" + CRLF +
"Host: a" + CRLF +
CRLF;
// @formatter:on
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
// Expected response is a 400 response.
Assert.assertTrue(client.isResponse400());
}
@Test
public void testMissingHostHeader() throws Exception {
Tomcat tomcat = getTomcatInstance();
// This setting means the connection will be closed at the end of the
// request
Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1"));
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add servlet
Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet());
ctx.addServletMappingDecoded("/foo", "TesterServlet");
tomcat.start();
String request = "GET /foo HTTP/1.1" + CRLF + CRLF;
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
// Expected response is a 400 response.
Assert.assertTrue(client.isResponse400());
}
@Test
public void testInconsistentHostHeader01() throws Exception {
Tomcat tomcat = getTomcatInstance();
// This setting means the connection will be closed at the end of the
// request
Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1"));
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add servlet
Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet());
ctx.addServletMappingDecoded("/foo", "TesterServlet");
tomcat.start();
// @formatter:off
String request =
"GET http://a/foo HTTP/1.1" + CRLF +
"Host: b" + CRLF +
CRLF;
// @formatter:on
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
// Expected response is a 400 response.
Assert.assertTrue(client.isResponse400());
}
@Test
public void testInconsistentHostHeader02() throws Exception {
Tomcat tomcat = getTomcatInstance();
// This setting means the connection will be closed at the end of the
// request
Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1"));
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add servlet
Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet());
ctx.addServletMappingDecoded("/foo", "TesterServlet");
tomcat.start();
// @formatter:off
String request =
"GET http://a:8080/foo HTTP/1.1" + CRLF +
"Host: b:8080" + CRLF +
CRLF;
// @formatter:on
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
// Expected response is a 400 response.
Assert.assertTrue(client.isResponse400());
}
@Test
public void testInconsistentHostHeader03() throws Exception {
Tomcat tomcat = getTomcatInstance();
// This setting means the connection will be closed at the end of the
// request
Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1"));
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add servlet
Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet());
ctx.addServletMappingDecoded("/foo", "TesterServlet");
tomcat.start();
// @formatter:off
String request =
"GET http://user:pwd@a/foo HTTP/1.1" + CRLF +
"Host: b" + CRLF +
CRLF;
// @formatter:on
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
// Expected response is a 400 response.
Assert.assertTrue(client.isResponse400());
}
/*
* Hostname (no port) is included in the request line, but Host header is empty. Added for bug 62739.
*/
@Test
public void testInconsistentHostHeader04() throws Exception {
Tomcat tomcat = getTomcatInstance();
// This setting means the connection will be closed at the end of the
// request
Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1"));
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add servlet
Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet());
ctx.addServletMappingDecoded("/foo", "TesterServlet");
tomcat.start();
// @formatter:off
String request =
"GET http://a/foo HTTP/1.1" + CRLF +
"Host: " + CRLF +
CRLF;
// @formatter:on
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
// Expected response is a 400 response.
Assert.assertTrue(client.isResponse400());
}
/*
* Hostname (with port) is included in the request line, but Host header is empty. Added for bug 62739.
*/
@Test
public void testInconsistentHostHeader05() throws Exception {
Tomcat tomcat = getTomcatInstance();
// This setting means the connection will be closed at the end of the
// request
Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1"));
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add servlet
Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet());
ctx.addServletMappingDecoded("/foo", "TesterServlet");
tomcat.start();
// @formatter:off
String request =
"GET http://a:8080/foo HTTP/1.1" + CRLF +
"Host: " + CRLF +
CRLF;
// @formatter:on
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
// Expected response is a 400 response.
Assert.assertTrue(client.isResponse400());
}
/*
* Hostname (with port and user) is included in the request line, but Host header is empty. Added for bug 62739.
*/
@Test
public void testInconsistentHostHeader06() throws Exception {
Tomcat tomcat = getTomcatInstance();
// This setting means the connection will be closed at the end of the
// request
Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1"));
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add servlet
Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet());
ctx.addServletMappingDecoded("/foo", "TesterServlet");
tomcat.start();
// @formatter:off
String request =
"GET http://user:pwd@a/foo HTTP/1.1" + CRLF +
"Host: " + CRLF +
CRLF;
// @formatter:on
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
// Expected response is a 400 response.
Assert.assertTrue(client.isResponse400());
}
/*
* Request line host is an exact match for Host header (no port)
*/
@Test
public void testConsistentHostHeader01() throws Exception {
Tomcat tomcat = getTomcatInstance();
// This setting means the connection will be closed at the end of the
// request
Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1"));
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add servlet
Tomcat.addServlet(ctx, "TesterServlet", new ServerNameTesterServlet());
ctx.addServletMappingDecoded("/foo", "TesterServlet");
tomcat.start();
// @formatter:off
String request =
"GET http://a/foo HTTP/1.1" + CRLF +
"Host: a" + CRLF +
CRLF;
// @formatter:on
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
// Expected response is a 200 response.
Assert.assertTrue(client.isResponse200());
Assert.assertEquals("request.getServerName() is [a] and request.getServerPort() is 80",
client.getResponseBody());
}
/*
* Request line host is an exact match for Host header (with port)
*/
@Test
public void testConsistentHostHeader02() throws Exception {
Tomcat tomcat = getTomcatInstance();
// This setting means the connection will be closed at the end of the
// request
Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1"));
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add servlet
Tomcat.addServlet(ctx, "TesterServlet", new ServerNameTesterServlet());
ctx.addServletMappingDecoded("/foo", "TesterServlet");
tomcat.start();
// @formatter:off
String request =
"GET http://a:8080/foo HTTP/1.1" + CRLF +
"Host: a:8080" + CRLF +
CRLF;
// @formatter:on
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
// Expected response is a 200 response.
Assert.assertTrue(client.isResponse200());
Assert.assertEquals("request.getServerName() is [a] and request.getServerPort() is 8080",
client.getResponseBody());
}
/*
* Request line host is an exact match for Host header (no port, with user info)
*/
@Test
public void testConsistentHostHeader03() throws Exception {
Tomcat tomcat = getTomcatInstance();
// This setting means the connection will be closed at the end of the
// request
Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1"));
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add servlet
Tomcat.addServlet(ctx, "TesterServlet", new ServerNameTesterServlet());
ctx.addServletMappingDecoded("/foo", "TesterServlet");
tomcat.start();
// @formatter:off
String request =
"GET http://user:pwd@a/foo HTTP/1.1" + CRLF +
"Host: a" + CRLF +
CRLF;
// @formatter:on
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
// Expected response is a 200 response.
Assert.assertTrue(client.isResponse200());
Assert.assertEquals("request.getServerName() is [a] and request.getServerPort() is 80",
client.getResponseBody());
}
/*
* Request line host is case insensitive match for Host header (no port, no user info)
*/
@Test
public void testConsistentHostHeader04() throws Exception {
Tomcat tomcat = getTomcatInstance();
// This setting means the connection will be closed at the end of the
// request
Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1"));
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add servlet
Tomcat.addServlet(ctx, "TesterServlet", new ServerNameTesterServlet());
ctx.addServletMappingDecoded("/foo", "TesterServlet");
tomcat.start();
// @formatter:off
String request =
"GET http://a/foo HTTP/1.1" + CRLF +
"Host: A" + CRLF +
CRLF;
// @formatter:on
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
// Expected response is a 200 response.
Assert.assertTrue(client.isResponse200());
Assert.assertEquals("request.getServerName() is [A] and request.getServerPort() is 80",
client.getResponseBody());
}
/*
* Host header exists but its value is an empty string. This is valid if the request line does not include a
* hostname/port. Added for bug 62739.
*/
@Test
public void testBlankHostHeader01() throws Exception {
Tomcat tomcat = getTomcatInstance();
// This setting means the connection will be closed at the end of the
// request
Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1"));
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add servlet
Tomcat.addServlet(ctx, "TesterServlet", new ServerNameTesterServlet());
ctx.addServletMappingDecoded("/foo", "TesterServlet");
tomcat.start();
// @formatter:off
String request =
"GET /foo HTTP/1.1" + CRLF +
"Host: " + CRLF +
CRLF;
// @formatter:on
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
// Expected response is a 200 response.
Assert.assertTrue(client.isResponse200());
Assert.assertEquals("request.getServerName() is [] and request.getServerPort() is " + getPort(),
client.getResponseBody());
}
/*
* Host header exists but has its value is empty (and there are multiple spaces after the ':'. This is valid if the
* request line does not include a hostname/port. Added for bug 62739.
*/
@Test
public void testBlankHostHeader02() throws Exception {
Tomcat tomcat = getTomcatInstance();
// This setting means the connection will be closed at the end of the
// request
Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1"));
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add servlet
Tomcat.addServlet(ctx, "TesterServlet", new ServerNameTesterServlet());
ctx.addServletMappingDecoded("/foo", "TesterServlet");
tomcat.start();
// @formatter:off
String request =
"GET /foo HTTP/1.1" + CRLF +
"Host: " + CRLF +
CRLF;
// @formatter:on
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
// Expected response is a 200 response.
Assert.assertTrue(client.isResponse200());
Assert.assertEquals("request.getServerName() is [] and request.getServerPort() is " + getPort(),
client.getResponseBody());
}
@Test
public void testKeepAliveHeader01() throws Exception {
doTestKeepAliveHeader(false, 3000, 10, false);
}
@Test
public void testKeepAliveHeader02() throws Exception {
doTestKeepAliveHeader(true, 5000, 1, false);
}
@Test
public void testKeepAliveHeader03() throws Exception {
doTestKeepAliveHeader(true, 5000, 10, false);
}
@Test
public void testKeepAliveHeader04() throws Exception {
doTestKeepAliveHeader(true, -1, 10, false);
}
@Test
public void testKeepAliveHeader05() throws Exception {
doTestKeepAliveHeader(true, -1, 1, false);
}
@Test
public void testKeepAliveHeader06() throws Exception {
doTestKeepAliveHeader(true, -1, -1, false);
}
@Test
public void testKeepAliveHeader07() throws Exception {
doTestKeepAliveHeader(false, 3000, 10, true);
}
@Test
public void testKeepAliveHeader08() throws Exception {
doTestKeepAliveHeader(true, 5000, 1, true);
}
@Test
public void testKeepAliveHeader09() throws Exception {
doTestKeepAliveHeader(true, 5000, 10, true);
}
@Test
public void testKeepAliveHeader10() throws Exception {
doTestKeepAliveHeader(true, -1, 10, true);
}
@Test
public void testKeepAliveHeader11() throws Exception {
doTestKeepAliveHeader(true, -1, 1, true);
}
@Test
public void testKeepAliveHeader12() throws Exception {
doTestKeepAliveHeader(true, -1, -1, true);
}
private void doTestKeepAliveHeader(boolean sendKeepAlive, int keepAliveTimeout, int maxKeepAliveRequests,
boolean explicitClose) throws Exception {
Tomcat tomcat = getTomcatInstance();
tomcat.getConnector().setProperty("keepAliveTimeout", Integer.toString(keepAliveTimeout));
tomcat.getConnector().setProperty("maxKeepAliveRequests", Integer.toString(maxKeepAliveRequests));
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add servlet
Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet(explicitClose));
ctx.addServletMappingDecoded("/foo", "TesterServlet");
tomcat.start();
// @formatter:off
String request =
"GET /foo HTTP/1.1" + CRLF +
"Host: localhost:" + getPort() + CRLF +
(sendKeepAlive ? "Connection: keep-alive" + CRLF : "") +
CRLF;
// @formatter:on
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest(false);
Assert.assertTrue(client.isResponse200());
String connectionHeaderValue = null;
String keepAliveHeaderValue = null;
for (String header : client.getResponseHeaders()) {
if (header.startsWith("Connection:")) {
connectionHeaderValue = header.substring(header.indexOf(':') + 1).trim();
}
if (header.startsWith("Keep-Alive:")) {
keepAliveHeaderValue = header.substring(header.indexOf(':') + 1).trim();
}
}
if (explicitClose) {
Assert.assertEquals("close", connectionHeaderValue);
Assert.assertNull(keepAliveHeaderValue);
} else if (!sendKeepAlive || keepAliveTimeout < 0 && (maxKeepAliveRequests < 0 || maxKeepAliveRequests > 1)) {
Assert.assertNull(connectionHeaderValue);
Assert.assertNull(keepAliveHeaderValue);
} else {
List<String> connectionHeaders = new ArrayList<>();
TokenList.parseTokenList(new StringReader(connectionHeaderValue), connectionHeaders);
if (sendKeepAlive && keepAliveTimeout > 0 && (maxKeepAliveRequests < 0 || maxKeepAliveRequests > 1)) {
Assert.assertEquals(1, connectionHeaders.size());
Assert.assertEquals("keep-alive", connectionHeaders.get(0));
Assert.assertEquals("timeout=" + keepAliveTimeout / 1000L, keepAliveHeaderValue);
}
if (sendKeepAlive && maxKeepAliveRequests == 1) {
Assert.assertEquals(1, connectionHeaders.size());
Assert.assertEquals("close", connectionHeaders.get(0));
Assert.assertNull(keepAliveHeaderValue);
}
}
}
/**
* Test servlet that prints out the values of HttpServletRequest.getServerName() and
* HttpServletRequest.getServerPort() in the response body, e.g.: "request.getServerName() is [foo] and
* request.getServerPort() is 8080" or: "request.getServerName() is null and request.getServerPort() is 8080"
*/
private static class ServerNameTesterServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/plain");
PrintWriter out = resp.getWriter();
if (null == req.getServerName()) {
out.print("request.getServerName() is null");
} else {
out.print("request.getServerName() is [" + req.getServerName() + "]");
}
out.print(" and request.getServerPort() is " + req.getServerPort());
}
}
@Test
public void testSlowUploadTimeoutWithLongerUploadTimeout() throws Exception {
doTestSlowUploadTimeout(true);
}
@Test
public void testSlowUploadTimeoutWithoutLongerUploadTimeout() throws Exception {
doTestSlowUploadTimeout(false);
}
private void doTestSlowUploadTimeout(boolean useLongerUploadTimeout) throws Exception {
Tomcat tomcat = getTomcatInstance();
Connector connector = tomcat.getConnector();
int connectionTimeout = ((Integer) connector.getProperty("connectionTimeout")).intValue();
// These factors should make the differences large enough that the CI
// tests pass consistently. If not, may need to reduce connectionTimeout
// and increase delay and connectionUploadTimeout
int delay = connectionTimeout * 2;
int connectionUploadTimeout = connectionTimeout * 4;
if (useLongerUploadTimeout) {
connector.setProperty("connectionUploadTimeout", "" + connectionUploadTimeout);
connector.setProperty("disableUploadTimeout", "false");
}
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add servlet
Tomcat.addServlet(ctx, "TesterServlet", new SwallowBodyTesterServlet());
ctx.addServletMappingDecoded("/foo", "TesterServlet");
tomcat.start();
// @formatter:off
String request =
"POST /foo HTTP/1.1" + CRLF +
"Host: localhost:" + getPort() + CRLF +
"Content-Length: 10" + CRLF +
CRLF;
// @formatter:on
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request, "XXXXXXXXXX" });
client.setRequestPause(delay);
client.connect();
try {
client.processRequest();
} catch (IOException ioe) {
// Failure is expected on some platforms (notably Windows) if the
// longer upload timeout is not used but record the exception in
// case it is useful for debugging purposes.
// The assertions below will check for the correct behaviour.
ioe.printStackTrace();
}
if (useLongerUploadTimeout) {
// Expected response is a 200 response.
Assert.assertTrue(client.isResponse200());
Assert.assertEquals("OK", client.getResponseBody());
} else {
// Different failure modes with different connectors
Assert.assertFalse(client.isResponse200());
}
}
private static class SwallowBodyTesterServlet extends TesterServlet {
private static final long serialVersionUID = 1L;
SwallowBodyTesterServlet() {
super(true);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// Swallow the body
byte[] buf = new byte[1024];
InputStream is = req.getInputStream();
while (is.read(buf) > 0) {
// Loop
}
// Standard response
doGet(req, resp);
}
}
private static class Bug64974Servlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// Get requests can have bodies although these requests don't.
// Needs to be async to trigger the problematic code path
AsyncContext ac = req.startAsync();
ServletInputStream sis = req.getInputStream();
// This triggers a call to Http11InputBuffer.available(true) which
// did not handle the pipelining case.
sis.setReadListener(new Bug64974ReadListener());
ac.complete();
resp.setContentType("text/plain");
PrintWriter out = resp.getWriter();
out.print("OK");
}
}
private static class Bug64974ReadListener implements ReadListener {
@Override
public void onDataAvailable() throws IOException {
// NO-OP
}
@Override
public void onAllDataRead() throws IOException {
// NO-OP
}
@Override
public void onError(Throwable throwable) {
// NO-OP
}
}
@Test
public void testTEHeaderUnknown01() throws Exception {
doTestTEHeaderInvalid("identity", false);
}
@Test
public void testTEHeaderUnknown02() throws Exception {
doTestTEHeaderInvalid("identity, chunked", false);
}
@Test
public void testTEHeaderUnknown03() throws Exception {
doTestTEHeaderInvalid("unknown, chunked", false);
}
@Test
public void testTEHeaderUnknown04() throws Exception {
doTestTEHeaderInvalid("void", false);
}
@Test
public void testTEHeaderUnknown05() throws Exception {
doTestTEHeaderInvalid("void, chunked", false);
}
@Test
public void testTEHeaderUnknown06() throws Exception {
doTestTEHeaderInvalid("void, identity", false);
}
@Test
public void testTEHeaderUnknown07() throws Exception {
doTestTEHeaderInvalid("identity, void", false);
}
@Test
public void testTEHeaderChunkedNotLast01() throws Exception {
doTestTEHeaderInvalid("chunked, void", true);
}
private void doTestTEHeaderInvalid(String headerValue, boolean badRequest) throws Exception {
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add servlet
Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet(false));
ctx.addServletMappingDecoded("/foo", "TesterServlet");
tomcat.start();
// @formatter:off
String request =
"GET /foo HTTP/1.1" + CRLF +
"Host: localhost:" + getPort() + CRLF +
"Transfer-Encoding: " + headerValue + CRLF +
CRLF;
// @formatter:on
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest(false);
if (badRequest) {
Assert.assertTrue(client.isResponse400());
} else {
Assert.assertTrue(client.isResponse501());
}
}
@Test
public void testWithTEChunkedHttp10() throws Exception {
getTomcatInstanceTestWebapp(false, true);
// @formatter:off
String request =
"POST /test/echo-params.jsp HTTP/1.0" + CRLF +
"Host: any" + CRLF +
"Transfer-encoding: chunked" + CRLF +
SimpleHttpClient.HTTP_HEADER_CONTENT_TYPE_FORM_URL_ENCODING +
"Connection: close" + CRLF +
CRLF +
"9" + CRLF +
"test=data" + CRLF +
"0" + CRLF +
CRLF;
// @formatter:on
Client client = new Client(getPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
Assert.assertTrue(client.isResponse200());
Assert.assertTrue(client.getResponseBody().contains("test - data"));
}
@Test
public void test100ContinueWithNoAck() throws Exception {
Tomcat tomcat = getTomcatInstance();
final Connector connector = tomcat.getConnector();
connector.setProperty("continueResponseTiming", "onRead");
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add servlet
Tomcat.addServlet(ctx, "TestPostNoReadServlet", new TestPostNoReadServlet());
ctx.addServletMappingDecoded("/foo", "TestPostNoReadServlet");
tomcat.start();
// @formatter:off
String request =
"POST /foo HTTP/1.1" + CRLF +
"Host: localhost:" + getPort() + CRLF +
"Expect: 100-continue" + CRLF +
"Content-Length: 10" + CRLF +
CRLF +
"0123456789";
// @formatter:on
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest(false);
Assert.assertTrue(client.isResponse200());
if (client.getResponseHeaders().contains("Connection: close")) {
client.connect();
}
client.processRequest(false);
Assert.assertTrue(client.isResponse200());
}
@Test
public void testConnect() throws Exception {
getTomcatInstanceTestWebapp(false, true);
// @formatter:off
String request =
"CONNECT example.local HTTP/1.1" + CRLF +
"Host: example.local" + CRLF +
CRLF;
// @formatter:on
Client client = new Client(getPort());
client.setRequest(new String[] { request });
client.connect();
client.processRequest();
Assert.assertTrue(client.isResponse501());
}
private static class TestPostNoReadServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}
@Test
public void testEarlyHints() throws Exception {
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add servlet
Tomcat.addServlet(ctx, "EarlyHintsServlet", new EarlyHintsServlet());
ctx.addServletMappingDecoded("/ehs", "EarlyHintsServlet");
tomcat.start();
// @formatter:off
String request =
"GET /ehs HTTP/1.1" + CRLF +
"Host: localhost:" + getPort() + CRLF +
CRLF;
// @formatter:on
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.connect(600000, 600000);
client.processRequest(false);
Assert.assertEquals(103, client.getStatusCode());
client.readResponse(false);
Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
}
@Test
public void testEarlyHintsSendError() throws Exception {
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add servlet
Tomcat.addServlet(ctx, "EarlyHintsServlet", new EarlyHintsServlet(true, null));
ctx.addServletMappingDecoded("/ehs", "EarlyHintsServlet");
tomcat.start();
// @formatter:off
String request =
"GET /ehs HTTP/1.1" + CRLF +
"Host: localhost:" + getPort() + CRLF +
CRLF;
// @formatter:on
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.connect(600000, 600000);
client.processRequest(false);
Assert.assertEquals(103, client.getStatusCode());
client.readResponse(false);
Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
}
@Test
public void testEarlyHintsSendErrorWithMessage() throws Exception {
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = getProgrammaticRootContext();
// Add servlet
Tomcat.addServlet(ctx, "EarlyHintsServlet", new EarlyHintsServlet(true, "ignored"));
ctx.addServletMappingDecoded("/ehs", "EarlyHintsServlet");
tomcat.start();
// @formatter:off
String request =
"GET /ehs HTTP/1.1" + CRLF +
"Host: localhost:" + getPort() + CRLF +
CRLF;
// @formatter:on
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] { request });
client.connect(600000, 600000);
client.processRequest(false);
Assert.assertEquals(103, client.getStatusCode());
client.readResponse(false);
Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
}
private static class EarlyHintsServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final boolean useSendError;
private final String errorString;
EarlyHintsServlet() {
this(false, null);
}
EarlyHintsServlet(boolean useSendError, String errorString) {
this.useSendError = useSendError;
this.errorString = errorString;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.addHeader("Link", "</style.css>; rel=preload; as=style");
if (useSendError) {
if (null == errorString) {
resp.sendError(103);
} else {
resp.sendError(103, errorString);
}
} else {
((ResponseFacade) resp).sendEarlyHints();
}
resp.setCharacterEncoding(StandardCharsets.UTF_8);
resp.setContentType("text/plain");
resp.getWriter().write("OK");
}
}
}