Jetty 12.0.x 9444 servlet paths fully decoded (#9479)
getServletPath and getPathInfo will never return an encoded path segment. Instead, they will throw an IllegalArgumentException if they are called when there is a URI with violations. Signed-off-by: gregw <gregw@webtide.com> Co-authored-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>
This commit is contained in:
parent
bcd175a156
commit
bd0186c2f7
|
@ -99,19 +99,29 @@ public final class UriCompliance implements ComplianceViolation.Mode
|
|||
}
|
||||
}
|
||||
|
||||
public static final Set<Violation> AMBIGUOUS_VIOLATIONS = EnumSet.of(
|
||||
Violation.AMBIGUOUS_EMPTY_SEGMENT,
|
||||
Violation.AMBIGUOUS_PATH_ENCODING,
|
||||
Violation.AMBIGUOUS_PATH_PARAMETER,
|
||||
Violation.AMBIGUOUS_PATH_SEGMENT,
|
||||
Violation.AMBIGUOUS_PATH_SEPARATOR);
|
||||
|
||||
/**
|
||||
* The default compliance mode that extends <a href="https://tools.ietf.org/html/rfc3986">RFC3986</a> compliance with
|
||||
* additional violations to avoid most ambiguous URIs.
|
||||
* This mode does allow {@link Violation#AMBIGUOUS_PATH_SEPARATOR}, but disallows all out {@link Violation}s.
|
||||
* Compliance mode that exactly follows <a href="https://tools.ietf.org/html/rfc3986">RFC3986</a>,
|
||||
* excluding all URI Violations.
|
||||
*/
|
||||
public static final UriCompliance DEFAULT = new UriCompliance("DEFAULT",
|
||||
of(Violation.AMBIGUOUS_PATH_SEPARATOR,
|
||||
Violation.AMBIGUOUS_PATH_ENCODING));
|
||||
public static final UriCompliance RFC3986 = new UriCompliance("RFC3986", noneOf(Violation.class));
|
||||
|
||||
/**
|
||||
* The default compliance mode allows no violations from <a href="https://tools.ietf.org/html/rfc3986">RFC3986</a>
|
||||
* and is equivalent to {@link #RFC3986} compliance.
|
||||
*/
|
||||
public static final UriCompliance DEFAULT = RFC3986;
|
||||
|
||||
/**
|
||||
* LEGACY compliance mode that models Jetty-9.4 behavior by allowing {@link Violation#AMBIGUOUS_PATH_SEGMENT},
|
||||
* {@link Violation#AMBIGUOUS_EMPTY_SEGMENT}, {@link Violation#AMBIGUOUS_PATH_SEPARATOR}, {@link Violation#AMBIGUOUS_PATH_ENCODING}
|
||||
* and {@link Violation#UTF16_ENCODINGS}
|
||||
* and {@link Violation#UTF16_ENCODINGS}.
|
||||
*/
|
||||
public static final UriCompliance LEGACY = new UriCompliance("LEGACY",
|
||||
of(Violation.AMBIGUOUS_PATH_SEGMENT,
|
||||
|
@ -121,24 +131,12 @@ public final class UriCompliance implements ComplianceViolation.Mode
|
|||
Violation.UTF16_ENCODINGS));
|
||||
|
||||
/**
|
||||
* Compliance mode that exactly follows <a href="https://tools.ietf.org/html/rfc3986">RFC3986</a>,
|
||||
* including allowing all additional ambiguous URI Violations.
|
||||
*/
|
||||
public static final UriCompliance RFC3986 = new UriCompliance("RFC3986", allOf(Violation.class));
|
||||
|
||||
/**
|
||||
* Compliance mode that follows <a href="https://tools.ietf.org/html/rfc3986">RFC3986</a>
|
||||
* plus it does not allow any ambiguous URI {@link Violation}s.
|
||||
*/
|
||||
public static final UriCompliance RFC3986_UNAMBIGUOUS = new UriCompliance("RFC3986_UNAMBIGUOUS", noneOf(Violation.class));
|
||||
|
||||
/**
|
||||
* Compliance mode that allows all URI Violations, including allowing ambiguous paths in non canonicalized form.
|
||||
* Compliance mode that allows all URI Violations, including allowing ambiguous paths in non-canonical form.
|
||||
*/
|
||||
public static final UriCompliance UNSAFE = new UriCompliance("UNSAFE", allOf(Violation.class));
|
||||
|
||||
private static final AtomicInteger __custom = new AtomicInteger();
|
||||
private static final List<UriCompliance> KNOWN_MODES = List.of(DEFAULT, LEGACY, RFC3986, RFC3986_UNAMBIGUOUS, UNSAFE);
|
||||
private static final List<UriCompliance> KNOWN_MODES = List.of(DEFAULT, LEGACY, RFC3986, UNSAFE);
|
||||
|
||||
public static UriCompliance valueOf(String name)
|
||||
{
|
||||
|
|
|
@ -38,7 +38,7 @@ import org.eclipse.jetty.util.resource.Resource;
|
|||
public interface Context extends Attributes, Decorator, Executor
|
||||
{
|
||||
/**
|
||||
* @return the context path of this Context
|
||||
* @return the encoded context path of this Context
|
||||
*/
|
||||
String getContextPath();
|
||||
|
||||
|
@ -94,18 +94,46 @@ public interface Context extends Attributes, Decorator, Executor
|
|||
void run(Runnable task, Request request);
|
||||
|
||||
/**
|
||||
* <p>Returns a URI path scoped to this Context.</p>
|
||||
* <p>For example, if the context path is {@code /ctx} then a
|
||||
* full path of {@code /ctx/foo/bar} will return {@code /foo/bar}.</p>
|
||||
*
|
||||
* @param fullPath a full URI path
|
||||
* <p>Returns the URI path scoped to this Context.</p>
|
||||
* @see #getPathInContext(String, String)
|
||||
* @param canonicallyEncodedPath a full URI path that should be canonically encoded as
|
||||
* per {@link org.eclipse.jetty.util.URIUtil#canonicalPath(String)}
|
||||
* @return the URI path scoped to this Context, or {@code null} if the full path does not match this Context.
|
||||
* The empty string is returned if the full path is exactly the context path.
|
||||
*/
|
||||
String getPathInContext(String fullPath);
|
||||
default String getPathInContext(String canonicallyEncodedPath)
|
||||
{
|
||||
return getPathInContext(getContextPath(), canonicallyEncodedPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a non-{@code null} temporary directory, configured either for the context, the server or the JVM
|
||||
*/
|
||||
File getTempDirectory();
|
||||
|
||||
/**
|
||||
* <p>Returns the URI path scoped to the passed context path.</p>
|
||||
* <p>For example, if the context path passed is {@code /ctx} then a
|
||||
* path of {@code /ctx/foo/bar} will return {@code /foo/bar}.</p>
|
||||
*
|
||||
* @param encodedContextPath The context path that should be canonically encoded as
|
||||
* per {@link org.eclipse.jetty.util.URIUtil#canonicalPath(String)}.
|
||||
* @param encodedPath a full URI path that should be canonically encoded as
|
||||
* per {@link org.eclipse.jetty.util.URIUtil#canonicalPath(String)}.
|
||||
* @return the URI {@code encodedPath} scoped to the {@code encodedContextPath},
|
||||
* or {@code null} if the {@code encodedPath} does not match the context.
|
||||
* The empty string is returned if the {@code encodedPath} is exactly the {@code encodedContextPath}.
|
||||
*/
|
||||
static String getPathInContext(String encodedContextPath, String encodedPath)
|
||||
{
|
||||
if (encodedContextPath.length() == 0 || "/".equals(encodedContextPath))
|
||||
return encodedPath;
|
||||
if (encodedContextPath.length() > encodedPath.length() || !encodedPath.startsWith(encodedContextPath))
|
||||
return null;
|
||||
if (encodedPath.length() == encodedContextPath.length())
|
||||
return "";
|
||||
if (encodedPath.charAt(encodedContextPath.length()) != '/')
|
||||
return null;
|
||||
return encodedPath.substring(encodedContextPath.length());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,6 @@ import org.eclipse.jetty.http.HttpURI;
|
|||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.http.Trailers;
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.server.handler.ErrorHandler;
|
||||
import org.eclipse.jetty.server.internal.HttpChannelState;
|
||||
import org.eclipse.jetty.util.Attributes;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
|
@ -726,14 +725,14 @@ public interface Request extends Attributes, Content.Source
|
|||
* {@code newPathInContext=/newPath}, the returned HttpURI is {@code http://host/ctx/newPath?a=b}.</p>
|
||||
*
|
||||
* @param request The request to base the new HttpURI on.
|
||||
* @param newPathInContext The new path in context for the new HttpURI
|
||||
* @param newEncodedPathInContext The new path in context for the new HttpURI
|
||||
* @return A new immutable HttpURI with the path in context replaced, but query string and path
|
||||
* parameters retained.
|
||||
*/
|
||||
static HttpURI newHttpURIFrom(Request request, String newPathInContext)
|
||||
static HttpURI newHttpURIFrom(Request request, String newEncodedPathInContext)
|
||||
{
|
||||
return HttpURI.build(request.getHttpURI())
|
||||
.path(URIUtil.addPaths(getContextPath(request), newPathInContext))
|
||||
.path(URIUtil.addPaths(getContextPath(request), newEncodedPathInContext))
|
||||
.asImmutable();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -914,9 +914,9 @@ public class Server extends Handler.Wrapper implements Attributes
|
|||
}
|
||||
|
||||
@Override
|
||||
public String getPathInContext(String fullPath)
|
||||
public String getPathInContext(String canonicallyEncodedPath)
|
||||
{
|
||||
return fullPath;
|
||||
return canonicallyEncodedPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1297,17 +1297,9 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Grace
|
|||
}
|
||||
|
||||
@Override
|
||||
public String getPathInContext(String fullPath)
|
||||
public String getPathInContext(String canonicallyEncodedPath)
|
||||
{
|
||||
if (_rootContext)
|
||||
return fullPath;
|
||||
if (!fullPath.startsWith(_contextPath))
|
||||
return null;
|
||||
if (fullPath.length() == _contextPath.length())
|
||||
return "";
|
||||
if (fullPath.charAt(_contextPath.length()) != '/')
|
||||
return null;
|
||||
return fullPath.substring(_contextPath.length());
|
||||
return _rootContext ? canonicallyEncodedPath : Context.getPathInContext(_contextPath, canonicallyEncodedPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1390,7 +1390,7 @@ public class HttpConnectionTest
|
|||
Host: whatever
|
||||
|
||||
""";
|
||||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986_UNAMBIGUOUS);
|
||||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986);
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
|
||||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.UNSAFE);
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
|
||||
|
@ -1409,6 +1409,10 @@ public class HttpConnectionTest
|
|||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.LEGACY);
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
|
||||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986);
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
|
||||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.from(EnumSet.of(UriCompliance.Violation.AMBIGUOUS_PATH_PARAMETER)));
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
|
||||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.UNSAFE);
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
|
||||
}
|
||||
|
||||
|
@ -1425,7 +1429,7 @@ public class HttpConnectionTest
|
|||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.LEGACY);
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
|
||||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986);
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -1437,13 +1441,13 @@ public class HttpConnectionTest
|
|||
\r
|
||||
""";
|
||||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.DEFAULT);
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
|
||||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(new UriCompliance("Test", EnumSet.noneOf(UriCompliance.Violation.class)));
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
|
||||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.from("DEFAULT,AMBIGUOUS_PATH_SEPARATOR"));
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
|
||||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.LEGACY);
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
|
||||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986);
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -1476,11 +1480,13 @@ public class HttpConnectionTest
|
|||
\r
|
||||
""";
|
||||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.DEFAULT);
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
|
||||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.from(EnumSet.of(UriCompliance.Violation.AMBIGUOUS_PATH_ENCODING)));
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
|
||||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.LEGACY);
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
|
||||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986);
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
|
||||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.UNSAFE);
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
|
||||
|
||||
|
@ -1500,12 +1506,10 @@ public class HttpConnectionTest
|
|||
""";
|
||||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.DEFAULT);
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
|
||||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986_UNAMBIGUOUS);
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
|
||||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.LEGACY);
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
|
||||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986);
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
|
||||
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.UNSAFE);
|
||||
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
|
||||
}
|
||||
|
|
|
@ -88,9 +88,7 @@ public class RequestTest
|
|||
\r
|
||||
""";
|
||||
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
|
||||
assertEquals(HttpStatus.OK_200, response.getStatus());
|
||||
assertThat(response.getContent(), containsString("httpURI.path=/fo%6f%2fbar"));
|
||||
assertThat(response.getContent(), containsString("pathInContext=/foo%2Fbar"));
|
||||
assertEquals(HttpStatus.BAD_REQUEST_400, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -28,7 +28,6 @@ import java.util.Comparator;
|
|||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.StringTokenizer;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jetty.util.Utf8Appendable.NotUtf8Exception;
|
||||
|
@ -479,51 +478,6 @@ public final class URIUtil
|
|||
* @see #normalizePath(String)
|
||||
*/
|
||||
public static String decodePath(String path, int offset, int length)
|
||||
{
|
||||
return decodePath(path, offset, length, Utf8StringBuilder::append);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a percent-encoded URI path (assuming UTF-8 characters) and strips path parameters, but leaves reserved path characters percent-encoded.
|
||||
* @param path A String holding the URI path to decode
|
||||
* @see #canonicalPath(String)
|
||||
* @see #normalizePath(String)
|
||||
*/
|
||||
public static String safeDecodePath(String path)
|
||||
{
|
||||
return safeDecodePath(path, 0, path.length());
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a percent-encoded URI path (assuming UTF-8 characters) and strips path parameters, but leaves reserved path characters percent-encoded.
|
||||
* @param path A String holding the URI path to decode
|
||||
* @param offset The start of the URI within the path string
|
||||
* @param length The length of the URI within the path string
|
||||
* @see #canonicalPath(String)
|
||||
* @see #normalizePath(String)
|
||||
*/
|
||||
public static String safeDecodePath(String path, int offset, int length)
|
||||
{
|
||||
return decodePath(path, offset, length, URIUtil::safePathAppend);
|
||||
}
|
||||
|
||||
private static void safePathAppend(Utf8StringBuilder builder, byte b)
|
||||
{
|
||||
switch (b)
|
||||
{
|
||||
case '/' -> builder.append("%2F");
|
||||
case '%' -> builder.append("%25");
|
||||
case '?' -> builder.append("%3F");
|
||||
default -> builder.append(b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a URI path and strip parameters of UTF-8 path
|
||||
* @see #canonicalPath(String)
|
||||
* @see #normalizePath(String)
|
||||
*/
|
||||
public static String decodePath(String path, int offset, int length, BiConsumer<Utf8StringBuilder, Byte> decoder)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -550,13 +504,13 @@ public final class URIUtil
|
|||
String str = new String(codePoints, 0, 1);
|
||||
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
|
||||
for (byte b: bytes)
|
||||
decoder.accept(builder, b);
|
||||
builder.append(b);
|
||||
i += 5;
|
||||
}
|
||||
else
|
||||
{
|
||||
byte b = (byte)(0xff & (TypeUtil.convertHexDigit(u) * 16 + TypeUtil.convertHexDigit(path.charAt(i + 2))));
|
||||
decoder.accept(builder, b);
|
||||
builder.append(b);
|
||||
i += 2;
|
||||
}
|
||||
}
|
||||
|
@ -706,40 +660,41 @@ public final class URIUtil
|
|||
* <p>
|
||||
* For example the path {@code /fo %2fo/b%61r} will be normalized to {@code /fo%20%2Fo/bar},
|
||||
* whilst {@link #decodePath(String)} would result in the ambiguous and URI illegal {@code /fo /o/bar}.
|
||||
* @param encodedPath An encoded URI path
|
||||
* @return the canonical path or null if it is non-normal
|
||||
* @see #decodePath(String)
|
||||
* @see #normalizePath(String)
|
||||
* @see URI
|
||||
*/
|
||||
public static String canonicalPath(String path)
|
||||
public static String canonicalPath(String encodedPath)
|
||||
{
|
||||
if (path == null)
|
||||
if (encodedPath == null)
|
||||
return null;
|
||||
try
|
||||
{
|
||||
|
||||
Utf8StringBuilder builder = null;
|
||||
int end = path.length();
|
||||
int end = encodedPath.length();
|
||||
boolean slash = true;
|
||||
boolean normal = true;
|
||||
for (int i = 0; i < end; i++)
|
||||
{
|
||||
char c = path.charAt(i);
|
||||
char c = encodedPath.charAt(i);
|
||||
switch (c)
|
||||
{
|
||||
case '%':
|
||||
if (builder == null)
|
||||
{
|
||||
builder = new Utf8StringBuilder(path.length());
|
||||
builder.append(path, 0, i);
|
||||
builder = new Utf8StringBuilder(encodedPath.length());
|
||||
builder.append(encodedPath, 0, i);
|
||||
}
|
||||
if ((i + 2) < end)
|
||||
{
|
||||
char u = path.charAt(i + 1);
|
||||
char u = encodedPath.charAt(i + 1);
|
||||
if (u == 'u')
|
||||
{
|
||||
// UTF16 encoding is only supported with UriCompliance.Violation.UTF16_ENCODINGS.
|
||||
int code = TypeUtil.parseInt(path, i + 2, 4, 16);
|
||||
int code = TypeUtil.parseInt(encodedPath, i + 2, 4, 16);
|
||||
if (isSafeElseEncode(code, builder))
|
||||
{
|
||||
char[] chars = Character.toChars(code);
|
||||
|
@ -755,7 +710,7 @@ public final class URIUtil
|
|||
}
|
||||
else
|
||||
{
|
||||
int code = TypeUtil.convertHexDigit(u) * 16 + TypeUtil.convertHexDigit(path.charAt(i + 2));
|
||||
int code = TypeUtil.convertHexDigit(u) * 16 + TypeUtil.convertHexDigit(encodedPath.charAt(i + 2));
|
||||
if (isSafeElseEncode(code, builder))
|
||||
{
|
||||
builder.append((byte)(0xff & code));
|
||||
|
@ -774,13 +729,13 @@ public final class URIUtil
|
|||
case ';':
|
||||
if (builder == null)
|
||||
{
|
||||
builder = new Utf8StringBuilder(path.length());
|
||||
builder.append(path, 0, i);
|
||||
builder = new Utf8StringBuilder(encodedPath.length());
|
||||
builder.append(encodedPath, 0, i);
|
||||
}
|
||||
|
||||
while (++i < end)
|
||||
{
|
||||
if (path.charAt(i) == '/')
|
||||
if (encodedPath.charAt(i) == '/')
|
||||
{
|
||||
builder.append('/');
|
||||
break;
|
||||
|
@ -803,8 +758,8 @@ public final class URIUtil
|
|||
default:
|
||||
if (builder == null && !isSafe(c))
|
||||
{
|
||||
builder = new Utf8StringBuilder(path.length());
|
||||
builder.append(path, 0, i);
|
||||
builder = new Utf8StringBuilder(encodedPath.length());
|
||||
builder.append(encodedPath, 0, i);
|
||||
}
|
||||
|
||||
if (builder != null && isSafeElseEncode(c, builder))
|
||||
|
@ -815,13 +770,13 @@ public final class URIUtil
|
|||
slash = c == '/';
|
||||
}
|
||||
|
||||
String canonical = (builder != null) ? builder.toString() : path;
|
||||
String canonical = (builder != null) ? builder.toString() : encodedPath;
|
||||
return normal ? canonical : normalizePath(canonical);
|
||||
}
|
||||
catch (NotUtf8Exception e)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("{} {}", path, e.toString());
|
||||
LOG.debug("{} {}", encodedPath, e.toString());
|
||||
throw e;
|
||||
}
|
||||
catch (IllegalArgumentException e)
|
||||
|
|
|
@ -16,7 +16,6 @@ package org.eclipse.jetty.util;
|
|||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.text.Normalizer;
|
||||
|
@ -37,8 +36,6 @@ import org.junit.jupiter.params.ParameterizedTest;
|
|||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
|
@ -95,60 +92,79 @@ public class URIUtilTest
|
|||
|
||||
public static Stream<Arguments> decodePathSource()
|
||||
{
|
||||
List<Arguments> arguments = new ArrayList<>();
|
||||
arguments.add(Arguments.of("/foo/bar", "/foo/bar", "/foo/bar"));
|
||||
return Stream.of(
|
||||
Arguments.of("/foo/bar", "/foo/bar", "/foo/bar"),
|
||||
|
||||
// Simple encoding
|
||||
arguments.add(Arguments.of("/f%20%6f/b%20r", "/f%20o/b%20r", "/f o/b r"));
|
||||
Arguments.of("/f%20%6f/b%20r", "/f%20o/b%20r", "/f o/b r"),
|
||||
|
||||
// UTF8 and unicode handling
|
||||
// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
|
||||
arguments.add(Arguments.of("/foo/b\u00e4\u00e4", "/foo/b\u00e4\u00e4", "/foo/b\u00e4\u00e4"));
|
||||
arguments.add(Arguments.of("/f%d8%a9%D8%A9/bar", "/f\u0629\u0629/bar", "/f\u0629\u0629/bar"));
|
||||
Arguments.of("/foo/b\u00e4\u00e4", "/foo/bää", "/foo/bää"),
|
||||
Arguments.of("/f\u20ac\u20ac/bar", "/f€€/bar", "/f€€/bar"),
|
||||
Arguments.of("/f%d8%a9%D8%A9/bar", "/f\u0629\u0629/bar", "/f\u0629\u0629/bar"),
|
||||
|
||||
// Encoded UTF-8 unicode (euro)
|
||||
Arguments.of("/f%e2%82%ac%E2%82%AC/bar", "/f€€/bar", "/f€€/bar"),
|
||||
|
||||
// Encoded delimiters
|
||||
arguments.add(Arguments.of("/foo%2fbar", "/foo%2Fbar", "/foo/bar"));
|
||||
arguments.add(Arguments.of("/foo%252fbar", "/foo%252fbar", "/foo%2fbar"));
|
||||
arguments.add(Arguments.of("/foo%3bbar", "/foo%3Bbar", "/foo;bar"));
|
||||
arguments.add(Arguments.of("/foo%3fbar", "/foo%3Fbar", "/foo?bar"));
|
||||
Arguments.of("/foo%2fbar", "/foo%2Fbar", "/foo/bar"),
|
||||
Arguments.of("/foo%252fbar", "/foo%252fbar", "/foo%2fbar"),
|
||||
Arguments.of("/foo%3bbar", "/foo%3Bbar", "/foo;bar"),
|
||||
Arguments.of("/foo%3fbar", "/foo%3Fbar", "/foo?bar"),
|
||||
|
||||
// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
|
||||
arguments.add(Arguments.of("/f%20o/b%20r", "/f%20o/b%20r", "/f o/b r"));
|
||||
arguments.add(Arguments.of("f\u00e4\u00e4%2523%3b%2c:%3db%20a%20r%3D", "f\u00e4\u00e4%2523%3B,:=b%20a%20r=", "f\u00e4\u00e4%23;,:=b a r="));
|
||||
arguments.add(Arguments.of("f%d8%a9%D8%A9%2523%3b%2c:%3db%20a%20r", "f\u0629\u0629%2523%3B,:=b%20a%20r", "f\u0629\u0629%23;,:=b a r"));
|
||||
Arguments.of("/f%20o/b%20r", "/f%20o/b%20r", "/f o/b r"),
|
||||
Arguments.of("f\u00e4\u00e4%2523%3b%2c:%3db%20a%20r%3D", "f\u00e4\u00e4%2523%3B,:=b%20a%20r=", "f\u00e4\u00e4%23;,:=b a r="),
|
||||
Arguments.of("f%d8%a9%D8%A9%2523%3b%2c:%3db%20a%20r", "f\u0629\u0629%2523%3B,:=b%20a%20r", "f\u0629\u0629%23;,:=b a r"),
|
||||
|
||||
// path parameters should be ignored
|
||||
arguments.add(Arguments.of("/foo;ignore/bar;ignore", "/foo/bar", "/foo/bar"));
|
||||
arguments.add(Arguments.of("/f\u00e4\u00e4;ignore/bar;ignore", "/fää/bar", "/fää/bar"));
|
||||
arguments.add(Arguments.of("/f%d8%a9%d8%a9%2523;ignore/bar;ignore", "/f\u0629\u0629%2523/bar", "/f\u0629\u0629%23/bar"));
|
||||
arguments.add(Arguments.of("foo%2523%3b%2c:%3db%20a%20r;rubbish", "foo%2523%3B,:=b%20a%20r", "foo%23;,:=b a r"));
|
||||
Arguments.of("/foo;ignore/bar;ignore", "/foo/bar", "/foo/bar"),
|
||||
Arguments.of("/f\u00e4\u00e4;ignore/bar;ignore", "/fää/bar", "/fää/bar"),
|
||||
Arguments.of("/f%d8%a9%d8%a9%2523;ignore/bar;ignore", "/f\u0629\u0629%2523/bar", "/f\u0629\u0629%23/bar"),
|
||||
Arguments.of("foo%2523%3b%2c:%3db%20a%20r;rubbish", "foo%2523%3B,:=b%20a%20r", "foo%23;,:=b a r"),
|
||||
|
||||
// test for chars that are somehow already decoded, but shouldn't be
|
||||
arguments.add(Arguments.of("/foo bar\n", "/foo%20bar%0A", "/foo bar\n"));
|
||||
arguments.add(Arguments.of("/foo\u0000bar", "/foo%00bar", "/foo\u0000bar"));
|
||||
arguments.add(Arguments.of("/foo/bär", "/foo/bär", "/foo/bär"));
|
||||
arguments.add(Arguments.of("/foo/€/bar", "/foo/€/bar", "/foo/€/bar"));
|
||||
arguments.add(Arguments.of("/fo %2fo/b%61r", "/fo%20%2Fo/bar", "/fo /o/bar"));
|
||||
Arguments.of("/foo bar\n", "/foo%20bar%0A", "/foo bar\n"),
|
||||
Arguments.of("/foo\u0000bar", "/foo%00bar", "/foo\u0000bar"),
|
||||
Arguments.of("/foo/bär", "/foo/bär", "/foo/bär"),
|
||||
Arguments.of("/foo/€/bar", "/foo/€/bar", "/foo/€/bar"),
|
||||
Arguments.of("/fo %2fo/b%61r", "/fo%20%2Fo/bar", "/fo /o/bar"),
|
||||
|
||||
// Test for null character (real world ugly test case)
|
||||
byte[] oddBytes = {'/', 0x00, '/'};
|
||||
String odd = new String(oddBytes, StandardCharsets.ISO_8859_1);
|
||||
arguments.add(Arguments.of("/%00/", "/%00/", odd));
|
||||
Arguments.of("/%00/", "/%00/", "/\u0000/"),
|
||||
|
||||
// Deprecated Microsoft Percent-U encoding
|
||||
arguments.add(Arguments.of("abc%u3040", "abc\u3040", "abc\u3040"));
|
||||
Arguments.of("abc%u3040", "abc\u3040", "abc\u3040"),
|
||||
|
||||
// Invalid UTF-8 - replacement characters should be present on invalid sequences
|
||||
// URI paths do not support ISO-8859-1, so this should not be a fallback of our decodePath implementation
|
||||
/* TODO: remove ISO-8859-1 fallback mode in decodePath - Issue #9489
|
||||
Arguments.of("/a%D8%2fbar", "/a<>%2Fbar", "/a<>%2Fbar"), // invalid 2 octet sequence
|
||||
Arguments.of("/abc%C3%28", "/abc<62>", "/abc<62>"), // invalid 2 octet sequence
|
||||
Arguments.of("/abc%A0%A1", "/abc<62><63>", "/abc<62><63>"), // invalid 2 octet sequence
|
||||
Arguments.of("/abc%e2%28%a1", "/abc<62><63>", "/abc<62><63>"), // invalid 3 octet sequence
|
||||
Arguments.of("/abc%e2%82%28", "/abc<62>", "/abc<62>"), // invalid 3 octet sequence
|
||||
Arguments.of("/abc%f0%28%8c%bc", "/abc<62><63><EFBFBD>", "/abc<62><63><EFBFBD>"), // invalid 4 octet sequence
|
||||
Arguments.of("/abc%f0%90%28%bc", "/abc<62><63>", "/abc<62><63>"), // invalid 4 octet sequence
|
||||
Arguments.of("/abc%f0%28%8c%28", "/abc<62><63>(", "/abc<62><63>("), // invalid 4 octet sequence
|
||||
Arguments.of("/abc%f8%a1%a1%a1%a1", "/abc<62><63><EFBFBD><EFBFBD><EFBFBD>", "/abc<62><63><EFBFBD><EFBFBD><EFBFBD>"), // valid sequence, but not unicode
|
||||
Arguments.of("/abc%fc%a1%a1%a1%a1%a1", "/abc<62><63><EFBFBD><EFBFBD><EFBFBD><EFBFBD>", "/abc<62><63><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"), // valid sequence, but not unicode
|
||||
Arguments.of("/abc%f8%a1%a1%a1", "/abc<62><63><EFBFBD><EFBFBD>", "/abc<62><63><EFBFBD><EFBFBD>"), // incomplete sequence
|
||||
*/
|
||||
|
||||
// Deprecated Microsoft Percent-U encoding
|
||||
Arguments.of("/abc%u3040", "/abc\u3040", "/abc\u3040"),
|
||||
|
||||
// Canonical paths are also normalized
|
||||
arguments.add(Arguments.of("./bar", "bar", "./bar"));
|
||||
arguments.add(Arguments.of("/foo/./bar", "/foo/bar", "/foo/./bar"));
|
||||
arguments.add(Arguments.of("/foo/../bar", "/bar", "/foo/../bar"));
|
||||
arguments.add(Arguments.of("/foo/.../bar", "/foo/.../bar", "/foo/.../bar"));
|
||||
arguments.add(Arguments.of("/foo/%2e/bar", "/foo/bar", "/foo/./bar")); // Not by the RFC, but safer
|
||||
arguments.add(Arguments.of("/foo/%2e%2e/bar", "/bar", "/foo/../bar")); // Not by the RFC, but safer
|
||||
arguments.add(Arguments.of("/foo/%2e%2e%2e/bar", "/foo/.../bar", "/foo/.../bar"));
|
||||
|
||||
return arguments.stream();
|
||||
|
||||
Arguments.of("./bar", "bar", "./bar"),
|
||||
Arguments.of("/foo/./bar", "/foo/bar", "/foo/./bar"),
|
||||
Arguments.of("/foo/../bar", "/bar", "/foo/../bar"),
|
||||
Arguments.of("/foo/.../bar", "/foo/.../bar", "/foo/.../bar"),
|
||||
Arguments.of("/foo/%2e/bar", "/foo/bar", "/foo/./bar"), // Not by the RFC, but safer
|
||||
Arguments.of("/foo/%2e%2e/bar", "/bar", "/foo/../bar"), // Not by the RFC, but safer
|
||||
Arguments.of("/foo/%2e%2e%2e/bar", "/foo/.../bar", "/foo/.../bar")
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "[{index}] {0}")
|
||||
|
@ -171,14 +187,6 @@ public class URIUtilTest
|
|||
{
|
||||
List<Arguments> arguments = new ArrayList<>();
|
||||
|
||||
// Test for null character (real world ugly test case)
|
||||
// TODO is this a bad decoding or a bad URI ?
|
||||
// arguments.add(Arguments.of("/%00/"));
|
||||
|
||||
// Deprecated Microsoft Percent-U encoding
|
||||
// TODO still supported for now ?
|
||||
// arguments.add(Arguments.of("abc%u3040"));
|
||||
|
||||
// Bad %## encoding
|
||||
arguments.add(Arguments.of("abc%xyz"));
|
||||
|
||||
|
@ -196,21 +204,6 @@ public class URIUtilTest
|
|||
arguments.add(Arguments.of("abc%uA"));
|
||||
arguments.add(Arguments.of("abc%u"));
|
||||
|
||||
// Invalid UTF-8 and ISO8859-1
|
||||
// TODO currently ISO8859 is too forgiving to detect these
|
||||
/*
|
||||
arguments.add(Arguments.of("abc%C3%28")); // invalid 2 octext sequence
|
||||
arguments.add(Arguments.of("abc%A0%A1")); // invalid 2 octext sequence
|
||||
arguments.add(Arguments.of("abc%e2%28%a1")); // invalid 3 octext sequence
|
||||
arguments.add(Arguments.of("abc%e2%82%28")); // invalid 3 octext sequence
|
||||
arguments.add(Arguments.of("abc%f0%28%8c%bc")); // invalid 4 octext sequence
|
||||
arguments.add(Arguments.of("abc%f0%90%28%bc")); // invalid 4 octext sequence
|
||||
arguments.add(Arguments.of("abc%f0%28%8c%28")); // invalid 4 octext sequence
|
||||
arguments.add(Arguments.of("abc%f8%a1%a1%a1%a1")); // valid sequence, but not unicode
|
||||
arguments.add(Arguments.of("abc%fc%a1%a1%a1%a1%a1")); // valid sequence, but not unicode
|
||||
arguments.add(Arguments.of("abc%f8%a1%a1%a1")); // incomplete sequence
|
||||
*/
|
||||
|
||||
return arguments.stream();
|
||||
}
|
||||
|
||||
|
|
|
@ -359,18 +359,22 @@ public class DefaultServlet extends HttpServlet
|
|||
{
|
||||
String includedServletPath = (String)req.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
|
||||
boolean included = includedServletPath != null;
|
||||
String pathInContext;
|
||||
String encodedPathInContext;
|
||||
if (included)
|
||||
pathInContext = getIncludedPathInContext(req, includedServletPath, isPathInfoOnly());
|
||||
encodedPathInContext = URIUtil.encodePath(getIncludedPathInContext(req, includedServletPath, isPathInfoOnly()));
|
||||
else if (isPathInfoOnly())
|
||||
encodedPathInContext = URIUtil.encodePath(req.getPathInfo());
|
||||
else if (req instanceof ServletApiRequest apiRequest)
|
||||
encodedPathInContext = Context.getPathInContext(req.getContextPath(), apiRequest.getServletContextRequest().getHttpURI().getCanonicalPath());
|
||||
else
|
||||
pathInContext = URIUtil.addPaths(isPathInfoOnly() ? "/" : req.getServletPath(), req.getPathInfo());
|
||||
encodedPathInContext = Context.getPathInContext(req.getContextPath(), URIUtil.canonicalPath(req.getRequestURI()));
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("doGet(req={}, resp={}) pathInContext={}, included={}", req, resp, pathInContext, included);
|
||||
LOG.debug("doGet(req={}, resp={}) pathInContext={}, included={}", req, resp, encodedPathInContext, included);
|
||||
|
||||
try
|
||||
{
|
||||
HttpContent content = _resourceService.getContent(pathInContext, ServletContextRequest.getServletContextRequest(req));
|
||||
HttpContent content = _resourceService.getContent(encodedPathInContext, ServletContextRequest.getServletContextRequest(req));
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("content = {}", content);
|
||||
|
||||
|
@ -384,7 +388,7 @@ public class DefaultServlet extends HttpServlet
|
|||
* If the exception isn’t caught and handled, and the response
|
||||
* hasn’t been committed, the status code MUST be set to 500.
|
||||
*/
|
||||
throw new FileNotFoundException(pathInContext);
|
||||
throw new FileNotFoundException(encodedPathInContext);
|
||||
}
|
||||
|
||||
// no content
|
||||
|
@ -432,9 +436,9 @@ public class DefaultServlet extends HttpServlet
|
|||
catch (InvalidPathException e)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("InvalidPathException for pathInContext: {}", pathInContext, e);
|
||||
LOG.debug("InvalidPathException for pathInContext: {}", encodedPathInContext, e);
|
||||
if (included)
|
||||
throw new FileNotFoundException(pathInContext);
|
||||
throw new FileNotFoundException(encodedPathInContext);
|
||||
resp.setStatus(404);
|
||||
}
|
||||
}
|
||||
|
@ -481,9 +485,9 @@ public class DefaultServlet extends HttpServlet
|
|||
if (request.getDispatcherType() == DispatcherType.REQUEST)
|
||||
_uri = getWrapped().getHttpURI();
|
||||
else if (included)
|
||||
_uri = Request.newHttpURIFrom(getWrapped(), getIncludedPathInContext(request, includedServletPath, false));
|
||||
_uri = Request.newHttpURIFrom(getWrapped(), URIUtil.encodePath(getIncludedPathInContext(request, includedServletPath, false)));
|
||||
else
|
||||
_uri = Request.newHttpURIFrom(getWrapped(), URIUtil.addPaths(_servletRequest.getServletPath(), _servletRequest.getPathInfo()));
|
||||
_uri = Request.newHttpURIFrom(getWrapped(), URIUtil.encodePath(URIUtil.addPaths(_servletRequest.getServletPath(), _servletRequest.getPathInfo())));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -35,7 +35,6 @@ import jakarta.servlet.http.HttpServletResponseWrapper;
|
|||
import org.eclipse.jetty.ee10.servlet.util.ServletOutputStreamWrapper;
|
||||
import org.eclipse.jetty.http.HttpURI;
|
||||
import org.eclipse.jetty.http.pathmap.MatchedResource;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.util.Fields;
|
||||
import org.eclipse.jetty.util.MultiMap;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
|
@ -64,30 +63,30 @@ public class Dispatcher implements RequestDispatcher
|
|||
|
||||
private final ServletContextHandler _contextHandler;
|
||||
private final HttpURI _uri;
|
||||
private final String _pathInContext;
|
||||
private final String _decodedPathInContext;
|
||||
private final String _named;
|
||||
private final ServletHandler.MappedServlet _mappedServlet;
|
||||
private final ServletHandler _servletHandler;
|
||||
private final ServletPathMapping _servletPathMapping;
|
||||
|
||||
public Dispatcher(ServletContextHandler contextHandler, HttpURI uri, String pathInContext)
|
||||
public Dispatcher(ServletContextHandler contextHandler, HttpURI uri, String decodedPathInContext)
|
||||
{
|
||||
_contextHandler = contextHandler;
|
||||
_uri = uri.asImmutable();
|
||||
_pathInContext = pathInContext;
|
||||
_decodedPathInContext = decodedPathInContext;
|
||||
_named = null;
|
||||
|
||||
_servletHandler = _contextHandler.getServletHandler();
|
||||
MatchedResource<ServletHandler.MappedServlet> matchedServlet = _servletHandler.getMatchedServlet(pathInContext);
|
||||
MatchedResource<ServletHandler.MappedServlet> matchedServlet = _servletHandler.getMatchedServlet(decodedPathInContext);
|
||||
_mappedServlet = matchedServlet.getResource();
|
||||
_servletPathMapping = _mappedServlet.getServletPathMapping(_pathInContext, matchedServlet.getMatchedPath());
|
||||
_servletPathMapping = _mappedServlet.getServletPathMapping(_decodedPathInContext, matchedServlet.getMatchedPath());
|
||||
}
|
||||
|
||||
public Dispatcher(ServletContextHandler contextHandler, String name) throws IllegalStateException
|
||||
{
|
||||
_contextHandler = contextHandler;
|
||||
_uri = null;
|
||||
_pathInContext = null;
|
||||
_decodedPathInContext = null;
|
||||
_named = name;
|
||||
|
||||
_servletHandler = _contextHandler.getServletHandler();
|
||||
|
@ -100,7 +99,7 @@ public class Dispatcher implements RequestDispatcher
|
|||
HttpServletRequest httpRequest = (request instanceof HttpServletRequest) ? (HttpServletRequest)request : new ServletRequestHttpWrapper(request);
|
||||
HttpServletResponse httpResponse = (response instanceof HttpServletResponse) ? (HttpServletResponse)response : new ServletResponseHttpWrapper(response);
|
||||
|
||||
_mappedServlet.handle(_servletHandler, _pathInContext, new ErrorRequest(httpRequest), httpResponse);
|
||||
_mappedServlet.handle(_servletHandler, _decodedPathInContext, new ErrorRequest(httpRequest), httpResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -111,7 +110,7 @@ public class Dispatcher implements RequestDispatcher
|
|||
|
||||
ServletContextRequest servletContextRequest = ServletContextRequest.getServletContextRequest(request);
|
||||
servletContextRequest.getResponse().resetForForward();
|
||||
_mappedServlet.handle(_servletHandler, _pathInContext, new ForwardRequest(httpRequest), httpResponse);
|
||||
_mappedServlet.handle(_servletHandler, _decodedPathInContext, new ForwardRequest(httpRequest), httpResponse);
|
||||
|
||||
// If we are not async and not closed already, then close via the possibly wrapped response.
|
||||
if (!servletContextRequest.getState().isAsync() && !servletContextRequest.getHttpOutput().isClosed())
|
||||
|
@ -136,7 +135,7 @@ public class Dispatcher implements RequestDispatcher
|
|||
|
||||
try
|
||||
{
|
||||
_mappedServlet.handle(_servletHandler, _pathInContext, new IncludeRequest(httpRequest), new IncludeResponse(httpResponse));
|
||||
_mappedServlet.handle(_servletHandler, _decodedPathInContext, new IncludeRequest(httpRequest), new IncludeResponse(httpResponse));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
@ -149,7 +148,7 @@ public class Dispatcher implements RequestDispatcher
|
|||
HttpServletRequest httpRequest = (request instanceof HttpServletRequest) ? (HttpServletRequest)request : new ServletRequestHttpWrapper(request);
|
||||
HttpServletResponse httpResponse = (response instanceof HttpServletResponse) ? (HttpServletResponse)response : new ServletResponseHttpWrapper(response);
|
||||
|
||||
_mappedServlet.handle(_servletHandler, _pathInContext, new AsyncRequest(httpRequest), httpResponse);
|
||||
_mappedServlet.handle(_servletHandler, _decodedPathInContext, new AsyncRequest(httpRequest), httpResponse);
|
||||
}
|
||||
|
||||
public class ParameterRequestWrapper extends HttpServletRequestWrapper
|
||||
|
|
|
@ -60,6 +60,7 @@ import org.eclipse.jetty.ee10.servlet.security.UserIdentity;
|
|||
import org.eclipse.jetty.http.BadMessageException;
|
||||
import org.eclipse.jetty.http.CookieCompliance;
|
||||
import org.eclipse.jetty.http.HttpCookie;
|
||||
import org.eclipse.jetty.http.HttpException;
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
|
@ -1117,7 +1118,7 @@ public class ServletApiRequest implements HttpServletRequest
|
|||
// handle relative path
|
||||
if (!path.startsWith("/"))
|
||||
{
|
||||
String relTo = _request.getPathInContext();
|
||||
String relTo = _request.getDecodedPathInContext();
|
||||
int slash = relTo.lastIndexOf("/");
|
||||
if (slash > 1)
|
||||
relTo = relTo.substring(0, slash + 1);
|
||||
|
@ -1195,7 +1196,7 @@ public class ServletApiRequest implements HttpServletRequest
|
|||
@Override
|
||||
public HttpServletMapping getHttpServletMapping()
|
||||
{
|
||||
return _request._mappedServlet.getServletPathMapping(_request.getPathInContext());
|
||||
return _request._mappedServlet.getServletPathMapping(_request.getDecodedPathInContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1246,4 +1247,24 @@ public class ServletApiRequest implements HttpServletRequest
|
|||
}
|
||||
return trailersMap;
|
||||
}
|
||||
|
||||
static class AmbiguousURI extends ServletApiRequest
|
||||
{
|
||||
protected AmbiguousURI(ServletContextRequest servletContextRequest)
|
||||
{
|
||||
super(servletContextRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPathInfo()
|
||||
{
|
||||
throw new HttpException.IllegalArgumentException(HttpStatus.BAD_REQUEST_400, "Ambiguous URI encoding");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServletPath()
|
||||
{
|
||||
throw new HttpException.IllegalArgumentException(HttpStatus.BAD_REQUEST_400, "Ambiguous URI encoding");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -461,9 +461,9 @@ public class ServletChannel
|
|||
}
|
||||
}
|
||||
// We first worked with the core pathInContext above, but now need to convert to servlet style
|
||||
pathInContext = URIUtil.safeDecodePath(pathInContext);
|
||||
String decodedPathInContext = URIUtil.decodePath(pathInContext);
|
||||
|
||||
Dispatcher dispatcher = new Dispatcher(getContextHandler(), uri, pathInContext);
|
||||
Dispatcher dispatcher = new Dispatcher(getContextHandler(), uri, decodedPathInContext);
|
||||
dispatcher.async(asyncContextEvent.getSuppliedRequest(), asyncContextEvent.getSuppliedResponse());
|
||||
});
|
||||
break;
|
||||
|
|
|
@ -1150,10 +1150,10 @@ public class ServletContextHandler extends ContextHandler implements Graceful
|
|||
protected ServletContextRequest newServletContextRequest(ServletChannel servletChannel,
|
||||
Request request,
|
||||
Response response,
|
||||
String pathInContext,
|
||||
String decodedPathInContext,
|
||||
MatchedResource<ServletHandler.MappedServlet> matchedResource)
|
||||
{
|
||||
return new ServletContextRequest(_servletContext, servletChannel, request, response, pathInContext, matchedResource, getSessionHandler());
|
||||
return new ServletContextRequest(_servletContext, servletChannel, request, response, decodedPathInContext, matchedResource, getSessionHandler());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1161,9 +1161,9 @@ public class ServletContextHandler extends ContextHandler implements Graceful
|
|||
{
|
||||
// Need to ask directly to the Context for the pathInContext, rather than using
|
||||
// Request.getPathInContext(), as the request is not yet wrapped in this Context.
|
||||
String pathInContext = URIUtil.safeDecodePath(getContext().getPathInContext(request.getHttpURI().getCanonicalPath()));
|
||||
String decodedPathInContext = URIUtil.decodePath(getContext().getPathInContext(request.getHttpURI().getCanonicalPath()));
|
||||
|
||||
MatchedResource<ServletHandler.MappedServlet> matchedResource = _servletHandler.getMatchedServlet(pathInContext);
|
||||
MatchedResource<ServletHandler.MappedServlet> matchedResource = _servletHandler.getMatchedServlet(decodedPathInContext);
|
||||
if (matchedResource == null)
|
||||
return null;
|
||||
ServletHandler.MappedServlet mappedServlet = matchedResource.getResource();
|
||||
|
@ -1179,7 +1179,7 @@ public class ServletContextHandler extends ContextHandler implements Graceful
|
|||
cache.setAttribute(ServletChannel.class.getName(), servletChannel);
|
||||
}
|
||||
|
||||
ServletContextRequest servletContextRequest = newServletContextRequest(servletChannel, request, response, pathInContext, matchedResource);
|
||||
ServletContextRequest servletContextRequest = newServletContextRequest(servletChannel, request, response, decodedPathInContext, matchedResource);
|
||||
servletChannel.associate(servletContextRequest);
|
||||
return servletContextRequest;
|
||||
}
|
||||
|
@ -1197,7 +1197,7 @@ public class ServletContextHandler extends ContextHandler implements Graceful
|
|||
{
|
||||
ServletContextRequest scopedRequest = Request.as(request, ServletContextRequest.class);
|
||||
DispatcherType dispatch = scopedRequest.getHttpServletRequest().getDispatcherType();
|
||||
if (dispatch == DispatcherType.REQUEST && isProtectedTarget(scopedRequest.getPathInContext()))
|
||||
if (dispatch == DispatcherType.REQUEST && isProtectedTarget(scopedRequest.getDecodedPathInContext()))
|
||||
{
|
||||
Response.writeError(request, response, callback, HttpServletResponse.SC_NOT_FOUND, null);
|
||||
return true;
|
||||
|
@ -2824,16 +2824,16 @@ public class ServletContextHandler extends ContextHandler implements Graceful
|
|||
String contextPath = getContextPath();
|
||||
// uriInContext is canonicalized by HttpURI.
|
||||
HttpURI.Mutable uri = HttpURI.build(uriInContext);
|
||||
String pathInfo = uri.getCanonicalPath();
|
||||
if (StringUtil.isEmpty(pathInfo))
|
||||
String encodedPathInContext = uri.getCanonicalPath();
|
||||
if (StringUtil.isEmpty(encodedPathInContext))
|
||||
return null;
|
||||
|
||||
if (!StringUtil.isEmpty(contextPath))
|
||||
{
|
||||
uri.path(URIUtil.addPaths(contextPath, uri.getPath()));
|
||||
pathInfo = uri.getCanonicalPath().substring(contextPath.length());
|
||||
encodedPathInContext = uri.getCanonicalPath().substring(contextPath.length());
|
||||
}
|
||||
return new Dispatcher(ServletContextHandler.this, uri, pathInfo);
|
||||
return new Dispatcher(ServletContextHandler.this, uri, URIUtil.decodePath(encodedPathInContext));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
|
@ -27,6 +27,7 @@ import jakarta.servlet.http.HttpServletResponse;
|
|||
import org.eclipse.jetty.http.HttpCookie;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.UriCompliance;
|
||||
import org.eclipse.jetty.http.pathmap.MatchedPath;
|
||||
import org.eclipse.jetty.http.pathmap.MatchedResource;
|
||||
import org.eclipse.jetty.http.pathmap.PathSpec;
|
||||
|
@ -39,13 +40,10 @@ import org.eclipse.jetty.session.AbstractSessionManager;
|
|||
import org.eclipse.jetty.session.ManagedSession;
|
||||
import org.eclipse.jetty.session.SessionManager;
|
||||
import org.eclipse.jetty.util.Fields;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class ServletContextRequest extends ContextRequest
|
||||
{
|
||||
public static final String MULTIPART_CONFIG_ELEMENT = "org.eclipse.jetty.multipartConfig";
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ServletContextRequest.class);
|
||||
static final int INPUT_NONE = 0;
|
||||
static final int INPUT_STREAM = 1;
|
||||
static final int INPUT_READER = 2;
|
||||
|
@ -78,7 +76,7 @@ public class ServletContextRequest extends ContextRequest
|
|||
private final ServletContextResponse _response;
|
||||
final ServletHandler.MappedServlet _mappedServlet;
|
||||
private final HttpInput _httpInput;
|
||||
private final String _pathInContext;
|
||||
private final String _decodedPathInContext;
|
||||
private final ServletChannel _servletChannel;
|
||||
private final PathSpec _pathSpec;
|
||||
private final SessionManager _sessionManager;
|
||||
|
@ -93,7 +91,7 @@ public class ServletContextRequest extends ContextRequest
|
|||
ServletChannel servletChannel,
|
||||
Request request,
|
||||
Response response,
|
||||
String pathInContext,
|
||||
String decodedPathInContext,
|
||||
MatchedResource<ServletHandler.MappedServlet> matchedResource,
|
||||
SessionManager sessionManager)
|
||||
{
|
||||
|
@ -102,7 +100,7 @@ public class ServletContextRequest extends ContextRequest
|
|||
_httpServletRequest = newServletApiRequest();
|
||||
_mappedServlet = matchedResource.getResource();
|
||||
_httpInput = _servletChannel.getHttpInput();
|
||||
_pathInContext = pathInContext;
|
||||
_decodedPathInContext = decodedPathInContext;
|
||||
_pathSpec = matchedResource.getPathSpec();
|
||||
_matchedPath = matchedResource.getMatchedPath();
|
||||
_response = newServletContextResponse(response);
|
||||
|
@ -111,6 +109,15 @@ public class ServletContextRequest extends ContextRequest
|
|||
|
||||
protected ServletApiRequest newServletApiRequest()
|
||||
{
|
||||
if (getHttpURI().hasViolations() && !getServletChannel().getContextHandler().getServletHandler().isDecodeAmbiguousURIs())
|
||||
{
|
||||
for (UriCompliance.Violation violation : getHttpURI().getViolations())
|
||||
{
|
||||
if (UriCompliance.AMBIGUOUS_VIOLATIONS.contains(violation))
|
||||
return new ServletApiRequest.AmbiguousURI(this);
|
||||
}
|
||||
}
|
||||
|
||||
return new ServletApiRequest(this);
|
||||
}
|
||||
|
||||
|
@ -119,9 +126,9 @@ public class ServletContextRequest extends ContextRequest
|
|||
return new ServletContextResponse(_servletChannel, this, response);
|
||||
}
|
||||
|
||||
public String getPathInContext()
|
||||
public String getDecodedPathInContext()
|
||||
{
|
||||
return _pathInContext;
|
||||
return _decodedPathInContext;
|
||||
}
|
||||
|
||||
public PathSpec getPathSpec()
|
||||
|
@ -209,8 +216,8 @@ public class ServletContextRequest extends ContextRequest
|
|||
{
|
||||
case "o.e.j.s.s.ServletScopedRequest.request" -> _httpServletRequest;
|
||||
case "o.e.j.s.s.ServletScopedRequest.response" -> _response.getHttpServletResponse();
|
||||
case "o.e.j.s.s.ServletScopedRequest.servlet" -> _mappedServlet.getServletPathMapping(getPathInContext()).getServletName();
|
||||
case "o.e.j.s.s.ServletScopedRequest.url-pattern" -> _mappedServlet.getServletPathMapping(getPathInContext()).getPattern();
|
||||
case "o.e.j.s.s.ServletScopedRequest.servlet" -> _mappedServlet.getServletPathMapping(getDecodedPathInContext()).getServletName();
|
||||
case "o.e.j.s.s.ServletScopedRequest.url-pattern" -> _mappedServlet.getServletPathMapping(getDecodedPathInContext()).getPattern();
|
||||
default -> super.getAttribute(name);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -116,6 +116,7 @@ public class ServletHandler extends Handler.Wrapper
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected final ConcurrentMap<String, FilterChain>[] _chainCache = new ConcurrentMap[FilterMapping.ALL];
|
||||
private boolean _decodeAmbiguousURIs = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
|
@ -124,6 +125,21 @@ public class ServletHandler extends Handler.Wrapper
|
|||
{
|
||||
}
|
||||
|
||||
@ManagedAttribute(value = "True if URIs with violations are decoded")
|
||||
public boolean isDecodeAmbiguousURIs()
|
||||
{
|
||||
return _decodeAmbiguousURIs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param decodeAmbiguousURIs {@code True} if ambiguous URIs are decoded by {@link ServletApiRequest#getServletPath()}
|
||||
* and {@link ServletApiRequest#getPathInfo()}.
|
||||
*/
|
||||
public void setDecodeAmbiguousURIs(boolean decodeAmbiguousURIs)
|
||||
{
|
||||
_decodeAmbiguousURIs = decodeAmbiguousURIs;
|
||||
}
|
||||
|
||||
AutoLock lock()
|
||||
{
|
||||
return _lock.lock();
|
||||
|
|
|
@ -471,10 +471,10 @@ public abstract class SecurityHandler extends Handler.Wrapper implements Authent
|
|||
if (authenticator != null)
|
||||
authenticator.prepareRequest(request);
|
||||
|
||||
RoleInfo roleInfo = prepareConstraintInfo(servletContextRequest.getPathInContext(), servletApiRequest);
|
||||
RoleInfo roleInfo = prepareConstraintInfo(servletContextRequest.getDecodedPathInContext(), servletApiRequest);
|
||||
|
||||
// Check data constraints
|
||||
if (!checkUserDataPermissions(servletContextRequest.getPathInContext(), servletContextRequest, response, callback, roleInfo))
|
||||
if (!checkUserDataPermissions(servletContextRequest.getDecodedPathInContext(), servletContextRequest, response, callback, roleInfo))
|
||||
return true;
|
||||
|
||||
// is Auth mandatory?
|
||||
|
|
|
@ -225,7 +225,7 @@ public class FormAuthenticator extends LoginAuthenticator
|
|||
ServletContextRequest servletContextRequest = Request.as(req, ServletContextRequest.class);
|
||||
ServletApiRequest servletApiRequest = servletContextRequest.getServletApiRequest();
|
||||
|
||||
String pathInContext = servletContextRequest.getPathInContext();
|
||||
String pathInContext = servletContextRequest.getDecodedPathInContext();
|
||||
boolean jSecurityCheck = isJSecurityCheck(pathInContext);
|
||||
mandatory |= jSecurityCheck;
|
||||
if (!mandatory)
|
||||
|
|
|
@ -70,11 +70,15 @@ public class AsyncContextTest
|
|||
_contextHandler.addServlet(new ServletHolder(new TestServlet()), "/path with spaces/servletPath");
|
||||
_contextHandler.addServlet(new ServletHolder(new TestServlet2()), "/servletPath2");
|
||||
|
||||
ServletHolder testHolder = new ServletHolder(new TestServlet());
|
||||
testHolder.setInitParameter("dispatchPath", "/test2/something%2felse");
|
||||
_contextHandler.addServlet(testHolder, "/test/*");
|
||||
ServletHolder encodedTestHolder = new ServletHolder(new TestServlet());
|
||||
encodedTestHolder.setInitParameter("dispatchPath", "/test2/something%25else");
|
||||
_contextHandler.addServlet(encodedTestHolder, "/encoded/*");
|
||||
_contextHandler.addServlet(new ServletHolder(new TestServlet2()), "/test2/*");
|
||||
|
||||
ServletHolder ambiguousTestHolder = new ServletHolder(new TestServlet());
|
||||
ambiguousTestHolder.setInitParameter("dispatchPath", "/test2/something%2Felse");
|
||||
_contextHandler.addServlet(ambiguousTestHolder, "/ambiguous/*");
|
||||
|
||||
_contextHandler.addServlet(new ServletHolder(new SelfDispatchingServlet()), "/self/*");
|
||||
|
||||
_contextHandler.addServlet(new ServletHolder(new TestStartThrowServlet()), "/startthrow/*");
|
||||
|
@ -221,11 +225,52 @@ public class AsyncContextTest
|
|||
@Test
|
||||
public void testDispatchAsyncContextEncodedUrl() throws Exception
|
||||
{
|
||||
String request = "GET /ctx/test/hello%20there?dispatch=true HTTP/1.1\r\n" +
|
||||
"Host: localhost\r\n" +
|
||||
"Content-Type: application/x-www-form-urlencoded\r\n" +
|
||||
"Connection: close\r\n" +
|
||||
"\r\n";
|
||||
String request = """
|
||||
GET /ctx/encoded/hello%20there?dispatch=true HTTP/1.1\r
|
||||
Host: localhost\r
|
||||
Content-Type: application/x-www-form-urlencoded\r
|
||||
Connection: close\r
|
||||
\r
|
||||
""";
|
||||
HttpTester.Response response = HttpTester.parseResponse(_connector.getResponse(request));
|
||||
assertThat("Response.status", response.getStatus(), is(HttpServletResponse.SC_OK));
|
||||
|
||||
String responseBody = response.getContent();
|
||||
|
||||
// initial values
|
||||
assertThat("servlet gets right path", responseBody, containsString("doGet:getServletPath:/test2"));
|
||||
assertThat("request uri has correct encoding", responseBody, containsString("doGet:getRequestURI:/ctx/test2/something%25else"));
|
||||
assertThat("request url has correct encoding", responseBody, containsString("doGet:getRequestURL:http://localhost/ctx/test2/something%25else"));
|
||||
assertThat("path info has correct encoding", responseBody, containsString("doGet:getPathInfo:/something%else"));
|
||||
|
||||
// async values
|
||||
assertThat("async servlet gets right path", responseBody, containsString("doGet:async:getServletPath:/test2"));
|
||||
assertThat("async request uri has correct encoding", responseBody, containsString("doGet:async:getRequestURI:/ctx/test2/something%25else"));
|
||||
assertThat("async request url has correct encoding", responseBody, containsString("doGet:async:getRequestURL:http://localhost/ctx/test2/something%25else"));
|
||||
assertThat("async path info has correct encoding", responseBody, containsString("doGet:async:getPathInfo:/something%else"));
|
||||
|
||||
// async run attributes
|
||||
assertThat("async run attr servlet path is original", responseBody, containsString("async:run:attr:servletPath:/encoded"));
|
||||
assertThat("async run attr path info has correct encoding", responseBody, containsString("async:run:attr:pathInfo:/hello there"));
|
||||
assertThat("async run attr query string", responseBody, containsString("async:run:attr:queryString:dispatch=true"));
|
||||
assertThat("async run context path", responseBody, containsString("async:run:attr:contextPath:/ctx"));
|
||||
assertThat("async run request uri has correct encoding", responseBody, containsString("async:run:attr:requestURI:/ctx/encoded/hello%20there"));
|
||||
assertThat("http servlet mapping matchValue is correct", responseBody, containsString("async:run:attr:mapping:matchValue:encoded"));
|
||||
assertThat("http servlet mapping pattern is correct", responseBody, containsString("async:run:attr:mapping:pattern:/encoded/*"));
|
||||
assertThat("http servlet mapping servletName is correct", responseBody, containsString("async:run:attr:mapping:servletName:"));
|
||||
assertThat("http servlet mapping mappingMatch is correct", responseBody, containsString("async:run:attr:mapping:mappingMatch:PATH"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDispatchAsyncAmbiguousUrl() throws Exception
|
||||
{
|
||||
String request = """
|
||||
GET /ctx/ambiguous/hello%20there?dispatch=true HTTP/1.1\r
|
||||
Host: localhost\r
|
||||
Content-Type: application/x-www-form-urlencoded\r
|
||||
Connection: close\r
|
||||
\r
|
||||
""";
|
||||
HttpTester.Response response = HttpTester.parseResponse(_connector.getResponse(request));
|
||||
assertThat("Response.status", response.getStatus(), is(HttpServletResponse.SC_OK));
|
||||
|
||||
|
@ -235,22 +280,22 @@ public class AsyncContextTest
|
|||
assertThat("servlet gets right path", responseBody, containsString("doGet:getServletPath:/test2"));
|
||||
assertThat("request uri has correct encoding", responseBody, containsString("doGet:getRequestURI:/ctx/test2/something%2Felse"));
|
||||
assertThat("request url has correct encoding", responseBody, containsString("doGet:getRequestURL:http://localhost/ctx/test2/something%2Felse"));
|
||||
assertThat("path info has correct encoding", responseBody, containsString("doGet:getPathInfo:/something%2Felse"));
|
||||
assertThat("path info has correct encoding", responseBody, containsString("doGet:getPathInfo:/something/else"));
|
||||
|
||||
// async values
|
||||
assertThat("async servlet gets right path", responseBody, containsString("doGet:async:getServletPath:/test2"));
|
||||
assertThat("async request uri has correct encoding", responseBody, containsString("doGet:async:getRequestURI:/ctx/test2/something%2Felse"));
|
||||
assertThat("async request url has correct encoding", responseBody, containsString("doGet:async:getRequestURL:http://localhost/ctx/test2/something%2Felse"));
|
||||
assertThat("async path info has correct encoding", responseBody, containsString("doGet:async:getPathInfo:/something%2Felse"));
|
||||
assertThat("async path info has correct encoding", responseBody, containsString("doGet:async:getPathInfo:/something/else"));
|
||||
|
||||
// async run attributes
|
||||
assertThat("async run attr servlet path is original", responseBody, containsString("async:run:attr:servletPath:/test"));
|
||||
assertThat("async run attr servlet path is original", responseBody, containsString("async:run:attr:servletPath:/ambiguous"));
|
||||
assertThat("async run attr path info has correct encoding", responseBody, containsString("async:run:attr:pathInfo:/hello there"));
|
||||
assertThat("async run attr query string", responseBody, containsString("async:run:attr:queryString:dispatch=true"));
|
||||
assertThat("async run context path", responseBody, containsString("async:run:attr:contextPath:/ctx"));
|
||||
assertThat("async run request uri has correct encoding", responseBody, containsString("async:run:attr:requestURI:/ctx/test/hello%20there"));
|
||||
assertThat("http servlet mapping matchValue is correct", responseBody, containsString("async:run:attr:mapping:matchValue:test"));
|
||||
assertThat("http servlet mapping pattern is correct", responseBody, containsString("async:run:attr:mapping:pattern:/test/*"));
|
||||
assertThat("async run request uri has correct encoding", responseBody, containsString("async:run:attr:requestURI:/ctx/ambiguous/hello%20there"));
|
||||
assertThat("http servlet mapping matchValue is correct", responseBody, containsString("async:run:attr:mapping:matchValue:ambiguous"));
|
||||
assertThat("http servlet mapping pattern is correct", responseBody, containsString("async:run:attr:mapping:pattern:/ambiguous/*"));
|
||||
assertThat("http servlet mapping servletName is correct", responseBody, containsString("async:run:attr:mapping:servletName:"));
|
||||
assertThat("http servlet mapping mappingMatch is correct", responseBody, containsString("async:run:attr:mapping:mappingMatch:PATH"));
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@ import org.eclipse.jetty.http.HttpField;
|
|||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http.HttpTester;
|
||||
import org.eclipse.jetty.http.UriCompliance;
|
||||
import org.eclipse.jetty.http.content.ResourceHttpContentFactory;
|
||||
import org.eclipse.jetty.logging.StacklessLogging;
|
||||
import org.eclipse.jetty.server.AllowedResourceAliasChecker;
|
||||
|
@ -207,6 +208,8 @@ public class DefaultServletTest
|
|||
@Test
|
||||
public void testGetPercent2F() throws Exception
|
||||
{
|
||||
connector.getConnectionFactory(HttpConfiguration.ConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.UNSAFE);
|
||||
|
||||
Path file = docRoot.resolve("file.txt");
|
||||
Files.writeString(file, "How now brown cow", UTF_8);
|
||||
|
||||
|
@ -256,6 +259,7 @@ public class DefaultServletTest
|
|||
assertThat(response.toString(), response.getContent(), is("In a while"));
|
||||
|
||||
// Attempt access of content in sub-dir of context, using "%2F" instead of "/", should be a 404
|
||||
// as neither getServletPath and getPathInfo are used and thus they don't throw.
|
||||
rawResponse = connector.getResponse("""
|
||||
GET /context/dirFoo%2Fother.txt HTTP/1.1\r
|
||||
Host: local\r
|
||||
|
|
|
@ -212,12 +212,12 @@ public class DispatcherTest
|
|||
String expected = """
|
||||
HTTP/1.1 200 OK\r
|
||||
Content-Type: text/plain\r
|
||||
Content-Length: 56\r
|
||||
Content-Length: 54\r
|
||||
Connection: close\r
|
||||
\r
|
||||
/context\r
|
||||
/EchoURI\r
|
||||
/x%20x\r
|
||||
/x x\r
|
||||
/context/EchoURI/x%20x;a=1\r
|
||||
""";
|
||||
assertEquals(expected, responses);
|
||||
|
|
|
@ -27,6 +27,7 @@ import jakarta.servlet.ServletResponse;
|
|||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.eclipse.jetty.http.UriCompliance;
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.LocalConnector;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
|
@ -148,15 +149,24 @@ public class EncodedURITest
|
|||
ServletContextHandler context2 = new ServletContextHandler();
|
||||
context2.setContextPath("/context_path".replace("_", separator));
|
||||
_contextCollection.addHandler(context2);
|
||||
context2.addServlet(TestServlet.class, "/test_servlet/*".replace("_", separator));
|
||||
context2.addServlet(TestServlet.class, URIUtil.decodePath("/test_servlet/*".replace("_", separator)));
|
||||
_connector.getConnectionFactory(HttpConfiguration.ConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.UNSAFE);
|
||||
_server.start();
|
||||
|
||||
String response = _connector.getResponse("GET /context_path/test_servlet/path_info HTTP/1.0\n\n".replace("_", separator));
|
||||
assertThat(response, startsWith("HTTP/1.1 200 "));
|
||||
assertThat(response, Matchers.containsString("requestURI=/context_path/test_servlet/path_info".replace("_", separator)));
|
||||
assertThat(response, Matchers.containsString("contextPath=/context_path".replace("_", separator)));
|
||||
assertThat(response, Matchers.containsString("servletPath=/test_servlet".replace("_", separator)));
|
||||
assertThat(response, Matchers.containsString("pathInfo=/path_info".replace("_", separator)));
|
||||
if ("%2F".equals(separator))
|
||||
{
|
||||
assertThat(response, Matchers.containsString("servletPath=org.eclipse.jetty.http.HttpException$IllegalArgumentException: 400: Ambiguous URI encoding"));
|
||||
assertThat(response, Matchers.containsString("pathInfo=org.eclipse.jetty.http.HttpException$IllegalArgumentException: 400: Ambiguous URI encoding"));
|
||||
}
|
||||
else
|
||||
{
|
||||
assertThat(response, Matchers.containsString("servletPath=/test_servlet".replace("_", "?")));
|
||||
assertThat(response, Matchers.containsString("pathInfo=/path_info".replace("_", "?")));
|
||||
}
|
||||
}
|
||||
|
||||
public static class TestServlet extends HttpServlet
|
||||
|
@ -167,9 +177,23 @@ public class EncodedURITest
|
|||
response.setContentType("text/plain");
|
||||
response.getWriter().println("requestURI=" + request.getRequestURI());
|
||||
response.getWriter().println("contextPath=" + request.getContextPath());
|
||||
try
|
||||
{
|
||||
response.getWriter().println("servletPath=" + request.getServletPath());
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
response.getWriter().println("servletPath=" + e);
|
||||
}
|
||||
try
|
||||
{
|
||||
response.getWriter().println("pathInfo=" + request.getPathInfo());
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
response.getWriter().println("pathInfo=" + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class AsyncServlet extends HttpServlet
|
||||
|
|
|
@ -59,6 +59,7 @@ import org.eclipse.jetty.util.IO;
|
|||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
@ -173,6 +174,7 @@ public class MultiPartServletTest
|
|||
}
|
||||
|
||||
@Test
|
||||
@Tag("Flaky")
|
||||
public void testManyParts() throws Exception
|
||||
{
|
||||
start(new HttpServlet()
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
|
||||
package org.eclipse.jetty.ee10.servlet;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
|
@ -30,6 +32,8 @@ import org.junit.jupiter.api.AfterEach;
|
|||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
public class RequestTest
|
||||
|
@ -124,27 +128,70 @@ public class RequestTest
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testSafeURI() throws Exception
|
||||
public void testAmbiguousURI() throws Exception
|
||||
{
|
||||
AtomicInteger count = new AtomicInteger();
|
||||
startServer(new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void service(HttpServletRequest request, HttpServletResponse resp)
|
||||
protected void service(HttpServletRequest request, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
count.incrementAndGet();
|
||||
String requestURI = request.getRequestURI();
|
||||
String servletPath;
|
||||
String pathInfo;
|
||||
try
|
||||
{
|
||||
servletPath = request.getServletPath();
|
||||
}
|
||||
catch (IllegalArgumentException iae)
|
||||
{
|
||||
servletPath = iae.toString();
|
||||
}
|
||||
try
|
||||
{
|
||||
pathInfo = request.getPathInfo();
|
||||
}
|
||||
catch (IllegalArgumentException iae)
|
||||
{
|
||||
pathInfo = iae.toString();
|
||||
}
|
||||
|
||||
resp.getOutputStream().println("requestURI=%s servletPath=%s pathInfo=%s".formatted(requestURI, servletPath, pathInfo));
|
||||
}
|
||||
});
|
||||
|
||||
_connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986_UNAMBIGUOUS);
|
||||
|
||||
String rawResponse = _connector.getResponse(
|
||||
"""
|
||||
_connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986);
|
||||
String rawRequest = """
|
||||
GET /test/foo%2fbar HTTP/1.1\r
|
||||
Host: localhost\r
|
||||
Connection: close\r
|
||||
\r
|
||||
""");
|
||||
""";
|
||||
String rawResponse = _connector.getResponse(rawRequest);
|
||||
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
|
||||
assertThat(response.getStatus(), is(HttpStatus.BAD_REQUEST_400));
|
||||
assertThat(count.get(), equalTo(0));
|
||||
|
||||
_connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.UNSAFE);
|
||||
rawResponse = _connector.getResponse(rawRequest);
|
||||
|
||||
response = HttpTester.parseResponse(rawResponse);
|
||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||
assertThat(response.getContent(), containsString("requestURI=/test/foo%2fbar"));
|
||||
assertThat(response.getContent(), containsString("servletPath=org.eclipse.jetty.http.HttpException$IllegalArgumentException: 400: Ambiguous URI encoding"));
|
||||
assertThat(response.getContent(), containsString("pathInfo=org.eclipse.jetty.http.HttpException$IllegalArgumentException: 400: Ambiguous URI encoding"));
|
||||
assertThat(count.get(), equalTo(1));
|
||||
|
||||
_server.getContainedBeans(ServletHandler.class).iterator().next().setDecodeAmbiguousURIs(true);
|
||||
rawResponse = _connector.getResponse(rawRequest);
|
||||
|
||||
response = HttpTester.parseResponse(rawResponse);
|
||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||
assertThat(response.getContent(), containsString("requestURI=/test/foo%2fbar"));
|
||||
assertThat(response.getContent(), containsString("servletPath= "));
|
||||
assertThat(response.getContent(), containsString("pathInfo=/test/foo/bar"));
|
||||
assertThat(count.get(), equalTo(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -165,14 +212,14 @@ public class RequestTest
|
|||
|
||||
String rawResponse = _connector.getResponse(
|
||||
"""
|
||||
GET /test/path%20info/foo%2fbar HTTP/1.1\r
|
||||
GET /test/path%20info/foo%2cbar HTTP/1.1\r
|
||||
Host: localhost\r
|
||||
Connection: close\r
|
||||
\r
|
||||
""");
|
||||
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
|
||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||
assertThat("request.getRequestURI", resultRequestURI.get(), is("/test/path%20info/foo%2fbar"));
|
||||
assertThat("request.getPathInfo", resultPathInfo.get(), is("/test/path info/foo%2Fbar"));
|
||||
assertThat("request.getRequestURI", resultRequestURI.get(), is("/test/path%20info/foo%2cbar"));
|
||||
assertThat("request.getPathInfo", resultPathInfo.get(), is("/test/path info/foo,bar"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -116,7 +116,7 @@ public class RequestURITest
|
|||
ServerConnector connector = new ServerConnector(server);
|
||||
connector.setPort(0);
|
||||
server.addConnector(connector);
|
||||
connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986);
|
||||
connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.UNSAFE);
|
||||
|
||||
ServletContextHandler context = new ServletContextHandler();
|
||||
context.setContextPath("/");
|
||||
|
|
|
@ -2406,9 +2406,9 @@ public class ServletContextHandlerTest
|
|||
ServletContextHandler context = new ServletContextHandler()
|
||||
{
|
||||
@Override
|
||||
protected ServletContextRequest newServletContextRequest(ServletChannel servletChannel, Request request, Response response, String pathInContext, MatchedResource<ServletHandler.MappedServlet> matchedResource)
|
||||
protected ServletContextRequest newServletContextRequest(ServletChannel servletChannel, Request request, Response response, String decodedPathInContext, MatchedResource<ServletHandler.MappedServlet> matchedResource)
|
||||
{
|
||||
return new ServletContextRequest(getContext().getServletContext(), servletChannel, request, response, pathInContext, matchedResource, getSessionHandler())
|
||||
return new ServletContextRequest(getContext().getServletContext(), servletChannel, request, response, decodedPathInContext, matchedResource, getSessionHandler())
|
||||
{
|
||||
@Override
|
||||
protected ServletContextResponse newServletContextResponse(Response response)
|
||||
|
|
|
@ -65,6 +65,7 @@ import org.slf4j.LoggerFactory;
|
|||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.either;
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
@ -329,7 +330,7 @@ public class WebAppContextTest
|
|||
|
||||
LocalConnector connector = new LocalConnector(server);
|
||||
server.addConnector(connector);
|
||||
connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986);
|
||||
connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.UNSAFE);
|
||||
|
||||
server.start();
|
||||
|
||||
|
@ -374,7 +375,8 @@ public class WebAppContextTest
|
|||
|
||||
server.start();
|
||||
|
||||
assertThat(HttpTester.parseResponse(connector.getResponse("GET " + target + " HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404));
|
||||
assertThat(HttpTester.parseResponse(connector.getResponse("GET " + target + " HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(),
|
||||
either(is(HttpStatus.NOT_FOUND_404)).or(is(HttpStatus.BAD_REQUEST_400)));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
|
|
|
@ -48,7 +48,7 @@ public class WebAppDefaultServletTest
|
|||
{
|
||||
server = new Server();
|
||||
connector = new LocalConnector(server);
|
||||
connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986);
|
||||
connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.UNSAFE);
|
||||
server.addConnector(connector);
|
||||
|
||||
Path directoryPath = workDir.getEmptyPathDir();
|
||||
|
|
|
@ -116,7 +116,7 @@ public class RequestURITest
|
|||
ServerConnector connector = new ServerConnector(server);
|
||||
connector.setPort(0);
|
||||
server.addConnector(connector);
|
||||
connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986);
|
||||
connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.UNSAFE);
|
||||
|
||||
ServletContextHandler context = new ServletContextHandler();
|
||||
context.setContextPath("/");
|
||||
|
|
|
@ -325,7 +325,7 @@ public class WebAppContextTest
|
|||
|
||||
LocalConnector connector = new LocalConnector(server);
|
||||
server.addConnector(connector);
|
||||
connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986);
|
||||
connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.UNSAFE);
|
||||
|
||||
server.start();
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ public class WebAppDefaultServletTest
|
|||
assertThat(FileSystemPool.INSTANCE.mounts(), empty());
|
||||
server = new Server();
|
||||
connector = new LocalConnector(server);
|
||||
connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986);
|
||||
connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.UNSAFE);
|
||||
server.addConnector(connector);
|
||||
|
||||
Path directoryPath = workDir.getEmptyPathDir();
|
||||
|
|
Loading…
Reference in New Issue