Skip to content

Commit bea48b5

Browse files
committed
8272758: Improve HttpServer to avoid partial file name matches while mapping request path to context path
Reviewed-by: dfuchs
1 parent 759fe58 commit bea48b5

File tree

6 files changed

+471
-26
lines changed

6 files changed

+471
-26
lines changed

src/jdk.httpserver/share/classes/com/sun/net/httpserver/HttpServer.java

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2005, 2026, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -75,7 +75,7 @@
7575
*
7676
* <p>The following table shows some request URIs and which, if any context they would
7777
* match with:
78-
* <table class="striped"><caption style="display:none">description</caption>
78+
* <table class="striped" style="text-align:left"><caption style="display:none">description</caption>
7979
* <thead>
8080
* <tr>
8181
* <th scope="col"><i>Request URI</i></th>
@@ -278,17 +278,29 @@ public static HttpServer create(InetSocketAddress addr,
278278
* <p>The class overview describes how incoming request URIs are
279279
* <a href="#mapping_description">mapped</a> to HttpContext instances.
280280
*
281-
* @apiNote The path should generally, but is not required to, end with '/'.
282-
* If the path does not end with '/', eg such as with {@code "/foo"} then
283-
* this would match requests with a path of {@code "/foobar"} or
284-
* {@code "/foo/bar"}.
281+
* @apiNote
282+
* The path should generally, but is not required to, end with {@code /}.
283+
* If the path does not end with {@code /}, e.g., such as with {@code /foo},
284+
* then some implementations may use <em>string prefix matching</em> where
285+
* this context path matches request paths {@code /foo},
286+
* {@code /foo/bar}, or {@code /foobar}. Others may use <em>path prefix
287+
* matching</em> where {@code /foo} matches request paths {@code /foo} and
288+
* {@code /foo/bar}, but not {@code /foobar}.
289+
*
290+
* @implNote
291+
* By default, the JDK built-in implementation uses path prefix matching.
292+
* String prefix matching can be enabled using the
293+
* {@link jdk.httpserver/##sun.net.httpserver.pathMatcher sun.net.httpserver.pathMatcher}
294+
* system property.
285295
*
286296
* @param path the root URI path to associate the context with
287297
* @param handler the handler to invoke for incoming requests
288298
* @throws IllegalArgumentException if path is invalid, or if a context
289299
* already exists for this path
290300
* @throws NullPointerException if either path, or handler are {@code null}
291301
* @return an instance of {@code HttpContext}
302+
*
303+
* @see jdk.httpserver/##sun.net.httpserver.pathMatcher sun.net.httpserver.pathMatcher
292304
*/
293305
public abstract HttpContext createContext(String path, HttpHandler handler);
294306

@@ -308,16 +320,28 @@ public static HttpServer create(InetSocketAddress addr,
308320
* <p>The class overview describes how incoming request URIs are
309321
* <a href="#mapping_description">mapped</a> to {@code HttpContext} instances.
310322
*
311-
* @apiNote The path should generally, but is not required to, end with '/'.
312-
* If the path does not end with '/', eg such as with {@code "/foo"} then
313-
* this would match requests with a path of {@code "/foobar"} or
314-
* {@code "/foo/bar"}.
323+
* @apiNote
324+
* The path should generally, but is not required to, end with {@code /}.
325+
* If the path does not end with {@code /}, e.g., such as with {@code /foo},
326+
* then some implementations may use <em>string prefix matching</em> where
327+
* this context path matches request paths {@code /foo},
328+
* {@code /foo/bar}, or {@code /foobar}. Others may use <em>path prefix
329+
* matching</em> where {@code /foo} matches request paths
330+
* {@code /foo} and {@code /foo/bar}, but not {@code /foobar}.
331+
*
332+
* @implNote
333+
* By default, the JDK built-in implementation uses path prefix matching.
334+
* String prefix matching can be enabled using the
335+
* {@link jdk.httpserver/##sun.net.httpserver.pathMatcher sun.net.httpserver.pathMatcher}
336+
* system property.
315337
*
316338
* @param path the root URI path to associate the context with
317339
* @throws IllegalArgumentException if path is invalid, or if a context
318340
* already exists for this path
319341
* @throws NullPointerException if path is {@code null}
320342
* @return an instance of {@code HttpContext}
343+
*
344+
* @see jdk.httpserver/##sun.net.httpserver.pathMatcher sun.net.httpserver.pathMatcher
321345
*/
322346
public abstract HttpContext createContext(String path);
323347

src/jdk.httpserver/share/classes/module-info.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2014, 2026, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -101,7 +101,35 @@
101101
* <li><p><b>{@systemProperty sun.net.httpserver.nodelay}</b> (default: false)<br>
102102
* Boolean value, which if true, sets the {@link java.net.StandardSocketOptions#TCP_NODELAY TCP_NODELAY}
103103
* socket option on all incoming connections.
104-
* </li></ul>
104+
* </li>
105+
* <li>
106+
* <p><b>{@systemProperty sun.net.httpserver.pathMatcher}</b> (default:
107+
* {@code pathPrefix})<br/>
108+
*
109+
* The path matching scheme used to route requests to context handlers.
110+
* The property can be configured with one of the following values:</p>
111+
*
112+
* <blockquote>
113+
* <dl>
114+
* <dt>{@code pathPrefix} (default)</dt>
115+
* <dd>The request path must begin with the context path and all matching path
116+
* segments must be identical. For instance, the context path {@code /foo}
117+
* would match request paths {@code /foo}, {@code /foo/}, and {@code /foo/bar},
118+
* but not {@code /foobar}.</dd>
119+
* <dt>{@code stringPrefix}</dt>
120+
* <dd>The request path string must begin with the context path string. For
121+
* instance, the context path {@code /foo} would match request paths
122+
* {@code /foo}, {@code /foo/}, {@code /foo/bar}, and {@code /foobar}.
123+
* </dd>
124+
* </dl>
125+
* </blockquote>
126+
*
127+
* <p>In case of a blank or invalid value, the default will be used.</p>
128+
*
129+
* <p>This property and the ability to restore the string prefix matching
130+
* behavior may be removed in a future release.</p>
131+
* </li>
132+
* </ul>
105133
*
106134
* @apiNote The API and SPI in this module are designed and implemented to support a minimal
107135
* HTTP server and simple HTTP semantics primarily.

src/jdk.httpserver/share/classes/sun/net/httpserver/ContextList.java

Lines changed: 186 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2005, 2026, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -26,35 +26,48 @@
2626
package sun.net.httpserver;
2727

2828
import java.util.*;
29+
import java.util.function.BiPredicate;
2930

3031
class ContextList {
3132

33+
private static final System.Logger LOGGER = System.getLogger(ContextList.class.getName());
34+
3235
private final LinkedList<HttpContextImpl> list = new LinkedList<>();
3336

3437
public synchronized void add(HttpContextImpl ctx) {
38+
assert ctx != null;
39+
// `findContext(String protocol, String path, ContextPathMatcher matcher)`
40+
// expects the protocol to be lower-cased using ROOT locale, hence:
41+
assert ctx.getProtocol().equals(ctx.getProtocol().toLowerCase(Locale.ROOT));
3542
assert ctx.getPath() != null;
43+
// `ContextPathMatcher` expects context paths to be non-empty:
44+
assert !ctx.getPath().isEmpty();
3645
if (contains(ctx)) {
3746
throw new IllegalArgumentException("cannot add context to list");
3847
}
3948
list.add(ctx);
4049
}
4150

4251
boolean contains(HttpContextImpl ctx) {
43-
return findContext(ctx.getProtocol(), ctx.getPath(), true) != null;
52+
return findContext(ctx.getProtocol(), ctx.getPath(), ContextPathMatcher.EXACT) != null;
4453
}
4554

4655
public synchronized int size() {
4756
return list.size();
4857
}
4958

50-
/* initially contexts are located only by protocol:path.
51-
* Context with longest prefix matches (currently case-sensitive)
59+
/**
60+
* {@return the context with the longest case-sensitive prefix match}
61+
*
62+
* @param protocol the request protocol
63+
* @param path the request path
5264
*/
53-
synchronized HttpContextImpl findContext(String protocol, String path) {
54-
return findContext(protocol, path, false);
65+
HttpContextImpl findContext(String protocol, String path) {
66+
var matcher = ContextPathMatcher.ofConfiguredPrefixPathMatcher();
67+
return findContext(protocol, path, matcher);
5568
}
5669

57-
synchronized HttpContextImpl findContext(String protocol, String path, boolean exact) {
70+
private synchronized HttpContextImpl findContext(String protocol, String path, ContextPathMatcher matcher) {
5871
protocol = protocol.toLowerCase(Locale.ROOT);
5972
String longest = "";
6073
HttpContextImpl lc = null;
@@ -63,9 +76,7 @@ synchronized HttpContextImpl findContext(String protocol, String path, boolean e
6376
continue;
6477
}
6578
String cpath = ctx.getPath();
66-
if (exact && !cpath.equals(path)) {
67-
continue;
68-
} else if (!exact && !path.startsWith(cpath)) {
79+
if (!matcher.test(cpath, path)) {
6980
continue;
7081
}
7182
if (cpath.length() > longest.length()) {
@@ -76,10 +87,174 @@ synchronized HttpContextImpl findContext(String protocol, String path, boolean e
7687
return lc;
7788
}
7889

90+
private enum ContextPathMatcher implements BiPredicate<String, String> {
91+
92+
/**
93+
* Tests if both the request path and the context path are identical.
94+
*/
95+
EXACT(String::equals),
96+
97+
/**
98+
* Tests <em>string prefix matches</em> where the request path string
99+
* starts with the context path string.
100+
*
101+
* <h3>Examples</h3>
102+
*
103+
* <table>
104+
* <thead>
105+
* <tr>
106+
* <th rowspan="2">Context path</th>
107+
* <th colspan="4">Request path</th>
108+
* </tr>
109+
* <tr>
110+
* <th>/foo</th>
111+
* <th>/foo/</th>
112+
* <th>/foo/bar</th>
113+
* <th>/foobar</th>
114+
* </tr>
115+
* </thead>
116+
* <tbody>
117+
* <tr>
118+
* <td>/</td>
119+
* <td>Y</td>
120+
* <td>Y</td>
121+
* <td>Y</td>
122+
* <td>Y</td>
123+
* </tr>
124+
* <tr>
125+
* <td>/foo</td>
126+
* <td>Y</td>
127+
* <td>Y</td>
128+
* <td>Y</td>
129+
* <td>Y</td>
130+
* </tr>
131+
* <tr>
132+
* <td>/foo/</td>
133+
* <td>N</td>
134+
* <td>Y</td>
135+
* <td>Y</td>
136+
* <td>N</td>
137+
* </tr>
138+
* </tbody>
139+
* </table>
140+
*/
141+
STRING_PREFIX((contextPath, requestPath) -> requestPath.startsWith(contextPath)),
142+
143+
/**
144+
* Tests <em>path prefix matches</em> where path segments must have an
145+
* exact match.
146+
*
147+
* <h3>Examples</h3>
148+
*
149+
* <table>
150+
* <thead>
151+
* <tr>
152+
* <th rowspan="2">Context path</th>
153+
* <th colspan="4">Request path</th>
154+
* </tr>
155+
* <tr>
156+
* <th>/foo</th>
157+
* <th>/foo/</th>
158+
* <th>/foo/bar</th>
159+
* <th>/foobar</th>
160+
* </tr>
161+
* </thead>
162+
* <tbody>
163+
* <tr>
164+
* <td>/</td>
165+
* <td>Y</td>
166+
* <td>Y</td>
167+
* <td>Y</td>
168+
* <td>Y</td>
169+
* </tr>
170+
* <tr>
171+
* <td>/foo</td>
172+
* <td>Y</td>
173+
* <td>Y</td>
174+
* <td>Y</td>
175+
* <td>N</td>
176+
* </tr>
177+
* <tr>
178+
* <td>/foo/</td>
179+
* <td>N</td>
180+
* <td>Y</td>
181+
* <td>Y</td>
182+
* <td>N</td>
183+
* </tr>
184+
* </tbody>
185+
* </table>
186+
*/
187+
PATH_PREFIX((contextPath, requestPath) -> {
188+
189+
// Fast-path for `/`
190+
if ("/".equals(contextPath)) {
191+
return true;
192+
}
193+
194+
// Does the request path prefix match?
195+
if (requestPath.startsWith(contextPath)) {
196+
197+
// Is it an exact match?
198+
int contextPathLength = contextPath.length();
199+
if (requestPath.length() == contextPathLength) {
200+
return true;
201+
}
202+
203+
// Is it a path-prefix match?
204+
assert contextPathLength > 0;
205+
return
206+
// Case 1: The request path starts with the context
207+
// path, but the context path has an extra path
208+
// separator suffix. For instance, the context path is
209+
// `/foo/` and the request path is `/foo/bar`.
210+
contextPath.charAt(contextPathLength - 1) == '/' ||
211+
// Case 2: The request path starts with the
212+
// context path, but the request path has an
213+
// extra path separator suffix. For instance,
214+
// context path is `/foo` and the request path
215+
// is `/foo/` or `/foo/bar`.
216+
requestPath.charAt(contextPathLength) == '/';
217+
218+
}
219+
220+
return false;
221+
222+
});
223+
224+
private final BiPredicate<String, String> predicate;
225+
226+
ContextPathMatcher(BiPredicate<String, String> predicate) {
227+
this.predicate = predicate;
228+
}
229+
230+
@Override
231+
public boolean test(String contextPath, String requestPath) {
232+
return predicate.test(contextPath, requestPath);
233+
}
234+
235+
private static ContextPathMatcher ofConfiguredPrefixPathMatcher() {
236+
var propertyName = "sun.net.httpserver.pathMatcher";
237+
var propertyValueDefault = "pathPrefix";
238+
var propertyValue = System.getProperty(propertyName, propertyValueDefault);
239+
return switch (propertyValue) {
240+
case "pathPrefix" -> ContextPathMatcher.PATH_PREFIX;
241+
case "stringPrefix" -> ContextPathMatcher.STRING_PREFIX;
242+
default -> {
243+
LOGGER.log(
244+
System.Logger.Level.WARNING,
245+
"System property \"{}\" contains an invalid value: \"{}\". Falling back to the default: \"{}\"",
246+
propertyName, propertyValue, propertyValueDefault);
247+
yield ContextPathMatcher.PATH_PREFIX;
248+
}
249+
};
250+
}
251+
252+
}
253+
79254
public synchronized void remove(String protocol, String path)
80255
throws IllegalArgumentException
81256
{
82-
HttpContextImpl ctx = findContext(protocol, path, true);
257+
HttpContextImpl ctx = findContext(protocol, path, ContextPathMatcher.EXACT);
83258
if (ctx == null) {
84259
throw new IllegalArgumentException("cannot remove element from list");
85260
}

0 commit comments

Comments
 (0)