Author: markt Date: Wed Jul 27 15:47:44 2016 New Revision: 1754281 URL: http://svn.apache.org/viewvc?rev=1754281&view=rev Log: Decode the path provided to the request dispatcher by default. The reasoning is: - the servlet spec is clear (see 9.1.1) that path can include a query string; - the query string is delimited by '?'; - '?' is a valid character in the path (if it is encoded); - users may want to use '?'in the path; - therefore users need to be able to provide an encoded path; - therefore the RequestDispatcher needs to decode the path.
This change should be transparent unless an application is passing an unencoded % to the request dispatcher which should be fairly rare. The behaviour is configurable via a Context attribute so the previous behaviour can easily be restored. Added: tomcat/trunk/test/org/apache/catalina/core/TestApplicationContextGetRequestDispatcher.java (with props) Modified: tomcat/trunk/java/org/apache/catalina/Context.java tomcat/trunk/java/org/apache/catalina/connector/Request.java tomcat/trunk/java/org/apache/catalina/core/ApplicationContext.java tomcat/trunk/java/org/apache/catalina/core/LocalStrings.properties tomcat/trunk/java/org/apache/catalina/core/StandardContext.java tomcat/trunk/java/org/apache/catalina/startup/FailedContext.java tomcat/trunk/test/org/apache/catalina/core/TestAsyncContextImpl.java tomcat/trunk/test/org/apache/tomcat/unittest/TesterContext.java tomcat/trunk/webapps/docs/changelog.xml tomcat/trunk/webapps/docs/config/context.xml Modified: tomcat/trunk/java/org/apache/catalina/Context.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/Context.java?rev=1754281&r1=1754280&r2=1754281&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/catalina/Context.java (original) +++ tomcat/trunk/java/org/apache/catalina/Context.java Wed Jul 27 15:47:44 2016 @@ -1766,4 +1766,26 @@ public interface Context extends Contain * @see #setUseRelativeRedirects(boolean) */ public boolean getUseRelativeRedirects(); + + /** + * Are paths used in calls to obtain a request dispatcher expected to be + * encoded? This affects both how Tomcat handles calls to obtain a request + * dispatcher as well as how Tomcat generates paths used to obtain request + * dispatchers internally. + * + * @param dispatchersUseEncodedPaths {@code true} to use encoded paths, + * otherwise {@code false} + */ + public void setDispatchersUseEncodedPaths(boolean dispatchersUseEncodedPaths); + + /** + * Are paths used in calls to obtain a request dispatcher expected to be + * encoded? This applys to both how Tomcat handles calls to obtain a request + * dispatcher as well as how Tomcat generates paths used to obtain request + * dispatchers internally. + * + * @return {@code true} if encoded paths will be used, otherwise + * {@code false} + */ + public boolean getDispatchersUseEncodedPaths(); } Modified: tomcat/trunk/java/org/apache/catalina/connector/Request.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/connector/Request.java?rev=1754281&r1=1754280&r2=1754281&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/catalina/connector/Request.java (original) +++ tomcat/trunk/java/org/apache/catalina/connector/Request.java Wed Jul 27 15:47:44 2016 @@ -83,6 +83,7 @@ import org.apache.catalina.core.Applicat import org.apache.catalina.core.AsyncContextImpl; import org.apache.catalina.mapper.MappingData; import org.apache.catalina.util.ParameterMap; +import org.apache.catalina.util.URLEncoder; import org.apache.coyote.ActionCode; import org.apache.coyote.UpgradeToken; import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler; @@ -1372,14 +1373,22 @@ public class Request implements HttpServ int pos = requestPath.lastIndexOf('/'); String relative = null; - if (pos >= 0) { - relative = requestPath.substring(0, pos + 1) + path; + if (context.getDispatchersUseEncodedPaths()) { + if (pos >= 0) { + relative = URLEncoder.DEFAULT.encode( + requestPath.substring(0, pos + 1), "UTF-8") + path; + } else { + relative = URLEncoder.DEFAULT.encode(requestPath, "UTF-8") + path; + } } else { - relative = requestPath + path; + if (pos >= 0) { + relative = requestPath.substring(0, pos + 1) + path; + } else { + relative = requestPath + path; + } } return context.getServletContext().getRequestDispatcher(relative); - } Modified: tomcat/trunk/java/org/apache/catalina/core/ApplicationContext.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/core/ApplicationContext.java?rev=1754281&r1=1754280&r2=1754281&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/catalina/core/ApplicationContext.java (original) +++ tomcat/trunk/java/org/apache/catalina/core/ApplicationContext.java Wed Jul 27 15:47:44 2016 @@ -17,9 +17,11 @@ package org.apache.catalina.core; import java.io.InputStream; +import java.io.UnsupportedEncodingException; import java.lang.reflect.InvocationTargetException; import java.net.MalformedURLException; import java.net.URL; +import java.net.URLDecoder; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; @@ -410,6 +412,26 @@ public class ApplicationContext implemen if (normalizedPath == null) return (null); + if (getContext().getDispatchersUseEncodedPaths()) { + // Decode + String decodedPath; + try { + decodedPath = URLDecoder.decode(normalizedPath, "UTF-8"); + } catch (UnsupportedEncodingException e) { + // Impossible + return null; + } + + // Security check to catch attempts to encode /../ sequences + normalizedPath = RequestUtil.normalize(decodedPath); + if (!decodedPath.equals(normalizedPath)) { + getContext().getLogger().warn( + sm.getString("applicationContext.illegalDispatchPath", path), + new IllegalArgumentException()); + return null; + } + } + pos = normalizedPath.length(); // Use the thread local URI and mapping data Modified: tomcat/trunk/java/org/apache/catalina/core/LocalStrings.properties URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/core/LocalStrings.properties?rev=1754281&r1=1754280&r2=1754281&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/catalina/core/LocalStrings.properties (original) +++ tomcat/trunk/java/org/apache/catalina/core/LocalStrings.properties Wed Jul 27 15:47:44 2016 @@ -22,6 +22,7 @@ applicationContext.addListener.ise=Liste applicationContext.addRole.ise=Roles cannot be added to context {0} as the context has been initialised applicationContext.addServlet.ise=Servlets cannot be added to context {0} as the context has been initialised applicationContext.attributeEvent=Exception thrown by attributes event listener +applicationContext.illegalDispatchPath=An application attempted to obtain a request dispatcher with an illegal path [{0}] that was rejected because it contained an encoded directory traversal attempt applicationContext.invalidFilterName=Unable to add filter definition due to invalid filter name [{0}]. applicationContext.invalidServletName=Unable to add servlet definition due to invalid servlet name [{0}]. applicationContext.mapping.error=Error during mapping Modified: tomcat/trunk/java/org/apache/catalina/core/StandardContext.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/core/StandardContext.java?rev=1754281&r1=1754280&r2=1754281&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/catalina/core/StandardContext.java (original) +++ tomcat/trunk/java/org/apache/catalina/core/StandardContext.java Wed Jul 27 15:47:44 2016 @@ -806,10 +806,29 @@ public class StandardContext extends Con private boolean useRelativeRedirects = !Globals.STRICT_SERVLET_COMPLIANCE; + private boolean dispatchersUseEncodedPaths = true; + // ----------------------------------------------------- Context Properties @Override + public void setDispatchersUseEncodedPaths(boolean dispatchersUseEncodedPaths) { + this.dispatchersUseEncodedPaths = dispatchersUseEncodedPaths; + } + + + /** + * {@inheritDoc} + * <p> + * The default value for this implementation is {@code true}. + */ + @Override + public boolean getDispatchersUseEncodedPaths() { + return dispatchersUseEncodedPaths; + } + + + @Override public void setUseRelativeRedirects(boolean useRelativeRedirects) { this.useRelativeRedirects = useRelativeRedirects; } Modified: tomcat/trunk/java/org/apache/catalina/startup/FailedContext.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/startup/FailedContext.java?rev=1754281&r1=1754280&r2=1754281&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/catalina/startup/FailedContext.java (original) +++ tomcat/trunk/java/org/apache/catalina/startup/FailedContext.java Wed Jul 27 15:47:44 2016 @@ -783,4 +783,9 @@ public class FailedContext extends Lifec public void setUseRelativeRedirects(boolean useRelativeRedirects) { /* NO-OP */ } @Override public boolean getUseRelativeRedirects() { return true; } + + @Override + public void setDispatchersUseEncodedPaths(boolean dispatchersUseEncodedPaths) { /* NO-OP */ } + @Override + public boolean getDispatchersUseEncodedPaths() { return true; } } \ No newline at end of file Added: tomcat/trunk/test/org/apache/catalina/core/TestApplicationContextGetRequestDispatcher.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/catalina/core/TestApplicationContextGetRequestDispatcher.java?rev=1754281&view=auto ============================================================================== --- tomcat/trunk/test/org/apache/catalina/core/TestApplicationContextGetRequestDispatcher.java (added) +++ tomcat/trunk/test/org/apache/catalina/core/TestApplicationContextGetRequestDispatcher.java Wed Jul 27 15:47:44 2016 @@ -0,0 +1,504 @@ +/* + * 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.core; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; + +import javax.servlet.AsyncContext; +import javax.servlet.RequestDispatcher; +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.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +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.catalina.util.URLEncoder; +import org.apache.tomcat.util.buf.ByteChunk; + +@RunWith(value = Parameterized.class) +public class TestApplicationContextGetRequestDispatcher extends TomcatBaseTest { + + private final boolean useAsync; + + public TestApplicationContextGetRequestDispatcher(boolean useAsync) { + this.useAsync = useAsync; + } + + @Parameters(name = "{index}: useAsync[{0}]") + public static Collection<Object[]> data() { + return Arrays.asList(new Object[][]{ + {Boolean.TRUE}, + {Boolean.FALSE} + }); + } + + @Test + public void testGetRequestDispatcherNullPath01() throws Exception { + doTestGetRequestDispatcher(true, "/start", null, null, "/target", DispatcherServlet.NULL); + } + + + @Test + public void testGetRequestDispatcherNullPath02() throws Exception { + doTestGetRequestDispatcher(false, "/start", null, null, "/target", DispatcherServlet.NULL); + } + + + @Test + public void testGetRequestDispatcherOutsideContextRoot01() throws Exception { + doTestGetRequestDispatcher( + true, "/start", null, "../outside", "/target", DispatcherServlet.NULL); + } + + + @Test + public void testGetRequestDispatcherOutsideContextRoot02() throws Exception { + doTestGetRequestDispatcher( + false, "/start", null, "../outside", "/target", DispatcherServlet.NULL); + } + + + @Test + public void testGetRequestDispatcherEncodedTraversal() throws Exception { + doTestGetRequestDispatcher( + true, "/prefix/start", null, "%2E%2E/target", "/target", DispatcherServlet.NULL); + } + + + @Test + public void testGetRequestDispatcherTraversal01() throws Exception { + doTestGetRequestDispatcher( + true, "/prefix/start", null, "../target", "/target", TargetServlet.OK); + } + + + @Test + public void testGetRequestDispatcherTraversal02() throws Exception { + doTestGetRequestDispatcher( + false, "/prefix/start", null, "../target", "/target", TargetServlet.OK); + } + + + @Test + public void testGetRequestDispatcherTraversal03() throws Exception { + doTestGetRequestDispatcher( + true, "/prefix/start", null, "../target?a=b", "/target", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcherTraversal04() throws Exception { + doTestGetRequestDispatcher( + false, "/prefix/start", null, "../target?a=b", "/target", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcherTraversal05() throws Exception { + doTestGetRequestDispatcher( + true, "/prefix/start", "a=b", "../target", "/target", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcherTraversal06() throws Exception { + doTestGetRequestDispatcher( + false, "/prefix/start", "a=b", "../target", "/target", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher01() throws Exception { + doTestGetRequestDispatcher( + true, "/prefix/start", null, "target", "/prefix/target", TargetServlet.OK); + } + + + @Test + public void testGetRequestDispatcher02() throws Exception { + doTestGetRequestDispatcher( + false, "/prefix/start", null, "target", "/prefix/target", TargetServlet.OK); + } + + + @Test + public void testGetRequestDispatcher03() throws Exception { + doTestGetRequestDispatcher(true, "/prefix/start", null, "target?a=b", "/prefix/target", + TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher04() throws Exception { + doTestGetRequestDispatcher(false, "/prefix/start", null, "target?a=b", "/prefix/target", + TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher05() throws Exception { + doTestGetRequestDispatcher(true, "/prefix/start", "a=b", "target", "/prefix/target", + TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher06() throws Exception { + doTestGetRequestDispatcher(false, "/prefix/start", "a=b", "target", "/prefix/target", + TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher11() throws Exception { + doTestGetRequestDispatcher(true, "/aa%3Fbb%3Dcc/start", null, "target", + "/aa%3Fbb%3Dcc/target", TargetServlet.OK); + } + + + @Test + public void testGetRequestDispatcher12() throws Exception { + // Expected to fail because when the RD processes this as unencoded it + // sees /aa?bb=cc/target which it thinks is a query string. This is why + // Tomcat encodes by default. + doTestGetRequestDispatcher(false, "/aa%3Fbb%3Dcc/start", null, "target", + "/aa%3Fbb%3Dcc/target", Default404Servlet.DEFAULT_404); + } + + + @Test + public void testGetRequestDispatcher13() throws Exception { + doTestGetRequestDispatcher(true, "/aa%3Fbb%3Dcc/start", null, "target?a=b", + "/aa%3Fbb%3Dcc/target", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher14() throws Exception { + // Expected to fail because when the RD processes this as unencoded it + // sees /aa?bb=cc/target which it thinks is a query string. This is why + // Tomcat encodes by default. + doTestGetRequestDispatcher(false, "/aa%3Fbb%3Dcc/start", null, "target?a=b", + "/aa%3Fbb%3Dcc/target", Default404Servlet.DEFAULT_404); + } + + + @Test + public void testGetRequestDispatcher15() throws Exception { + doTestGetRequestDispatcher(true, "/aa%3Fbb%3Dcc/start", "a=b", "target", + "/aa%3Fbb%3Dcc/target", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher16() throws Exception { + // Expected to fail because when the RD processes this as unencoded it + // sees /aa?bb=cc/target which it thinks is a query string. This is why + // Tomcat encodes by default. + doTestGetRequestDispatcher(false, "/aa%3Fbb%3Dcc/start", "a=b", "target", + "/aa%3Fbb%3Dcc/target", Default404Servlet.DEFAULT_404); + } + + + @Test + public void testGetRequestDispatcher21() throws Exception { + doTestGetRequestDispatcher(true, "/aa%3Dbb%3Dcc/start", null, "target", + "/aa%3Dbb%3Dcc/target", TargetServlet.OK); + } + + + @Test + public void testGetRequestDispatcher22() throws Exception { + doTestGetRequestDispatcher(false, "/aa%3Dbb%3Dcc/start", null, "target", + "/aa%3Dbb%3Dcc/target", TargetServlet.OK); + } + + + @Test + public void testGetRequestDispatcher23() throws Exception { + doTestGetRequestDispatcher(true, "/aa%3Dbb%3Dcc/start", null, "target?a=b", + "/aa%3Dbb%3Dcc/target", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher24() throws Exception { + doTestGetRequestDispatcher(false, "/aa%3Dbb%3Dcc/start", null, "target?a=b", + "/aa%3Dbb%3Dcc/target", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher25() throws Exception { + doTestGetRequestDispatcher(true, "/aa%3Dbb%3Dcc/start", "a=b", "target", + "/aa%3Dbb%3Dcc/target", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher26() throws Exception { + doTestGetRequestDispatcher(false, "/aa%3Dbb%3Dcc/start", "a=b", "target", + "/aa%3Dbb%3Dcc/target", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher31() throws Exception { + doTestGetRequestDispatcher(true, "/prefix/start", null, "aa%3Fbb%3Dcc", + "/prefix/aa%3Fbb%3Dcc", TargetServlet.OK); + } + + + @Test + public void testGetRequestDispatcher32() throws Exception { + doTestGetRequestDispatcher(false, "/prefix/start", null, "aa%3Fbb%3Dcc", + "/prefix/aa%3Fbb%3Dcc", Default404Servlet.DEFAULT_404); + } + + + @Test + public void testGetRequestDispatcher33() throws Exception { + doTestGetRequestDispatcher(true, "/prefix/start", null, "aa%3Fbb%3Dcc?a=b", + "/prefix/aa%3Fbb%3Dcc", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher34() throws Exception { + doTestGetRequestDispatcher(false, "/prefix/start", null, "aa%3Fbb%3Dcc?a=b", + "/prefix/aa%3Fbb%3Dcc", Default404Servlet.DEFAULT_404); + } + + + @Test + public void testGetRequestDispatcher35() throws Exception { + doTestGetRequestDispatcher(true, "/prefix/start", "a=b", "aa%3Fbb%3Dcc", + "/prefix/aa%3Fbb%3Dcc", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher36() throws Exception { + doTestGetRequestDispatcher(false, "/prefix/start", "a=b", "aa%3Fbb%3Dcc", + "/prefix/aa%3Fbb%3Dcc", Default404Servlet.DEFAULT_404); + } + + + @Test + public void testGetRequestDispatcher41() throws Exception { + doTestGetRequestDispatcher(true, "/prefix/start", null, "aa%3Fbb%3Dcc", + "/prefix/aa%253Fbb%253Dcc", Default404Servlet.DEFAULT_404); + } + + + @Test + public void testGetRequestDispatcher42() throws Exception { + doTestGetRequestDispatcher(false, "/prefix/start", null, "aa%3Fbb%3Dcc", + "/prefix/aa%253Fbb%253Dcc", TargetServlet.OK); + } + + + @Test + public void testGetRequestDispatcher43() throws Exception { + doTestGetRequestDispatcher(true, "/prefix/start", null, "aa%3Fbb%3Dcc?a=b", + "/prefix/aa%253Fbb%253Dcc", Default404Servlet.DEFAULT_404); + } + + + @Test + public void testGetRequestDispatcher44() throws Exception { + doTestGetRequestDispatcher(false, "/prefix/start", null, "aa%3Fbb%3Dcc?a=b", + "/prefix/aa%253Fbb%253Dcc", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher45() throws Exception { + doTestGetRequestDispatcher(true, "/prefix/start", "a=b", "aa%3Fbb%3Dcc", + "/prefix/aa%253Fbb%253Dcc", Default404Servlet.DEFAULT_404); + } + + + @Test + public void testGetRequestDispatcher46() throws Exception { + doTestGetRequestDispatcher(false, "/prefix/start", "a=b", "aa%3Fbb%3Dcc", + "/prefix/aa%253Fbb%253Dcc", TargetServlet.OK + "a=b"); + } + + + private void doTestGetRequestDispatcher(boolean useEncodedDispatchPaths, String startPath, + String startQueryString, String dispatchPath, String targetPath, String expectedBody) + throws Exception { + + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = tomcat.addContext("/test", null); + ctx.setDispatchersUseEncodedPaths(useEncodedDispatchPaths); + + // Add a default servlet to return 404 for not found resources + Tomcat.addServlet(ctx, "Default", new Default404Servlet()); + ctx.addServletMapping("/*", "Default"); + + // Add a target servlet to dispatch to + Tomcat.addServlet(ctx, "target", new TargetServlet()); + // Note: This will decode the provided path + ctx.addServletMapping(targetPath, "target"); + + if (useAsync) { + Wrapper w = Tomcat.addServlet( + ctx, "rd", new AsyncDispatcherServlet(dispatchPath, useEncodedDispatchPaths)); + w.setAsyncSupported(true); + } else { + Tomcat.addServlet(ctx, "rd", new DispatcherServlet(dispatchPath)); + } + // Note: This will decode the provided path + ctx.addServletMapping(startPath, "rd"); + + tomcat.start(); + + StringBuilder url = new StringBuilder("http://localhost:"); + url.append(getPort()); + url.append("/test"); + url.append(startPath); + if (startQueryString != null) { + url.append('?'); + url.append(startQueryString); + } + + ByteChunk bc = getUrl(url.toString()); + String body = bc.toString(); + + Assert.assertEquals(expectedBody, body); + } + + + private static class Default404Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + private static final String DEFAULT_404 = "DEFAULT-404"; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.getWriter().print(DEFAULT_404); + resp.setStatus(404); + } + } + + + private static class DispatcherServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + private static final String NULL = "RD-NULL"; + + private final String dispatchPath; + + public DispatcherServlet(String dispatchPath) { + this.dispatchPath = dispatchPath; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + RequestDispatcher rd = req.getRequestDispatcher(dispatchPath); + if (rd == null) { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.getWriter().print(NULL); + } else { + rd.forward(req, resp); + } + } + } + + + private static class TargetServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + private static final String OK = "OK"; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.getWriter().print(OK); + String qs = req.getQueryString(); + if (qs != null) { + resp.getWriter().print(qs); + } + } + } + + + private static class AsyncDispatcherServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + private static final String NULL = "RD-NULL"; + + private final String dispatchPath; + private final boolean encodePath; + + public AsyncDispatcherServlet(String dispatchPath, boolean encodePath) { + this.dispatchPath = dispatchPath; + this.encodePath = encodePath; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + AsyncContext ac = req.startAsync(); + // Quick and dirty. Sufficient for this test but ignores lots of + // edge cases. + String target = null; + if (dispatchPath != null) { + target = req.getServletPath(); + int lastSlash = target.lastIndexOf('/'); + target = target.substring(0, lastSlash + 1); + if (encodePath) { + target = URLEncoder.DEFAULT.encode(target, "UTF-8"); + } + target += dispatchPath; + } + try { + ac.dispatch(target); + } catch (UnsupportedOperationException uoe) { + ac.complete(); + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.getWriter().print(NULL); + } + } + } +} Propchange: tomcat/trunk/test/org/apache/catalina/core/TestApplicationContextGetRequestDispatcher.java ------------------------------------------------------------------------------ svn:eol-style = native Modified: tomcat/trunk/test/org/apache/catalina/core/TestAsyncContextImpl.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/catalina/core/TestAsyncContextImpl.java?rev=1754281&r1=1754280&r2=1754281&view=diff ============================================================================== --- tomcat/trunk/test/org/apache/catalina/core/TestAsyncContextImpl.java (original) +++ tomcat/trunk/test/org/apache/catalina/core/TestAsyncContextImpl.java Wed Jul 27 15:47:44 2016 @@ -2591,4 +2591,10 @@ public class TestAsyncContextImpl extend } } } + + + @Test + public void testAsyncDispatchEncoding() throws Exception { + + } } Modified: tomcat/trunk/test/org/apache/tomcat/unittest/TesterContext.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/tomcat/unittest/TesterContext.java?rev=1754281&r1=1754280&r2=1754281&view=diff ============================================================================== --- tomcat/trunk/test/org/apache/tomcat/unittest/TesterContext.java (original) +++ tomcat/trunk/test/org/apache/tomcat/unittest/TesterContext.java Wed Jul 27 15:47:44 2016 @@ -1246,4 +1246,9 @@ public class TesterContext implements Co public void setUseRelativeRedirects(boolean useRelativeRedirects) { /* NO-OP */ } @Override public boolean getUseRelativeRedirects() { return true; } + + @Override + public void setDispatchersUseEncodedPaths(boolean dispatchersUseEncodedPaths) { /* NO-OP */ } + @Override + public boolean getDispatchersUseEncodedPaths() { return true; } } Modified: tomcat/trunk/webapps/docs/changelog.xml URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=1754281&r1=1754280&r2=1754281&view=diff ============================================================================== --- tomcat/trunk/webapps/docs/changelog.xml (original) +++ tomcat/trunk/webapps/docs/changelog.xml Wed Jul 27 15:47:44 2016 @@ -62,6 +62,12 @@ Compatibility with rewrite from httpd for non existing headers. (jfclere) </fix> + <fix> + By default, treat paths used to obtain a request dispatcher as encoded. + This behaviour can be changed per web application via the + <code>dispatchersUseEncodedPaths</code> attribute of the Context. + (markt) + </fix> </changelog> </subsection> <subsection name="Coyote"> Modified: tomcat/trunk/webapps/docs/config/context.xml URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/config/context.xml?rev=1754281&r1=1754280&r2=1754281&view=diff ============================================================================== --- tomcat/trunk/webapps/docs/config/context.xml (original) +++ tomcat/trunk/webapps/docs/config/context.xml Wed Jul 27 15:47:44 2016 @@ -334,6 +334,14 @@ sufficient.</p> </attribute> + <attribute name="dispatchersUseEncodedPaths" required="false"> + <p>Controls whether paths used in calls to obtain a request dispatcher + ares expected to be encoded. This affects both how Tomcat handles calls + to obtain a request dispatcher as well as how Tomcat generates paths + used to obtain request dispatchers internally. If not specified, the + default value of <code>true</code> is used.</p> + </attribute> + <attribute name="failCtxIfServletStartFails" required="false"> <p>Set to <code>true</code> to have the context fail its startup if any servlet that has load-on-startup >=0 fails its own startup.</p> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org