This commit is contained in:
2024-11-30 19:03:49 +08:00
commit 1e6763c160
3806 changed files with 737676 additions and 0 deletions

View File

@@ -0,0 +1,173 @@
/*
* 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.catalina.connector;
import java.io.File;
import java.net.SocketTimeoutException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.Servlet;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.apache.catalina.servlets.DefaultServlet;
import org.apache.catalina.servlets.WebdavServlet;
import org.apache.catalina.startup.TesterServlet;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
import org.apache.tomcat.util.buf.ByteChunk;
/**
* Test cases for {@link Connector}.
*/
public class TestConnector extends TomcatBaseTest {
@Test
public void testStop() throws Exception {
Tomcat tomcat = getTomcatInstance();
Context root = tomcat.addContext("", TEMP_DIR);
Wrapper w =
Tomcat.addServlet(root, "tester", new TesterServlet());
w.setAsyncSupported(true);
root.addServletMappingDecoded("/", "tester");
Connector connector = tomcat.getConnector();
tomcat.start();
ByteChunk bc = new ByteChunk();
int rc = getUrl("http://localhost:" + getPort() + "/", bc, null, null);
Assert.assertEquals(200, rc);
Assert.assertEquals("OK", bc.toString());
rc = -1;
bc.recycle();
connector.stop();
try {
rc = getUrl("http://localhost:" + getPort() + "/", bc, 1000,
null, null);
} catch (SocketTimeoutException ste) {
// May also see this with NIO
// Make sure the test passes if we do
rc = 503;
}
Assert.assertEquals(503, rc);
}
@Test
public void testPort() throws Exception {
Tomcat tomcat = getTomcatInstance();
Connector connector1 = tomcat.getConnector();
connector1.setPort(0);
Connector connector2 = new Connector();
connector2.setPort(0);
tomcat.getService().addConnector(connector2);
tomcat.start();
int localPort1 = connector1.getLocalPort();
int localPort2 = connector2.getLocalPort();
Assert.assertTrue(localPort1 > 0);
Assert.assertTrue(localPort2 > 0);
}
@Test
public void testTraceAllowedDefault() throws Exception {
doTestTrace(new DefaultServlet(), true);
}
@Test
public void testTraceNotAllowedDefault() throws Exception {
doTestTrace(new DefaultServlet(), false);
}
@Test
public void testTraceAllowedWebDav() throws Exception {
doTestTrace(new WebdavServlet(), true);
}
@Test
public void testTraceNotAllowedWebDav() throws Exception {
doTestTrace(new WebdavServlet(), false);
}
@Test
public void testTraceAllowedCustom() throws Exception {
doTestTrace(new TesterServlet(), true);
}
@Test
public void testTraceNotAllowedCustom() throws Exception {
doTestTrace(new TesterServlet(), false);
}
private void doTestTrace(Servlet servlet, boolean allowTrace) throws Exception {
Tomcat tomcat = getTomcatInstance();
File appDir = new File("test/webapp");
Context root = tomcat.addContext("", appDir.getAbsolutePath());
Tomcat.addServlet(root, "default", servlet);
root.addServletMappingDecoded("/", "default");
Connector connector = tomcat.getConnector();
connector.setAllowTrace(allowTrace);
tomcat.start();
ByteChunk bc = new ByteChunk();
Map<String,List<String>> respHeaders = new HashMap<>();
int rc = methodUrl("http://localhost:" + getPort() + "/index.html",
bc, 30000, null, respHeaders, "OPTIONS");
Assert.assertEquals(200, rc);
boolean foundTrace = false;
for (String header : respHeaders.get("Allow")) {
if (header.contains("TRACE")) {
foundTrace = true;
break;
}
}
if (allowTrace) {
Assert.assertTrue(foundTrace);
} else {
Assert.assertFalse(foundTrace);
}
}
}

View File

@@ -0,0 +1,413 @@
/*
* 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.catalina.connector;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.apache.catalina.startup.SimpleHttpClient;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.buf.MessageBytes;
public class TestCoyoteAdapter extends TomcatBaseTest {
public static final String TEXT_8K;
public static final byte[] BYTES_8K;
static {
StringBuilder sb = new StringBuilder(8192);
for (int i = 0; i < 512; i++) {
sb.append("0123456789ABCDEF");
}
TEXT_8K = sb.toString();
BYTES_8K = TEXT_8K.getBytes(StandardCharsets.UTF_8);
}
@Test
public void testPathParmsRootNone() throws Exception {
pathParamTest("/", "none");
}
@Test
public void testPathParmsFooNone() throws Exception {
pathParamTest("/foo", "none");
}
@Test
public void testPathParmsRootSessionOnly() throws Exception {
pathParamTest("/;jsessionid=1234", "1234");
}
@Test
public void testPathParmsFooSessionOnly() throws Exception {
pathParamTest("/foo;jsessionid=1234", "1234");
}
@Test
public void testPathParmsFooSessionDummy() throws Exception {
pathParamTest("/foo;jsessionid=1234;dummy", "1234");
}
@Test
public void testPathParmsFooSessionDummyValue() throws Exception {
pathParamTest("/foo;jsessionid=1234;dummy=5678", "1234");
}
@Test
public void testPathParmsFooSessionValue() throws Exception {
pathParamTest("/foo;jsessionid=1234;=5678", "1234");
}
@Test
public void testPathParmsFooSessionBar() throws Exception {
pathParamTest("/foo;jsessionid=1234/bar", "1234");
}
@Test
public void testPathParamsRedirect() throws Exception {
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// Must have a real docBase. Don't use java.io.tmpdir as it may not be
// writable.
File docBase = new File(getTemporaryDirectory(), "testCoyoteAdapter");
addDeleteOnTearDown(docBase);
if (!docBase.mkdirs() && !docBase.isDirectory()) {
Assert.fail("Failed to create: [" + docBase.toString() + "]");
}
// Create the folder that will trigger the redirect
File foo = new File(docBase, "foo");
addDeleteOnTearDown(foo);
if (!foo.mkdirs() && !foo.isDirectory()) {
Assert.fail("Unable to create foo directory in docBase");
}
Context ctx = tomcat.addContext("", docBase.getAbsolutePath());
Tomcat.addServlet(ctx, "servlet", new PathParamServlet());
ctx.addServletMappingDecoded("/", "servlet");
tomcat.start();
testPath("/", "none");
testPath("/;jsessionid=1234", "1234");
testPath("/foo;jsessionid=1234", "1234");
testPath("/foo;jsessionid=1234;dummy", "1234");
testPath("/foo;jsessionid=1234;dummy=5678", "1234");
testPath("/foo;jsessionid=1234;=5678", "1234");
testPath("/foo;jsessionid=1234/bar", "1234");
}
private void pathParamTest(String path, String expected) throws Exception {
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("", null);
Tomcat.addServlet(ctx, "servlet", new PathParamServlet());
ctx.addServletMappingDecoded("/", "servlet");
tomcat.start();
ByteChunk res = getUrl("http://localhost:" + getPort() + path);
Assert.assertEquals(expected, res.toString());
}
private void testPath(String path, String expected) throws Exception {
ByteChunk res = getUrl("http://localhost:" + getPort() + path);
Assert.assertEquals(expected, res.toString());
}
private static class PathParamServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/plain");
PrintWriter pw = resp.getWriter();
String sessionId = req.getRequestedSessionId();
if (sessionId == null) {
sessionId = "none";
}
pw.write(sessionId);
}
}
@Test
public void testPathParamExtRootNoParam() throws Exception {
pathParamExtensionTest("/testapp/blah.txt", "none");
}
@Test
public void testPathParamExtLevel1NoParam() throws Exception {
pathParamExtensionTest("/testapp/blah/blah.txt", "none");
}
@Test
public void testPathParamExtLevel1WithParam() throws Exception {
pathParamExtensionTest("/testapp/blah;x=y/blah.txt", "none");
}
private void pathParamExtensionTest(String path, String expected)
throws Exception {
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("/testapp", null);
Tomcat.addServlet(ctx, "servlet", new PathParamServlet());
ctx.addServletMappingDecoded("*.txt", "servlet");
tomcat.start();
ByteChunk res = getUrl("http://localhost:" + getPort() + path);
Assert.assertEquals(expected, res.toString());
}
@Test
public void testBug54602a() throws Exception {
// No UTF-8
doTestUriDecoding("/foo", "UTF-8", "/foo");
}
@Test
public void testBug54602b() throws Exception {
// Valid UTF-8
doTestUriDecoding("/foo%c4%87", "UTF-8", "/foo\u0107");
}
@Test
public void testBug54602c() throws Exception {
// Partial UTF-8
doTestUriDecoding("/foo%c4", "UTF-8", "/foo\uFFFD");
}
@Test
public void testBug54602d() throws Exception {
// Invalid UTF-8
doTestUriDecoding("/foo%ff", "UTF-8", "/foo\uFFFD");
}
@Test
public void testBug54602e() throws Exception {
// Invalid UTF-8
doTestUriDecoding("/foo%ed%a0%80", "UTF-8", "/foo\uFFFD\uFFFD\uFFFD");
}
private void doTestUriDecoding(String path, String encoding,
String expectedPathInfo) throws Exception{
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
tomcat.getConnector().setURIEncoding(encoding);
// No file system docBase required
Context ctx = tomcat.addContext("", null);
PathInfoServlet servlet = new PathInfoServlet();
Tomcat.addServlet(ctx, "servlet", servlet);
ctx.addServletMappingDecoded("/*", "servlet");
tomcat.start();
int rc = getUrl("http://localhost:" + getPort() + path,
new ByteChunk(), null);
Assert.assertEquals(HttpServletResponse.SC_OK, rc);
Assert.assertEquals(expectedPathInfo, servlet.getPathInfo());
}
private static class PathInfoServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private volatile String pathInfo = null;
public String getPathInfo() {
return pathInfo;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// Not thread safe. Concurrent requests to this servlet will
// over-write all the results but the last processed.
pathInfo = req.getPathInfo();
}
}
@Test
public void testBug54928() throws Exception {
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("", null);
AsyncServlet servlet = new AsyncServlet();
Wrapper w = Tomcat.addServlet(ctx, "async", servlet);
w.setAsyncSupported(true);
ctx.addServletMappingDecoded("/async", "async");
tomcat.start();
SimpleHttpClient client = new SimpleHttpClient() {
@Override
public boolean isResponseBodyOK() {
return true;
}
};
String request = "GET /async HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: a" + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF;
client.setPort(getPort());
client.setRequest(new String[] {request});
client.connect();
client.sendRequest();
for (int i = 0; i < 10; i++) {
String line = client.readLine();
if (line != null && line.length() > 20) {
log.info(line.subSequence(0, 20) + "...");
}
}
client.disconnect();
// Wait for server thread to stop
Thread t = servlet.getThread();
long startTime = System.nanoTime();
t.join(5000);
long endTime = System.nanoTime();
log.info("Waited for servlet thread to stop for "
+ (endTime - startTime) / 1000000 + " ms");
Assert.assertTrue(servlet.isCompleted());
}
@Test
public void testNormalize01() {
doTestNormalize("/foo/../bar", "/bar");
}
private void doTestNormalize(String input, String expected) {
MessageBytes mb = MessageBytes.newInstance();
byte[] b = input.getBytes(StandardCharsets.UTF_8);
mb.setBytes(b, 0, b.length);
boolean result = CoyoteAdapter.normalize(mb);
mb.toString();
if (expected == null) {
Assert.assertFalse(result);
} else {
Assert.assertTrue(result);
Assert.assertEquals(expected, mb.toString());
}
}
private class AsyncServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
// This is a hack that won't work generally as servlets are expected to
// handle more than one request.
private Thread t;
private volatile boolean completed = false;
public Thread getThread() {
return t;
}
public boolean isCompleted() {
return completed;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/plain");
resp.setCharacterEncoding("UTF-8");
final OutputStream os = resp.getOutputStream();
final AsyncContext asyncCtxt = req.startAsync();
asyncCtxt.setTimeout(3000);
t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
// Some tests depend on this write failing (e.g.
// because the client has gone away). In some cases
// there may be a large (ish) buffer to fill before
// the write fails.
for (int j = 0 ; j < 8; j++) {
os.write(BYTES_8K);
}
os.flush();
Thread.sleep(1000);
} catch (Exception e) {
log.info("Exception caught " + e);
try {
// Note if request times out before this
// exception is thrown and the complete call
// below is made, the complete call below will
// fail since the timeout will have completed
// the request.
asyncCtxt.complete();
break;
} finally {
completed = true;
}
}
}
}
});
t.setName("testBug54928");
t.start();
}
}
}

View File

@@ -0,0 +1,161 @@
/*
* 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.catalina.connector;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import static org.apache.catalina.startup.SimpleHttpClient.CRLF;
import org.apache.catalina.Context;
import org.apache.catalina.servlets.DefaultServlet;
import org.apache.catalina.startup.SimpleHttpClient;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
import org.apache.tomcat.unittest.TesterData;
/*
* Various requests, usually originating from fuzzing, that have triggered an
* incorrect response from Tomcat - usually a 500 response rather than a 400
* response.
*/
@RunWith(Parameterized.class)
public class TestCoyoteAdapterRequestFuzzing extends TomcatBaseTest {
private static final String VALUE_16K = TesterData.string('x', 16 * 1024);
// Default max header count is 100
private static final String HEADER_150 = TesterData.string("X-Tomcat-Test: a" + CRLF, 150);
// Default max header count is 200 (need to keep under maxHeaderCount as well)
private static final String COOKIE_250 = TesterData.string("Cookie: a=b;c=d;e=f;g=h" + CRLF, 75);
@Parameterized.Parameters(name = "{index}: requestline[{0}], expected[{2}]")
public static Collection<Object[]> parameters() {
List<Object[]> parameterSets = new ArrayList<>();
parameterSets.add(new Object[] { "GET /00 HTTP/1.1",
"Host: lÿ#" + CRLF,
"400" } );
parameterSets.add(new Object[] { "GET *; HTTP/1.1",
"Host: localhost" + CRLF,
"400" } );
parameterSets.add(new Object[] { "GET /02 HTTP/1.1",
"Host: localhost" + CRLF +
"Content-Length: \u00A0" + CRLF,
"400" } );
parameterSets.add(new Object[] { "GET /03 HTTP/1.1",
"Content-Length: 1" + CRLF +
"Content-Length: 1" + CRLF,
"400" } );
parameterSets.add(new Object[] { "GET /04 HTTP/1.1",
"Transfer-Encoding: " + VALUE_16K + CRLF,
"400" } );
parameterSets.add(new Object[] { "GET /05 HTTP/1.1",
"Expect: " + VALUE_16K + CRLF,
"400" } );
parameterSets.add(new Object[] { "GET /06 HTTP/1.1",
"Connection: " + VALUE_16K + CRLF,
"400" } );
parameterSets.add(new Object[] { "GET /07 HTTP/1.1",
"User-Agent: " + VALUE_16K + CRLF,
"400" } );
parameterSets.add(new Object[] { "GET /08 HTTP/1.1",
HEADER_150,
"400" } );
parameterSets.add(new Object[] { "GET http://host/09 HTTP/1.0",
HEADER_150,
"400" } );
parameterSets.add(new Object[] { "GET /10 HTTP/1.1",
"Host: localhost" + CRLF +
COOKIE_250,
"400" } );
return parameterSets;
}
@Parameter(0)
public String requestLine;
@Parameter(1)
public String headers;
@Parameter(2)
public String expected;
@Test
public void doTest() throws Exception {
Tomcat tomcat = getTomcatInstance();
tomcat.getConnector().setAttribute("restrictedUserAgents", "value-not-important");
File appDir = new File("test/webapp");
Context ctxt = tomcat.addContext("", appDir.getAbsolutePath());
Tomcat.addServlet(ctxt, "default", DefaultServlet.class.getName());
ctxt.addServletMappingDecoded("/", "default");
tomcat.start();
Client client = new Client(tomcat.getConnector().getLocalPort());
client.setRequest(new String[] {requestLine + CRLF, headers + CRLF});
client.connect();
client.processRequest();
// Expected response
String line = client.getResponseLine();
Assert.assertTrue(line + CRLF + client.getResponseBody(), line.startsWith("HTTP/1.1 " + expected + " "));
}
private static final class Client extends SimpleHttpClient {
public Client(int port) {
setPort(port);
setRequestPause(0);
}
@Override
protected OutputStream createOutputStream(Socket socket) throws IOException {
// Override the default implementation so we can create a large
// enough buffer to hold the entire request.
// The default implementation uses the 8k buffer in the
// StreamEncoder. Since some requests are larger than this, those
// requests will be sent in several parts. If the first part is
// sufficient for Tomcat to determine the request is invalid, Tomcat
// will close the connection, causing the write of the remaining
// parts to fail which in turn causes the test to fail.
return new BufferedOutputStream(super.createOutputStream(socket), 32 * 1024);
}
@Override
public boolean isResponseBodyOK() {
// Response body varies. It is the response code that is of interest
// in these tests.
return true;
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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.catalina.connector;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
import org.apache.tomcat.util.buf.ByteChunk;
public class TestCoyoteInputStream extends TomcatBaseTest {
@Test
public void testReadWithByteBuffer() throws Exception {
Tomcat tomcat = getTomcatInstance();
Context root = tomcat.addContext("", TEMP_DIR);
Tomcat.addServlet(root, "testServlet", new TestServlet());
root.addServletMappingDecoded("/", "testServlet");
tomcat.start();
ByteChunk bc = new ByteChunk();
String requestBody = "HelloWorld";
int rc = postUrl(requestBody.getBytes(StandardCharsets.UTF_8),
"http://localhost:" + getPort() + "/", bc, null);
Assert.assertEquals(HttpServletResponse.SC_OK, rc);
Assert.assertTrue(requestBody.equals(bc.toString()));
}
private static final class TestServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
CoyoteInputStream is = (CoyoteInputStream) req.getInputStream();
ByteBuffer buffer = ByteBuffer.allocate(256);
is.read(buffer);
CoyoteOutputStream os = (CoyoteOutputStream) resp.getOutputStream();
os.write(buffer);
}
}
}

View File

@@ -0,0 +1,293 @@
/*
* 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.catalina.connector;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel.MapMode;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
import org.apache.tomcat.util.buf.ByteChunk;
public class TestCoyoteOutputStream extends TomcatBaseTest {
@Test
public void testNonBlockingWriteNoneBlockingWriteNoneContainerThread() throws Exception {
doNonBlockingTest(0, 0, true);
}
@Test
public void testNonBlockingWriteOnceBlockingWriteNoneContainerThread() throws Exception {
doNonBlockingTest(1, 0, true);
}
@Test
public void testNonBlockingWriteTwiceBlockingWriteNoneContainerThread() throws Exception {
doNonBlockingTest(2, 0, true);
}
@Test
public void testNonBlockingWriteNoneBlockingWriteOnceContainerThread() throws Exception {
doNonBlockingTest(0, 1, true);
}
@Test
public void testNonBlockingWriteOnceBlockingWriteOnceContainerThread() throws Exception {
doNonBlockingTest(1, 1, true);
}
@Test
public void testNonBlockingWriteTwiceBlockingWriteOnceContainerThread() throws Exception {
doNonBlockingTest(2, 1, true);
}
@Test
public void testNonBlockingWriteNoneBlockingWriteNoneNonContainerThread() throws Exception {
doNonBlockingTest(0, 0, false);
}
@Test
public void testNonBlockingWriteOnceBlockingWriteNoneNonContainerThread() throws Exception {
doNonBlockingTest(1, 0, false);
}
@Test
public void testNonBlockingWriteTwiceBlockingWriteNoneNonContainerThread() throws Exception {
doNonBlockingTest(2, 0, false);
}
@Test
public void testNonBlockingWriteNoneBlockingWriteOnceNonContainerThread() throws Exception {
doNonBlockingTest(0, 1, false);
}
@Test
public void testNonBlockingWriteOnceBlockingWriteOnceNonContainerThread() throws Exception {
doNonBlockingTest(1, 1, false);
}
@Test
public void testNonBlockingWriteTwiceBlockingWriteOnceNonContainerThread() throws Exception {
doNonBlockingTest(2, 1, false);
}
@Test
public void testWriteWithByteBuffer() throws Exception {
Tomcat tomcat = getTomcatInstance();
Context root = tomcat.addContext("", TEMP_DIR);
Tomcat.addServlet(root, "testServlet", new TestServlet());
root.addServletMappingDecoded("/", "testServlet");
tomcat.start();
ByteChunk bc = new ByteChunk();
int rc = getUrl("http://localhost:" + getPort() + "/", bc, null, null);
Assert.assertEquals(HttpServletResponse.SC_OK, rc);
File file = new File("test/org/apache/catalina/connector/test_content.txt");
try (RandomAccessFile raf = new RandomAccessFile(file, "r");) {
ByteChunk expected = new ByteChunk();
expected.append(raf.getChannel().map(MapMode.READ_ONLY, 0, file.length()));
Assert.assertTrue(expected.equals(bc));
}
}
private void doNonBlockingTest(int asyncWriteTarget, int syncWriteTarget,
boolean useContainerThreadToSetListener) throws Exception {
Tomcat tomcat = getTomcatInstance();
Context root = tomcat.addContext("", TEMP_DIR);
Wrapper w = Tomcat.addServlet(root, "nbWrite",
new NonBlockingWriteServlet(asyncWriteTarget, useContainerThreadToSetListener));
w.setAsyncSupported(true);
root.addServletMappingDecoded("/nbWrite", "nbWrite");
Tomcat.addServlet(root, "write",
new BlockingWriteServlet(asyncWriteTarget, syncWriteTarget));
w.setAsyncSupported(true);
root.addServletMappingDecoded("/write", "write");
tomcat.start();
ByteChunk bc = new ByteChunk();
// Extend timeout to 5 mins for debugging
int rc = getUrl("http://localhost:" + getPort() + "/nbWrite", bc,
300000, null, null);
int totalCount = asyncWriteTarget + syncWriteTarget;
StringBuilder sb = new StringBuilder(totalCount * 16);
for (int i = 0; i < totalCount; i++) {
sb.append("OK - " + i + System.lineSeparator());
}
String expected = null;
if (sb.length() > 0) {
expected = sb.toString();
}
Assert.assertEquals(200, rc);
Assert.assertEquals(expected, bc.toString());
}
private static final class NonBlockingWriteServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final int asyncWriteTarget;
private final AtomicInteger asyncWriteCount = new AtomicInteger(0);
private final boolean useContainerThreadToSetListener;
public NonBlockingWriteServlet(int asyncWriteTarget,
boolean useContainerThreadToSetListener) {
this.asyncWriteTarget = asyncWriteTarget;
this.useContainerThreadToSetListener = useContainerThreadToSetListener;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/plain");
resp.setCharacterEncoding("UTF-8");
ServletOutputStream sos = resp.getOutputStream();
AsyncContext asyncCtxt = req.startAsync();
asyncCtxt.setTimeout(5);
Runnable task = new AsyncTask(asyncCtxt, sos);
if (useContainerThreadToSetListener) {
asyncCtxt.start(task);
} else {
Thread t = new Thread(task);
t.start();
}
}
private void doAsyncWrite(AsyncContext asyncCtxt,
ServletOutputStream sos) throws IOException {
while (sos.isReady()) {
int next = asyncWriteCount.getAndIncrement();
if (next < asyncWriteTarget) {
sos.write(
("OK - " + next + System.lineSeparator()).getBytes(
StandardCharsets.UTF_8));
sos.flush();
} else {
asyncCtxt.dispatch("/write");
break;
}
}
}
private class AsyncTask implements Runnable {
private final AsyncContext asyncCtxt;
private final ServletOutputStream sos;
public AsyncTask(AsyncContext asyncCtxt, ServletOutputStream sos) {
this.asyncCtxt = asyncCtxt;
this.sos = sos;
}
@Override
public void run() {
sos.setWriteListener(new MyWriteListener(asyncCtxt, sos));
}
}
private final class MyWriteListener implements WriteListener {
private final AsyncContext asyncCtxt;
private final ServletOutputStream sos;
public MyWriteListener(AsyncContext asyncCtxt,
ServletOutputStream sos) {
this.asyncCtxt = asyncCtxt;
this.sos = sos;
}
@Override
public void onWritePossible() throws IOException {
doAsyncWrite(asyncCtxt, sos);
}
@Override
public void onError(Throwable throwable) {
// Not expected.
throwable.printStackTrace();
}
}
}
private static final class BlockingWriteServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final int start;
private final int len;
public BlockingWriteServlet(int start, int len) {
this.start = start;
this.len = len;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/plain");
resp.setCharacterEncoding("UTF-8");
ServletOutputStream sos = resp.getOutputStream();
for (int i = start; i < start + len; i++) {
sos.write(("OK - " + i + System.lineSeparator()).getBytes(
StandardCharsets.UTF_8));
}
}
}
private static final class TestServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
CoyoteOutputStream os = (CoyoteOutputStream) resp.getOutputStream();
File file = new File("test/org/apache/catalina/connector/test_content.txt");
try (RandomAccessFile raf = new RandomAccessFile(file, "r");) {
os.write(raf.getChannel().map(MapMode.READ_ONLY, 0, file.length()));
}
}
}
}

View File

@@ -0,0 +1,160 @@
/*
* 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.catalina.connector;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.MalformedInputException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.buf.TestUtf8;
import org.apache.tomcat.util.buf.TestUtf8.Utf8TestCase;
public class TestInputBuffer extends TomcatBaseTest {
@Test
public void testUtf8Body() throws Exception {
Tomcat tomcat = getTomcatInstance();
Context root = tomcat.addContext("", TEMP_DIR);
Tomcat.addServlet(root, "Echo", new Utf8Echo());
root.addServletMappingDecoded("/test", "Echo");
tomcat.start();
for (Utf8TestCase testCase : TestUtf8.TEST_CASES) {
String expected = null;
if (testCase.invalidIndex == -1) {
expected = testCase.outputReplaced;
}
doUtf8BodyTest(testCase.description, testCase.input, expected);
}
}
@Test
public void testBug60400() throws Exception {
Tomcat tomcat = getTomcatInstance();
Context root = tomcat.addContext("", TEMP_DIR);
Tomcat.addServlet(root, "Bug60400Servlet", new Bug60400Servlet());
root.addServletMappingDecoded("/", "Bug60400Servlet");
Assert.assertTrue(tomcat.getConnector().setProperty("socket.appReadBufSize", "9000"));
tomcat.start();
ByteChunk bc = new ByteChunk();
byte[] requestBody = new byte[9500];
Arrays.fill(requestBody, (byte) 1);
int rc = postUrl(requestBody, "http://localhost:" + getPort() + "/", bc, null);
Assert.assertEquals(HttpServletResponse.SC_OK, rc);
Assert.assertEquals(requestBody.length, bc.getLength());
}
private void doUtf8BodyTest(String description, int[] input,
String expected) throws Exception {
byte[] bytes = new byte[input.length];
for (int i = 0; i < input.length; i++) {
bytes[i] = (byte) input[i];
}
ByteChunk bc = new ByteChunk();
int rc = postUrl(bytes, "http://localhost:" + getPort() + "/test", bc,
null);
if (expected == null) {
Assert.assertEquals(description, HttpServletResponse.SC_OK, rc);
Assert.assertEquals(description, "FAILED", bc.toString());
} else if (expected.length() == 0) {
Assert.assertNull(description, bc.toString());
} else {
bc.setCharset(StandardCharsets.UTF_8);
Assert.assertEquals(description, expected, bc.toString());
}
}
private static class Utf8Echo extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// Should use POST
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
Reader r = req.getReader();
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/plain");
Writer w = resp.getWriter();
try {
// Copy one character at a time
int c = r.read();
while (c != -1) {
w.write(c);
c = r.read();
}
w.close();
} catch (MalformedInputException mie) {
resp.resetBuffer();
w.write("FAILED");
}
}
}
private static class Bug60400Servlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
StringBuilder builder = new StringBuilder();
try (BufferedReader reader = req.getReader()) {
String line;
while ((line = reader.readLine()) != null) {
builder.append(line);
}
}
resp.getWriter().print(builder);
}
}
}

View File

@@ -0,0 +1,141 @@
/*
* 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.catalina.connector;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
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 TestKeepAliveCount extends TomcatBaseTest {
@Test
public void testHttp10() throws Exception {
TestKeepAliveClient client = new TestKeepAliveClient();
client.doHttp10Request();
}
@Test
public void testHttp11() throws Exception {
TestKeepAliveClient client = new TestKeepAliveClient();
client.doHttp11Request();
}
private class TestKeepAliveClient extends SimpleHttpClient {
private boolean init;
private synchronized void init() {
if (init) return;
Tomcat tomcat = getTomcatInstance();
Context root = tomcat.addContext("", TEMP_DIR);
Tomcat.addServlet(root, "Simple", new SimpleServlet());
root.addServletMappingDecoded("/test", "Simple");
Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "5"));
Assert.assertTrue(tomcat.getConnector().setProperty("connectionTimeout", "20000"));
Assert.assertTrue(tomcat.getConnector().setProperty("keepAliveTimeout", "50000"));
init = true;
}
private void doHttp10Request() throws Exception {
Tomcat tomcat = getTomcatInstance();
init();
tomcat.start();
setPort(tomcat.getConnector().getLocalPort());
// Open connection
connect();
// Send request in two parts
String[] request = new String[1];
request[0] =
"GET /test HTTP/1.0" + CRLF + CRLF;
setRequest(request);
processRequest(false); // blocks until response has been read
boolean passed = (this.readLine()==null);
// Close the connection
disconnect();
reset();
tomcat.stop();
Assert.assertTrue(passed);
}
private void doHttp11Request() throws Exception {
Tomcat tomcat = getTomcatInstance();
init();
tomcat.start();
setPort(tomcat.getConnector().getLocalPort());
// Open connection
connect();
// Send request in two parts
String[] request = new String[1];
request[0] =
"GET /test HTTP/1.1" + CRLF +
"Host: localhost" + CRLF +
"Connection: Keep-Alive" + CRLF+
"Keep-Alive: 300"+ CRLF+ CRLF;
setRequest(request);
for (int i=0; i<5; i++) {
processRequest(false); // blocks until response has been read
Assert.assertTrue(getResponseLine()!=null && getResponseLine().startsWith("HTTP/1.1 200 "));
}
boolean passed = (this.readLine()==null);
// Close the connection
disconnect();
reset();
tomcat.stop();
Assert.assertTrue(passed);
}
@Override
public boolean isResponseBodyOK() {
return true;
}
}
private static class SimpleServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentLength(0);
resp.flushBuffer();
}
}
}

View File

@@ -0,0 +1,162 @@
/*
* 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.catalina.connector;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Test;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.startup.SimpleHttpClient;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
public class TestMaxConnections extends TomcatBaseTest {
private static final int MAX_CONNECTIONS = 3;
public static final int soTimeout = 5000;
public static final int connectTimeout = 1000;
@Test
public void testConnector() throws Exception {
init();
Assume.assumeFalse("This feature is not available for NIO2 (BZ58103)",
getTomcatInstance().getConnector().getProtocolHandlerClassName().contains("Nio2"));
ConnectThread[] t = new ConnectThread[10];
for (int i=0; i<t.length; i++) {
t[i] = new ConnectThread();
t[i].setName("ConnectThread["+i+"]");
}
for (int i=0; i<t.length; i++) {
t[i].start();
Thread.sleep(50);
}
for (int i=0; i<t.length; i++) {
t[i].join();
}
Assert.assertEquals(MAX_CONNECTIONS, SimpleServlet.getMaxConnections());
}
private class ConnectThread extends Thread {
@Override
public void run() {
try {
TestClient client = new TestClient();
client.doHttp10Request();
} catch (Exception x) {
// NO-OP. Some connections are expected to fail.
}
}
}
private synchronized void init() throws Exception {
Tomcat tomcat = getTomcatInstance();
StandardContext root = (StandardContext) tomcat.addContext("", SimpleHttpClient.TEMP_DIR);
root.setUnloadDelay(soTimeout);
Tomcat.addServlet(root, "Simple", new SimpleServlet());
root.addServletMappingDecoded("/test", "Simple");
Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1"));
Assert.assertTrue(tomcat.getConnector().setProperty("maxThreads", "10"));
Assert.assertTrue(tomcat.getConnector().setProperty("connectionTimeout", "20000"));
Assert.assertTrue(tomcat.getConnector().setProperty("keepAliveTimeout", "50000"));
Assert.assertTrue(tomcat.getConnector().setProperty("maxConnections", Integer.toString(MAX_CONNECTIONS)));
Assert.assertTrue(tomcat.getConnector().setProperty("acceptCount", "1"));
tomcat.start();
}
private class TestClient extends SimpleHttpClient {
private void doHttp10Request() throws Exception {
setPort(getPort());
long start = System.currentTimeMillis();
// Open connection
connect(connectTimeout,soTimeout);
// Send request in two parts
String[] request = new String[1];
request[0] =
"GET /test HTTP/1.0" + CRLF + CRLF;
setRequest(request);
boolean passed = false;
processRequest(false); // blocks until response has been read
long stop = System.currentTimeMillis();
log.info(Thread.currentThread().getName()+" Request complete:"+(stop-start)+" ms.");
passed = (this.readLine()==null);
// Close the connection
disconnect();
reset();
Assert.assertTrue(passed);
}
@Override
public boolean isResponseBodyOK() {
return true;
}
}
private static class SimpleServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static int currentConnections = 0;
private static int maxConnections = 0;
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
increment();
System.out.println("Processing thread: " + Thread.currentThread().getName());
try {
Thread.sleep(TestMaxConnections.soTimeout*4/5);
} catch (InterruptedException x) {
}
resp.setContentLength(0);
resp.flushBuffer();
decrement();
}
private static synchronized void increment() {
currentConnections++;
if (currentConnections > maxConnections) {
maxConnections = currentConnections;
}
}
private static synchronized void decrement() {
currentConnections--;
}
public static synchronized int getMaxConnections() {
return maxConnections;
}
}
}

View File

@@ -0,0 +1,214 @@
/*
* 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.catalina.connector;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
import org.apache.tomcat.util.buf.ByteChunk;
public class TestOutputBuffer extends TomcatBaseTest{
/*
* Expect that the buffered results are slightly slower since Tomcat now has
* an internal buffer so an extra one just adds overhead.
*
* @see "https://bz.apache.org/bugzilla/show_bug.cgi?id=52328"
*/
@Test
public void testWriteSpeed() throws Exception {
Tomcat tomcat = getTomcatInstance();
Context root = tomcat.addContext("", TEMP_DIR);
for (int i = 1; i <= WritingServlet.EXPECTED_CONTENT_LENGTH; i*=10) {
WritingServlet servlet = new WritingServlet(i);
Tomcat.addServlet(root, "servlet" + i, servlet);
root.addServletMappingDecoded("/servlet" + i, "servlet" + i);
}
tomcat.start();
ByteChunk bc = new ByteChunk();
for (int i = 1; i <= WritingServlet.EXPECTED_CONTENT_LENGTH; i*=10) {
int rc = getUrl("http://localhost:" + getPort() +
"/servlet" + i, bc, null, null);
Assert.assertEquals(HttpServletResponse.SC_OK, rc);
Assert.assertEquals(
WritingServlet.EXPECTED_CONTENT_LENGTH, bc.getLength());
bc.recycle();
rc = getUrl("http://localhost:" + getPort() +
"/servlet" + i + "?useBuffer=y", bc, null, null);
Assert.assertEquals(HttpServletResponse.SC_OK, rc);
Assert.assertEquals(
WritingServlet.EXPECTED_CONTENT_LENGTH, bc.getLength());
bc.recycle();
}
}
@Test
public void testBug52577() throws Exception {
Tomcat tomcat = getTomcatInstance();
Context root = tomcat.addContext("", TEMP_DIR);
Bug52577Servlet bug52577 = new Bug52577Servlet();
Tomcat.addServlet(root, "bug52577", bug52577);
root.addServletMappingDecoded("/", "bug52577");
tomcat.start();
ByteChunk bc = new ByteChunk();
int rc = getUrl("http://localhost:" + getPort() + "/", bc, null, null);
Assert.assertEquals(HttpServletResponse.SC_OK, rc);
Assert.assertEquals("OK", bc.toString());
}
private static class WritingServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected static final int EXPECTED_CONTENT_LENGTH = 100000;
private final String writeString;
private final int writeCount;
public WritingServlet(int writeLength) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < writeLength; i++) {
sb.append('x');
}
writeString = sb.toString();
writeCount = EXPECTED_CONTENT_LENGTH / writeLength;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/plain");
resp.setCharacterEncoding("ISO-8859-1");
Writer w = resp.getWriter();
// Wrap with a buffer if necessary
String useBufferStr = req.getParameter("useBuffer");
if (useBufferStr != null) {
w = new BufferedWriter(w);
}
long start = System.nanoTime();
for (int i = 0; i < writeCount; i++) {
w.write(writeString);
}
if (useBufferStr != null) {
w.flush();
}
long lastRunNano = System.nanoTime() - start;
System.out.println("Write length: " + writeString.length() +
", Buffered: " + (useBufferStr == null ? "n" : "y") +
", Time: " + lastRunNano + "ns");
}
}
private static class Bug52577Servlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
Writer w = resp.getWriter();
w.write("OK");
resp.resetBuffer();
w.write("OK");
}
}
@Test
public void testUtf8SurrogateBody() throws Exception {
// Create test data. This is carefully constructed to trigger the edge
// case. Small variations may cause the test to miss the edge case.
StringBuffer sb = new StringBuffer();
sb.append("a");
for (int i = 0x10000; i < 0x11000; i++) {
char[] chars = Character.toChars(i);
sb.append(chars);
}
String data = sb.toString();
Tomcat tomcat = getTomcatInstance();
Context root = tomcat.addContext("", TEMP_DIR);
Tomcat.addServlet(root, "Test", new Utf8WriteChars(data));
root.addServletMappingDecoded("/test", "Test");
tomcat.start();
ByteChunk bc = new ByteChunk();
getUrl("http://localhost:" + getPort() + "/test", bc, null);
bc.setCharset(StandardCharsets.UTF_8);
Assert.assertEquals(data, bc.toString());
}
private static class Utf8WriteChars extends HttpServlet {
private static final long serialVersionUID = 1L;
private final char[] chars;
public Utf8WriteChars(String data) {
chars = data.toCharArray();
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/plain");
Writer w = resp.getWriter();
for (int i = 0; i < chars.length; i++) {
w.write(chars[i]);
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,83 @@
/*
* 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.catalina.connector;
import java.net.URI;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.startup.LoggingBaseTest;
import org.apache.tomcat.unittest.TesterRequest;
public class TestResponsePerformance extends LoggingBaseTest {
private final int ITERATIONS = 100000;
@Test
public void testToAbsolutePerformance() throws Exception {
Request req = new TesterRequest();
Response resp = new Response();
resp.setRequest(req);
// Warm up
doHomebrew(resp);
doUri();
// Note: Java 9 on my OSX laptop consistently shows doUri() is faster
// than doHomebrew(). Worth a closer look for Tomcat 10 on the
// assumption it will require java 9
// To allow for timing differences between runs, a "best of n" approach
// is taken for this test
final int bestOf = 5;
final int winTarget = (bestOf + 1) / 2;
int homebrewWin = 0;
int count = 0;
while (count < bestOf && homebrewWin < winTarget) {
long homebrew = doHomebrew(resp);
long uri = doUri();
log.info("Current 'home-brew': " + homebrew + "ms, Using URI: " + uri + "ms");
if (homebrew < uri) {
homebrewWin++;
}
count++;
}
Assert.assertTrue(homebrewWin == winTarget);
}
private long doHomebrew(Response resp) {
long start = System.currentTimeMillis();
for (int i = 0; i < ITERATIONS; i++) {
resp.toAbsolute("bar.html");
}
return System.currentTimeMillis() - start;
}
private long doUri() {
long start = System.currentTimeMillis();
for (int i = 0; i < ITERATIONS; i++) {
URI base = URI.create(
"http://localhost:8080/level1/level2/foo.html");
base.resolve(URI.create("bar.html")).toASCIIString();
}
return System.currentTimeMillis() - start;
}
}

View File

@@ -0,0 +1,251 @@
/*
* 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.catalina.connector;
import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.Globals;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
import org.apache.tomcat.util.buf.ByteChunk;
public class TestSendFile extends TomcatBaseTest {
public static final int ITERATIONS = 10;
public static final int EXPECTED_CONTENT_LENGTH = 100000;
@Test
public void testSendFile() throws Exception {
Tomcat tomcat = getTomcatInstance();
Context root = tomcat.addContext("", TEMP_DIR);
File[] files = new File[ITERATIONS];
for (int i = 0; i < ITERATIONS; i++) {
files[i] = generateFile(TEMP_DIR, "-" + i, EXPECTED_CONTENT_LENGTH * (i + 1));
}
try {
for (int i = 0; i < ITERATIONS; i++) {
WritingServlet servlet = new WritingServlet(files[i]);
Tomcat.addServlet(root, "servlet" + i, servlet);
root.addServletMappingDecoded("/servlet" + i, "servlet" + i);
}
tomcat.start();
ByteChunk bc = new ByteChunk();
Map<String, List<String>> respHeaders = new HashMap<>();
for (int i = 0; i < ITERATIONS; i++) {
long start = System.currentTimeMillis();
int rc = getUrl("http://localhost:" + getPort() + "/servlet" + i, bc, null,
respHeaders);
Assert.assertEquals(HttpServletResponse.SC_OK, rc);
System.out.println("Client received " + bc.getLength() + " bytes in "
+ (System.currentTimeMillis() - start) + " ms.");
Assert.assertEquals(EXPECTED_CONTENT_LENGTH * (i + 1L), bc.getLength());
bc.recycle();
}
} finally {
for (File f : files) {
Assert.assertTrue("Failed to clean up [" + f + "]", f.delete());
}
}
}
public File generateFile(String dir, String suffix, int size) throws IOException {
String name = "testSendFile-" + System.currentTimeMillis() + suffix + ".txt";
File f = new File(dir, name);
try (FileWriter fw = new FileWriter(f, false); BufferedWriter w = new BufferedWriter(fw);) {
int defSize = 8192;
while (size > 0) {
int bytes = Math.min(size, defSize);
char[] b = new char[bytes];
Arrays.fill(b, 'X');
w.write(b);
size = size - bytes;
}
w.flush();
}
System.out
.println("Created file:" + f.getAbsolutePath() + " with " + f.length() + " bytes.");
return f;
}
private static class WritingServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final File f;
public WritingServlet(File f) {
this.f = f;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("'application/octet-stream");
resp.setCharacterEncoding("ISO-8859-1");
resp.setContentLengthLong(f.length());
if (Boolean.TRUE.equals(req.getAttribute(Globals.SENDFILE_SUPPORTED_ATTR))) {
req.setAttribute(Globals.SENDFILE_FILENAME_ATTR, f.getAbsolutePath());
req.setAttribute(Globals.SENDFILE_FILE_START_ATTR, Long.valueOf(0));
req.setAttribute(Globals.SENDFILE_FILE_END_ATTR, Long.valueOf(f.length()));
} else {
byte[] c = new byte[8192];
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(f))) {
int len = 0;
int written = 0;
long start = System.currentTimeMillis();
do {
len = in.read(c);
if (len > 0) {
resp.getOutputStream().write(c, 0, len);
written += len;
}
} while (len > 0);
System.out.println("Server Wrote " + written + " bytes in "
+ (System.currentTimeMillis() - start) + " ms.");
}
}
}
}
@Test
public void testBug60409() throws Exception {
Tomcat tomcat = getTomcatInstance();
Context ctx = tomcat.addContext("", TEMP_DIR);
File file = generateFile(TEMP_DIR, "", EXPECTED_CONTENT_LENGTH);
Tomcat.addServlet(ctx, "test", new Bug60409Servlet(file));
ctx.addServletMappingDecoded("/", "test");
tomcat.start();
ByteChunk bc = new ByteChunk();
getUrl("http://localhost:" + getPort() + "/test/?" + Globals.SENDFILE_SUPPORTED_ATTR
+ "=true", bc, null);
CountDownLatch latch = new CountDownLatch(2);
List<Throwable> exceptions = new ArrayList<>();
new Thread(
new RequestExecutor("http://localhost:" + getPort() + "/test/", latch, exceptions))
.start();
new Thread(
new RequestExecutor("http://localhost:" + getPort() + "/test/", latch, exceptions))
.start();
latch.await(3000, TimeUnit.MILLISECONDS);
if (exceptions.size() > 0) {
Assert.fail();
}
}
private static final class Bug60409Servlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final File file;
Bug60409Servlet(File file) {
this.file = file;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
if (Boolean.valueOf(req.getParameter(Globals.SENDFILE_SUPPORTED_ATTR)).booleanValue()) {
resp.setContentType("'application/octet-stream");
resp.setCharacterEncoding("ISO-8859-1");
resp.setContentLengthLong(file.length());
req.setAttribute(Globals.SENDFILE_FILENAME_ATTR, file.getAbsolutePath());
req.setAttribute(Globals.SENDFILE_FILE_START_ATTR, Long.valueOf(0));
req.setAttribute(Globals.SENDFILE_FILE_END_ATTR, Long.valueOf(file.length()));
if (!file.delete()) {
throw new ServletException("Failed to delete [" + file + "]");
}
} else {
byte[] c = new byte[1024];
Random rd = new Random();
rd.nextBytes(c);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
resp.getOutputStream().write(c);
}
}
}
private static final class RequestExecutor implements Runnable {
private final String url;
private final CountDownLatch latch;
private final List<Throwable> exceptions;
RequestExecutor(String url, CountDownLatch latch, List<Throwable> exceptions) {
this.url = url;
this.latch = latch;
this.exceptions = exceptions;
}
@Override
public void run() {
try {
ByteChunk result = new ByteChunk();
int rc = getUrl(url, result, null);
Assert.assertEquals(HttpServletResponse.SC_OK, rc);
Assert.assertEquals(1024, result.getLength());
} catch (Throwable e) {
e.printStackTrace();
exceptions.add(e);
} finally {
latch.countDown();
}
}
}
}

View File

@@ -0,0 +1,19 @@
# 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.
# This is a test file for the WebappServiceLoader
# It contains comment lines and blank lines
test content