Skip to content

Commit 68831e7

Browse files
committed
UNDERTOW-1844: Fix getHttpServletMapping when both prefix and extension match
This change results in adherance to the servlet 4.0 specification in which any path match is used prior to extension matches. From undertow-io/undertow#1038 includes both 1f36d68628b0398edc1302f714aa88364acd5e5b and fc1db053d3e3a8a5948a891f7842be7ce74b2dfb
1 parent aad09ba commit 68831e7

File tree

7 files changed

+249
-28
lines changed

7 files changed

+249
-28
lines changed

servlet/src/main/java/io/undertow/servlet/handlers/ServletPathMatches.java

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ private ServletPathMatchesData setupServletChains() {
230230
//now loop through all servlets.
231231
for (Map.Entry<String, ServletHandler> entry : servlets.getServletHandlers().entrySet()) {
232232
final ServletHandler handler = entry.getValue();
233-
//add the servlet to the approprite path maps
233+
//add the servlet to the appropriate path maps
234234
for (String path : handler.getManagedServlet().getServletInfo().getMappings()) {
235235
if (path.equals("/")) {
236236
//the default servlet
@@ -282,7 +282,7 @@ private ServletPathMatchesData setupServletChains() {
282282

283283
final Map<DispatcherType, List<ManagedFilter>> noExtension = new EnumMap<>(DispatcherType.class);
284284
final Map<String, Map<DispatcherType, List<ManagedFilter>>> extension = new HashMap<>();
285-
//initalize the extension map. This contains all the filers in the noExtension map, plus
285+
//initialize the extension map. This contains all the filers in the noExtension map, plus
286286
//any filters that match the extension key
287287
for (String ext : extensionMatches) {
288288
extension.put(ext, new EnumMap<DispatcherType, List<ManagedFilter>>(DispatcherType.class));
@@ -332,16 +332,36 @@ private ServletPathMatchesData setupServletChains() {
332332
ServletHandler pathServlet = targetServletMatch.handler;
333333
String pathMatch = targetServletMatch.matchedPath;
334334

335-
boolean defaultServletMatch = targetServletMatch.defaultServlet;
336-
if (defaultServletMatch && extensionServlets.containsKey(entry.getKey())) {
335+
final boolean defaultServletMatch;
336+
final String servletMatchPattern;
337+
final MappingMatch mappingMatch;
338+
if (targetServletMatch.defaultServlet) {
339+
// Path matches always take precedence over extension matches, however the default servlet is matched
340+
// at a lower priority, after extension matches. The "/*" pattern is applied implicitly onto the
341+
// default servlet. If there's an extension match in addition to a non-default servlet path match,
342+
// the servlet path match is higher priority. However if the path match is the default servlets
343+
// default catch-all path, the extension match is a higher priority.
344+
ServletHandler extensionServletHandler = extensionServlets.get(entry.getKey());
345+
if (extensionServletHandler != null) {
346+
defaultServletMatch = false;
347+
pathServlet = extensionServletHandler;
348+
servletMatchPattern = "*." + entry.getKey();
349+
mappingMatch = MappingMatch.EXTENSION;
350+
} else {
351+
defaultServletMatch = true;
352+
servletMatchPattern = "/";
353+
mappingMatch = MappingMatch.DEFAULT;
354+
}
355+
} else {
337356
defaultServletMatch = false;
338-
pathServlet = extensionServlets.get(entry.getKey());
357+
servletMatchPattern = path;
358+
mappingMatch = MappingMatch.PATH;
339359
}
340360
HttpHandler handler = pathServlet;
341361
if (!entry.getValue().isEmpty()) {
342362
handler = new FilterHandler(entry.getValue(), deploymentInfo.isAllowNonStandardWrappers(), handler);
343363
}
344-
builder.addExtensionMatch(prefix, entry.getKey(), servletChain(handler, pathServlet.getManagedServlet(), entry.getValue(), pathMatch, deploymentInfo, defaultServletMatch, defaultServletMatch ? MappingMatch.DEFAULT : MappingMatch.EXTENSION, defaultServletMatch ? "/" : "*." + entry.getKey()));
364+
builder.addExtensionMatch(prefix, entry.getKey(), servletChain(handler, pathServlet.getManagedServlet(), entry.getValue(), pathMatch, deploymentInfo, defaultServletMatch, mappingMatch, servletMatchPattern));
345365
}
346366
} else if (path.isEmpty()) {
347367
//the context root match

servlet/src/main/java/io/undertow/servlet/handlers/ServletPathMatchesData.java

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,19 +79,15 @@ public ServletPathMatch getServletHandlerByPath(final String path) {
7979
}
8080
}
8181
//this should never happen
82-
//as the default servlet is aways registered under /*
82+
//as the default servlet is always registered under /*
8383
throw UndertowMessages.MESSAGES.servletPathMatchFailed();
8484
}
8585

8686
private ServletPathMatch handleMatch(final String path, final PathMatch match, final int extensionPos) {
87-
if (match.extensionMatches.isEmpty()) {
87+
if (extensionPos == -1 || match.extensionMatches.isEmpty()) {
8888
return new ServletPathMatch(match.defaultHandler, path, match.requireWelcomeFileMatch);
8989
}
90-
if (extensionPos == -1) {
91-
return new ServletPathMatch(match.defaultHandler, path, match.requireWelcomeFileMatch);
92-
}
93-
final String ext;
94-
ext = path.substring(extensionPos + 1, path.length());
90+
final String ext = path.substring(extensionPos + 1);
9591
ServletChain handler = match.extensionMatches.get(ext);
9692
if (handler != null) {
9793
return new ServletPathMatch(handler, path, handler.getManagedServlet().getServletInfo().isRequireWelcomeFileMapping());

servlet/src/main/java/io/undertow/servlet/spec/HttpServletRequestImpl.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -228,15 +228,18 @@ public HttpServletMapping getHttpServletMapping() {
228228
break;
229229
case PATH:
230230
matchValue = match.getRemaining();
231-
if (matchValue.startsWith("/")) {
231+
if (matchValue == null) {
232+
matchValue = "";
233+
} else if (matchValue.startsWith("/")) {
232234
matchValue = matchValue.substring(1);
233235
}
234236
break;
235237
case EXTENSION:
236-
matchValue = match.getMatched().substring(0, match.getMatched().length() - match.getMatchString().length() + 1);
237-
if (matchValue.startsWith("/")) {
238-
matchValue = matchValue.substring(1);
239-
}
238+
String matched = match.getMatched();
239+
String matchString = match.getMatchString();
240+
int startIndex = matched.startsWith("/") ? 1 : 0;
241+
int endIndex = matched.length() - matchString.length() + 1;
242+
matchValue = matched.substring(startIndex, endIndex);
240243
break;
241244
default:
242245
matchValue = match.getRemaining();

servlet/src/test/java/io/undertow/servlet/test/path/GetMappingServlet.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t
3535
response.getWriter()
3636
.append("Mapping match:")
3737
.append(mapping.getMappingMatch().name())
38-
.append("\n")
39-
.append("Match value:")
38+
.append("\nMatch value:")
4039
.append(mapping.getMatchValue())
41-
.append("\n")
42-
.append("Pattern:")
43-
.append(mapping.getPattern());
40+
.append("\nPattern:")
41+
.append(mapping.getPattern())
42+
.append("\nServlet:")
43+
.append(mapping.getServletName());
4444
}
4545

4646
}

servlet/src/test/java/io/undertow/servlet/test/path/MappingTestCase.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,39 +63,39 @@ public void testGetMapping() throws IOException {
6363
String response = HttpClientUtils.readResponse(result);
6464
Assert.assertEquals("Mapping match:PATH\n" +
6565
"Match value:foo\n" +
66-
"Pattern:/path/*", response);
66+
"Pattern:/path/*\nServlet:path", response);
6767

6868
get = new HttpGet(DefaultServer.getDefaultServerURL() + "/servletContext/foo.ext");
6969
result = client.execute(get);
7070
Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode());
7171
response = HttpClientUtils.readResponse(result);
7272
Assert.assertEquals("Mapping match:EXTENSION\n" +
7373
"Match value:foo\n" +
74-
"Pattern:*.ext", response);
74+
"Pattern:*.ext\nServlet:path", response);
7575

7676
get = new HttpGet(DefaultServer.getDefaultServerURL() + "/servletContext/");
7777
result = client.execute(get);
7878
Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode());
7979
response = HttpClientUtils.readResponse(result);
8080
Assert.assertEquals("Mapping match:CONTEXT_ROOT\n" +
8181
"Match value:\n" +
82-
"Pattern:", response);
82+
"Pattern:\nServlet:path", response);
8383

8484
get = new HttpGet(DefaultServer.getDefaultServerURL() + "/servletContext/doesnotexist");
8585
result = client.execute(get);
8686
Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode());
8787
response = HttpClientUtils.readResponse(result);
8888
Assert.assertEquals("Mapping match:DEFAULT\n" +
8989
"Match value:\n" +
90-
"Pattern:/", response);
90+
"Pattern:/\nServlet:path", response);
9191

9292
get = new HttpGet(DefaultServer.getDefaultServerURL() + "/servletContext/exact");
9393
result = client.execute(get);
9494
Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode());
9595
response = HttpClientUtils.readResponse(result);
9696
Assert.assertEquals("Mapping match:EXACT\n" +
9797
"Match value:exact\n" +
98-
"Pattern:/exact", response);
98+
"Pattern:/exact\nServlet:path", response);
9999

100100
} finally {
101101
client.getConnectionManager().shutdown();
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* JBoss, Home of Professional Open Source.
3+
* Copyright 2021 Red Hat, Inc., and individual contributors
4+
* as indicated by the @author tags.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package io.undertow.servlet.test.path;
20+
21+
import io.undertow.servlet.api.ServletInfo;
22+
import io.undertow.servlet.test.util.DeploymentUtils;
23+
import io.undertow.testutils.DefaultServer;
24+
import io.undertow.testutils.HttpClientUtils;
25+
import io.undertow.testutils.TestHttpClient;
26+
import io.undertow.httpcore.StatusCodes;
27+
import org.apache.http.HttpResponse;
28+
import org.apache.http.client.methods.HttpGet;
29+
import org.junit.Assert;
30+
import org.junit.BeforeClass;
31+
import org.junit.Test;
32+
import org.junit.runner.RunWith;
33+
34+
import javax.servlet.ServletException;
35+
import javax.servlet.http.MappingMatch;
36+
import java.io.IOException;
37+
38+
/**
39+
* Test cases for <a href="https://issues.redhat.com/browse/UNDERTOW-1844">UNDERTOW-1844</a>.
40+
*
41+
* @author Carter Kozak
42+
*/
43+
@RunWith(DefaultServer.class)
44+
public class MultipleMatchingMappingTestCase {
45+
@BeforeClass
46+
public static void setup() throws ServletException {
47+
DeploymentUtils.setupServlet(
48+
new ServletInfo("path", GetMappingServlet.class)
49+
.addMapping("/path/*")
50+
.addMapping("/*")
51+
// This extension prefix is impossible to reach due to the '/*' path match.
52+
.addMapping("*.ext"));
53+
54+
}
55+
56+
@Test
57+
public void testMatchesPathAndExtension1() {
58+
doTest("/foo.ext", MappingMatch.PATH, "foo.ext", "/*", "path");
59+
}
60+
61+
@Test
62+
public void testMatchesPathAndExtension2() {
63+
doTest("/other/foo.ext", MappingMatch.PATH, "other/foo.ext", "/*", "path");
64+
}
65+
66+
@Test
67+
public void testMatchesPathAndExtension3() {
68+
doTest("/path/foo.ext", MappingMatch.PATH, "foo.ext", "/path/*", "path");
69+
}
70+
71+
private static void doTest(
72+
// Input request path excluding the servlet context path
73+
String path,
74+
// Expected HttpServletMapping result values
75+
MappingMatch mappingMatch,
76+
String matchValue,
77+
String pattern,
78+
String servletName) {
79+
TestHttpClient client = new TestHttpClient();
80+
try {
81+
HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL() + "/servletContext" + path);
82+
HttpResponse result = client.execute(get);
83+
Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode());
84+
String response = HttpClientUtils.readResponse(result);
85+
String expected = String.format("Mapping match:%s\nMatch value:%s\nPattern:%s\nServlet:%s",
86+
mappingMatch.name(), matchValue, pattern, servletName);
87+
Assert.assertEquals(expected, response);
88+
} catch (IOException e) {
89+
throw new AssertionError(e);
90+
} finally {
91+
client.getConnectionManager().shutdown();
92+
}
93+
}
94+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package io.undertow.servlet.test.path;
2+
3+
import io.undertow.servlet.api.ServletInfo;
4+
import io.undertow.servlet.test.util.DeploymentUtils;
5+
import io.undertow.testutils.DefaultServer;
6+
import io.undertow.testutils.HttpClientUtils;
7+
import io.undertow.testutils.TestHttpClient;
8+
import io.undertow.httpcore.StatusCodes;
9+
import org.apache.http.HttpResponse;
10+
import org.apache.http.client.methods.HttpGet;
11+
import org.junit.Assert;
12+
import org.junit.BeforeClass;
13+
import org.junit.Test;
14+
import org.junit.runner.RunWith;
15+
16+
import javax.servlet.ServletException;
17+
import javax.servlet.http.MappingMatch;
18+
import java.io.IOException;
19+
20+
/**
21+
* Test cases for the servlet mapping examples in section 12.2.2 of the
22+
* <a href="https://javaee.github.io/servlet-spec/downloads/servlet-4.0/servlet-4_0_FINAL.pdf">Servlet 4.0 specification</a>.
23+
*
24+
* @author Carter Kozak
25+
*/
26+
@RunWith(DefaultServer.class)
27+
public class ServletSpecExampleTestCase {
28+
29+
@BeforeClass
30+
public static void setup() throws ServletException {
31+
// Servlet 4.0 table 12-1 Example Set of Maps
32+
DeploymentUtils.setupServlet(
33+
new ServletInfo("servlet1", GetMappingServlet.class)
34+
.addMapping("/foo/bar/*"),
35+
new ServletInfo("servlet2", GetMappingServlet.class)
36+
.addMapping("/baz/*"),
37+
new ServletInfo("servlet3", GetMappingServlet.class)
38+
.addMapping("/catalog"),
39+
new ServletInfo("servlet4", GetMappingServlet.class)
40+
.addMapping("*.bop"),
41+
new ServletInfo("default", GetMappingServlet.class));
42+
43+
}
44+
45+
@Test
46+
public void testOne() {
47+
doTest("/foo/bar/index.html", MappingMatch.PATH, "index.html", "/foo/bar/*", "servlet1");
48+
}
49+
50+
@Test
51+
public void testTwo() {
52+
doTest("/foo/bar/index.bop", MappingMatch.PATH, "index.bop", "/foo/bar/*", "servlet1");
53+
}
54+
55+
@Test
56+
public void testThree() {
57+
doTest("/baz", MappingMatch.PATH, "", "/baz/*", "servlet2");
58+
}
59+
60+
@Test
61+
public void testFour() {
62+
doTest("/baz/index.html", MappingMatch.PATH, "index.html", "/baz/*", "servlet2");
63+
}
64+
65+
@Test
66+
public void testFive() {
67+
doTest("/catalog", MappingMatch.EXACT, "catalog", "/catalog", "servlet3");
68+
}
69+
70+
@Test
71+
public void testSix() {
72+
doTest("/catalog/index.html", MappingMatch.DEFAULT, "", "/", "default");
73+
}
74+
75+
@Test
76+
public void testSeven() {
77+
doTest("/catalog/racecar.bop", MappingMatch.EXTENSION, "catalog/racecar", "*.bop", "servlet4");
78+
}
79+
80+
@Test
81+
public void testEight() {
82+
doTest("/index.bop", MappingMatch.EXTENSION, "index", "*.bop", "servlet4");
83+
}
84+
85+
private static void doTest(
86+
// Input request path excluding the servlet context path
87+
String path,
88+
// Expected HttpServletMapping result values
89+
MappingMatch mappingMatch,
90+
String matchValue,
91+
String pattern,
92+
String servletName) {
93+
TestHttpClient client = new TestHttpClient();
94+
try {
95+
HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL() + "/servletContext" + path);
96+
HttpResponse result = client.execute(get);
97+
Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode());
98+
String response = HttpClientUtils.readResponse(result);
99+
String expected = String.format("Mapping match:%s\nMatch value:%s\nPattern:%s\nServlet:%s",
100+
mappingMatch.name(), matchValue, pattern, servletName);
101+
Assert.assertEquals(expected, response);
102+
} catch (IOException e) {
103+
throw new AssertionError(e);
104+
} finally {
105+
client.getConnectionManager().shutdown();
106+
}
107+
}
108+
}

0 commit comments

Comments
 (0)