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