/*
* 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();
}
}
}