merge from 9.4.x

Signed-off-by: gregw <gregw@webtide.com>
This commit is contained in:
gregw 2021-02-16 16:35:57 +01:00
parent 19c32d6087
commit 172cd61878
11 changed files with 315 additions and 160 deletions

View File

@ -20,9 +20,6 @@ import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableSet;
import static java.util.EnumSet.allOf;
@ -49,7 +46,8 @@ public final class HttpCompliance implements ComplianceViolation.Mode
MULTIPLE_CONTENT_LENGTHS("https://tools.ietf.org/html/rfc7230#section-3.3.1", "Multiple Content-Lengths"),
TRANSFER_ENCODING_WITH_CONTENT_LENGTH("https://tools.ietf.org/html/rfc7230#section-3.3.1", "Transfer-Encoding and Content-Length"),
WHITESPACE_AFTER_FIELD_NAME("https://tools.ietf.org/html/rfc7230#section-3.2.4", "Whitespace not allowed after field name"),
NO_COLON_AFTER_FIELD_NAME("https://tools.ietf.org/html/rfc7230#section-3.2", "Fields must have a Colon");
NO_COLON_AFTER_FIELD_NAME("https://tools.ietf.org/html/rfc7230#section-3.2", "Fields must have a Colon"),
AMBIGUOUS_PATH_SEGMENTS("https://tools.ietf.org/html/rfc3986#section-3.3", "Ambiguous URI path segments");
private final String url;
private final String description;
@ -79,18 +77,18 @@ public final class HttpCompliance implements ComplianceViolation.Mode
}
}
private static final Logger LOG = LoggerFactory.getLogger(HttpParser.class);
public static final String VIOLATIONS_ATTR = "org.eclipse.jetty.http.compliance.violations";
public static final HttpCompliance RFC7230 = new HttpCompliance("RFC7230", noneOf(Violation.class));
public static final HttpCompliance RFC2616 = new HttpCompliance("RFC2616", of(Violation.HTTP_0_9, Violation.MULTILINE_FIELD_VALUE));
public static final HttpCompliance LEGACY = new HttpCompliance("LEGACY", complementOf(of(Violation.CASE_INSENSITIVE_METHOD)));
public static final HttpCompliance LEGACY = new HttpCompliance("LEGACY", complementOf(of(Violation.CASE_INSENSITIVE_METHOD, Violation.AMBIGUOUS_PATH_SEGMENTS)));
public static final HttpCompliance RFC2616_LEGACY = RFC2616.with("RFC2616_LEGACY",
Violation.CASE_INSENSITIVE_METHOD,
Violation.NO_COLON_AFTER_FIELD_NAME,
Violation.TRANSFER_ENCODING_WITH_CONTENT_LENGTH,
Violation.MULTIPLE_CONTENT_LENGTHS);
public static final HttpCompliance RFC7230_LEGACY = RFC7230.with("RFC7230_LEGACY", Violation.CASE_INSENSITIVE_METHOD);
Violation.MULTIPLE_CONTENT_LENGTHS,
Violation.AMBIGUOUS_PATH_SEGMENTS);
public static final HttpCompliance RFC7230_LEGACY = RFC7230.with("RFC7230_LEGACY", Violation.CASE_INSENSITIVE_METHOD, Violation.AMBIGUOUS_PATH_SEGMENTS);
private static final List<HttpCompliance> KNOWN_MODES = Arrays.asList(RFC7230, RFC2616, LEGACY, RFC2616_LEGACY, RFC7230_LEGACY);
private static final AtomicInteger __custom = new AtomicInteger();
@ -157,11 +155,6 @@ public final class HttpCompliance implements ComplianceViolation.Mode
if (exclude)
element = element.substring(1);
Violation section = Violation.valueOf(element);
if (section == null)
{
LOG.warn("Unknown section '{}' in HttpCompliance spec: {}", element, spec);
continue;
}
if (exclude)
sections.remove(section);
else
@ -176,7 +169,7 @@ public final class HttpCompliance implements ComplianceViolation.Mode
private HttpCompliance(String name, Set<Violation> violations)
{
Objects.nonNull(violations);
Objects.requireNonNull(violations);
_name = name;
_violations = unmodifiableSet(violations.isEmpty() ? noneOf(Violation.class) : copyOf(violations));
}
@ -184,7 +177,7 @@ public final class HttpCompliance implements ComplianceViolation.Mode
@Override
public boolean allows(ComplianceViolation violation)
{
return _violations.contains(violation);
return violation instanceof Violation && _violations.contains(violation);
}
@Override

View File

@ -17,6 +17,7 @@ import java.net.URI;
import java.net.URISyntaxException;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.Index;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.URIUtil;
@ -130,6 +131,8 @@ public interface HttpURI
boolean isAbsolute();
boolean hasAmbiguousSegment();
default URI toURI()
{
try
@ -155,6 +158,7 @@ public interface HttpURI
private final String _fragment;
private String _uri;
private String _decodedPath;
private boolean _ambiguousSegment;
private Immutable(Mutable builder)
{
@ -168,6 +172,7 @@ public interface HttpURI
_fragment = builder._fragment;
_uri = builder._uri;
_decodedPath = builder._decodedPath;
_ambiguousSegment = builder._ambiguousSegment;
}
private Immutable(String uri)
@ -331,6 +336,12 @@ public interface HttpURI
return !StringUtil.isEmpty(_scheme);
}
@Override
public boolean hasAmbiguousSegment()
{
return _ambiguousSegment;
}
@Override
public String toString()
{
@ -368,6 +379,28 @@ public interface HttpURI
ASTERISK
}
/**
* The concept of URI path parameters was originally specified in
* <a href="https://tools.ietf.org/html/rfc2396#section-3.3">RFC2396</a>, but that was
* obsoleted by
* <a href="https://tools.ietf.org/html/rfc3986#section-3.3">RFC3986</a> which removed
* a normative definition of path parameters. Specifically it excluded them from the
* <a href="https://tools.ietf.org/html/rfc3986#section-5.2.4">Remove Dot Segments</a>
* algorithm. This results in some ambiguity as dot segments can result from later
* parameter removal or % encoding expansion, that are not removed from the URI
* by {@link URIUtil#canonicalPath(String)}. Thus this class flags such ambiguous
* path segments, so that they may be rejected by the server if so configured.
*/
private final static Index<Boolean> __ambiguousSegments = new Index.Builder<Boolean>()
.caseSensitive(false)
.with("%2e", Boolean.TRUE)
.with("%2e%2e", Boolean.TRUE)
.with(".%2e", Boolean.TRUE)
.with("%2e.", Boolean.TRUE)
.with("..", Boolean.FALSE)
.with(".", Boolean.FALSE)
.build();
private String _scheme;
private String _user;
private String _host;
@ -378,6 +411,7 @@ public interface HttpURI
private String _fragment;
private String _uri;
private String _decodedPath;
private boolean _ambiguousSegment;
private Mutable()
{
@ -406,10 +440,12 @@ public interface HttpURI
_user = baseURI.getUser();
_host = baseURI.getHost();
_port = baseURI.getPort();
_path = path;
_param = param;
_query = query;
_fragment = null;
if (path != null)
parse(State.PATH, path);
if (param != null)
_param = param;
if (query != null)
_query = query;
}
private Mutable(String uri)
@ -428,17 +464,9 @@ public interface HttpURI
_host = "";
_port = uri.getPort();
_user = uri.getUserInfo();
_path = uri.getRawPath();
String pathParam = uri.getPath();
if (pathParam != null)
{
int p = pathParam.lastIndexOf(';');
if (p >= 0)
_param = pathParam.substring(p + 1);
else
_decodedPath = pathParam;
}
String path = uri.getRawPath();
if (path != null)
parse(State.PATH, path);
_query = uri.getRawQuery();
_fragment = uri.getRawFragment();
}
@ -507,7 +535,7 @@ public interface HttpURI
_fragment = null;
_uri = null;
_decodedPath = null;
_ambiguousSegment = false;
return this;
}
@ -631,6 +659,12 @@ public interface HttpURI
return _scheme != null && !_scheme.isEmpty();
}
@Override
public boolean hasAmbiguousSegment()
{
return _ambiguousSegment;
}
public Mutable normalize()
{
HttpScheme scheme = _scheme == null ? null : HttpScheme.CACHE.get(_scheme);
@ -731,6 +765,7 @@ public interface HttpURI
_query = uri.getQuery();
_uri = null;
_decodedPath = uri.getDecodedPath();
_ambiguousSegment = uri.hasAmbiguousSegment();
return this;
}
@ -778,11 +813,13 @@ public interface HttpURI
private void parse(State state, final String uri)
{
boolean encoded = false;
int mark = 0; // the start of the current section being parsed
int pathMark = 0; // the start of the path section
int segment = 0; // the start of the current segment within the path
boolean encoded = false; // set to true if the path contains % encoded characters
boolean dot = false; // set to true if the path containers . or .. segments
int escapedSlash = 0; // state of parsing a %2f
int end = uri.length();
int mark = 0;
int pathMark = 0;
char last = '/';
for (int i = 0; i < end; i++)
{
char c = uri.charAt(i);
@ -815,25 +852,28 @@ public interface HttpURI
_path = "*";
state = State.ASTERISK;
break;
case '.':
pathMark = i;
state = State.PATH;
case '%':
encoded = true;
escapedSlash = 1;
mark = pathMark = segment = i;
state = State.PATH;
break;
case '.':
dot = true;
pathMark = segment = i;
state = State.PATH;
break;
default:
mark = i;
if (_scheme == null)
state = State.SCHEME_OR_PATH;
else
{
pathMark = i;
pathMark = segment = i;
state = State.PATH;
}
break;
}
continue;
}
@ -847,43 +887,38 @@ public interface HttpURI
// Start again with scheme set
state = State.START;
break;
case '/':
// must have been in a path and still are
segment = i + 1;
state = State.PATH;
break;
case ';':
// must have been in a path
mark = i + 1;
state = State.PARAM;
break;
case '?':
// must have been in a path
_path = uri.substring(mark, i);
mark = i + 1;
state = State.QUERY;
break;
case '%':
// must have be in an encoded path
encoded = true;
escapedSlash = 1;
state = State.PATH;
break;
case '#':
// must have been in a path
_path = uri.substring(mark, i);
state = State.FRAGMENT;
break;
default:
break;
}
continue;
}
case HOST_OR_PATH:
{
switch (c)
@ -893,27 +928,22 @@ public interface HttpURI
mark = i + 1;
state = State.HOST;
break;
case '%':
case '@':
case ';':
case '?':
case '#':
case '.':
// was a path, look again
i--;
pathMark = mark;
segment = mark + 1;
state = State.PATH;
break;
case '.':
// it is a path
encoded = true;
pathMark = mark;
state = State.PATH;
break;
default:
// it is a path
pathMark = mark;
segment = mark + 1;
state = State.PATH;
}
continue;
@ -926,6 +956,7 @@ public interface HttpURI
case '/':
_host = uri.substring(mark, i);
pathMark = mark = i;
segment = mark + 1;
state = State.PATH;
break;
case ':':
@ -940,17 +971,14 @@ public interface HttpURI
_user = uri.substring(mark, i);
mark = i + 1;
break;
case '[':
state = State.IPV6;
break;
default:
break;
}
break;
}
case IPV6:
{
switch (c)
@ -971,14 +999,11 @@ public interface HttpURI
state = State.PATH;
}
break;
default:
break;
}
break;
}
case PORT:
{
if (c == '@')
@ -994,42 +1019,57 @@ public interface HttpURI
{
_port = TypeUtil.parseInt(uri, mark, i - mark, 10);
pathMark = mark = i;
segment = i + 1;
state = State.PATH;
}
break;
}
case PATH:
{
switch (c)
{
case ';':
checkSegment(uri, segment, i, true);
mark = i + 1;
state = State.PARAM;
break;
case '?':
checkSegment(uri, segment, i, false);
_path = uri.substring(pathMark, i);
mark = i + 1;
state = State.QUERY;
break;
case '#':
checkSegment(uri, segment, i, false);
_path = uri.substring(pathMark, i);
mark = i + 1;
state = State.FRAGMENT;
break;
case '%':
encoded = true;
case '/':
checkSegment(uri, segment, i, false);
segment = i + 1;
break;
case '.':
if ('/' == last)
encoded = true;
dot |= segment == i;
break;
case '%':
encoded = true;
escapedSlash = 1;
break;
case '2':
escapedSlash = escapedSlash == 1 ? 2 : 0;
break;
case 'f':
case 'F':
_ambiguousSegment |= (escapedSlash == 2);
escapedSlash = 0;
break;
default:
escapedSlash = 0;
break;
}
break;
}
case PARAM:
{
switch (c)
@ -1048,7 +1088,7 @@ public interface HttpURI
break;
case '/':
encoded = true;
// ignore internal params
segment = i + 1;
state = State.PATH;
break;
case ';':
@ -1060,7 +1100,6 @@ public interface HttpURI
}
break;
}
case QUERY:
{
if (c == '#')
@ -1071,22 +1110,19 @@ public interface HttpURI
}
break;
}
case ASTERISK:
{
throw new IllegalArgumentException("Bad character '*'");
}
case FRAGMENT:
{
_fragment = uri.substring(mark, end);
i = end;
break;
}
default:
throw new IllegalStateException(state.toString());
}
last = c;
}
switch (state)
@ -1094,55 +1130,69 @@ public interface HttpURI
case START:
case ASTERISK:
break;
case SCHEME_OR_PATH:
_path = uri.substring(mark, end);
break;
case HOST_OR_PATH:
_path = uri.substring(mark, end);
break;
case HOST:
if (end > mark)
_host = uri.substring(mark, end);
break;
case IPV6:
throw new IllegalArgumentException("No closing ']' for ipv6 in " + uri);
case PORT:
_port = TypeUtil.parseInt(uri, mark, end - mark, 10);
break;
case PARAM:
_path = uri.substring(pathMark, end);
_param = uri.substring(mark, end);
break;
case PATH:
checkSegment(uri, segment, end, false);
_path = uri.substring(pathMark, end);
break;
case QUERY:
_query = uri.substring(mark, end);
break;
case FRAGMENT:
_fragment = uri.substring(mark, end);
break;
default:
throw new IllegalStateException(state.toString());
}
if (!encoded)
if (!encoded && !dot)
{
if (_param == null)
_decodedPath = _path;
else
_decodedPath = _path.substring(0, _path.length() - _param.length() - 1);
}
else if (_path != null)
{
String canonical = URIUtil.canonicalPath(_path);
if (canonical == null)
throw new BadMessageException("Bad URI");
_decodedPath = URIUtil.decodePath(canonical);
}
}
/**
* Check for ambiguous path segments.
*
* An ambiguous path segment is one that is perhaps technically legal, but is considered undesirable to handle
* due to possible ambiguity. Examples include segments like '..;', '%2e', '%2e%2e' etc.
* @param uri The URI string
* @param segment The inclusive starting index of the segment (excluding any '/')
* @param end The exclusive end index of the segment
*/
private void checkSegment(String uri, int segment, int end, boolean param)
{
if (!_ambiguousSegment)
{
Boolean ambiguous = __ambiguousSegments.get(uri, segment, end - segment);
_ambiguousSegment |= ambiguous == Boolean.TRUE || (param && ambiguous == Boolean.FALSE);
}
}
}
}

View File

@ -13,7 +13,12 @@
package org.eclipse.jetty.http;
import java.util.Arrays;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
@ -300,4 +305,80 @@ public class HttpURITest
uri = HttpURI.from("http:./path/info/.");
assertEquals("path/info/", uri.getDecodedPath());
}
public static Stream<Arguments> decodePathTests()
{
return Arrays.stream(new Object[][]
{
// Simple path example
{"http://host/path/info", "/path/info", false},
{"//host/path/info", "/path/info", false},
{"/path/info", "/path/info", false},
// legal non ambiguous relative paths
{"http://host/../path/info", null, false},
{"http://host/path/../info", "/info", false},
{"http://host/path/./info", "/path/info", false},
{"//host/path/../info", "/info", false},
{"//host/path/./info", "/path/info", false},
{"/path/../info", "/info", false},
{"/path/./info", "/path/info", false},
{"path/../info", "info", false},
{"path/./info", "path/info", false},
// illegal paths
{"//host/../path/info", null, false},
{"/../path/info", null, false},
{"../path/info", null, false},
{"/path/%XX/info", null, false},
{"/path/%2/F/info", null, false},
// ambiguous dot encodings or parameter inclusions
{"scheme://host/path/%2e/info", "/path/./info", true},
{"scheme:/path/%2e/info", "/path/./info", true},
{"/path/%2e/info", "/path/./info", true},
{"path/%2e/info/", "path/./info/", true},
{"/path/%2e%2e/info", "/path/../info", true},
{"/path/%2e%2e;/info", "/path/../info", true},
{"/path/%2e%2e;param/info", "/path/../info", true},
{"/path/%2e%2e;param;other/info;other", "/path/../info", true},
{"/path/.;/info", "/path/./info", true},
{"/path/.;param/info", "/path/./info", true},
{"/path/..;/info", "/path/../info", true},
{"/path/..;param/info", "/path/../info", true},
{"%2e/info", "./info", true},
{"%2e%2e/info", "../info", true},
{"%2e%2e;/info", "../info", true},
{".;/info", "./info", true},
{".;param/info", "./info", true},
{"..;/info", "../info", true},
{"..;param/info", "../info", true},
{"%2e", ".", true},
{"%2e.", "..", true},
{".%2e", "..", true},
{"%2e%2e", "..", true},
// ambiguous segment separators
{"/path/%2f/info", "/path///info", true},
{"%2f/info", "//info", true},
{"%2F/info", "//info", true},
}).map(Arguments::of);
}
@ParameterizedTest
@MethodSource("decodePathTests")
public void testDecodedPath(String input, String decodedPath, boolean ambiguous)
{
try
{
HttpURI uri = HttpURI.from(input);
assertThat(uri.getDecodedPath(), is(decodedPath));
assertThat(uri.hasAmbiguousSegment(), is(ambiguous));
}
catch (Exception e)
{
assertThat(decodedPath, nullValue());
}
}
}

View File

@ -67,6 +67,7 @@ import javax.servlet.http.WebConnection;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.ComplianceViolation;
import org.eclipse.jetty.http.HostPortHttpField;
import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpCookie.SetCookieHttpField;
import org.eclipse.jetty.http.HttpField;
@ -1684,7 +1685,10 @@ public class Request implements HttpServletRequest
_method = request.getMethod();
_httpFields = request.getFields();
final HttpURI uri = request.getURI();
if (uri.hasAmbiguousSegment() && !_channel.getHttpConfiguration().getHttpCompliance().allows(HttpCompliance.Violation.AMBIGUOUS_PATH_SEGMENTS))
throw new BadMessageException("Ambiguous segment in URI");
if (uri.isAbsolute() && uri.hasAuthority() && uri.getPath() != null)
{
_uri = uri;
@ -1723,7 +1727,7 @@ public class Request implements HttpServletRequest
// TODO this is not really right for CONNECT
path = _uri.isAbsolute() ? "/" : null;
else if (encoded.startsWith("/"))
path = (encoded.length() == 1) ? "/" : URIUtil.canonicalPath(_uri.getDecodedPath());
path = (encoded.length() == 1) ? "/" : _uri.getDecodedPath();
else if ("*".equals(encoded) || HttpMethod.CONNECT.is(getMethod()))
path = encoded;
else

View File

@ -497,23 +497,7 @@ public class HttpConnectionTest
public void testBadPathDotDotPath() throws Exception
{
String response = connector.getResponse("GET /ooops/../../path HTTP/1.0\r\nHost: localhost:80\r\n\n");
checkContains(response, 0, "HTTP/1.1 400 Bad Request");
checkContains(response, 0, "reason: Bad URI");
}
@Test
public void testOKPathEncodedDotDotPath() throws Exception
{
String response = connector.getResponse("GET /ooops/%2e%2e/path HTTP/1.0\r\nHost: localhost:80\r\n\n");
checkContains(response, 0, "HTTP/1.1 200 OK");
checkContains(response, 0, "pathInfo=/path");
}
@Test
public void testBadPathEncodedDotDotPath() throws Exception
{
String response = connector.getResponse("GET /ooops/%2e%2e/%2e%2e/path HTTP/1.0\r\nHost: localhost:80\r\n\n");
checkContains(response, 0, "HTTP/1.1 400 Bad Request");
checkContains(response, 0, "HTTP/1.1 400 ");
checkContains(response, 0, "reason: Bad URI");
}
@ -521,7 +505,7 @@ public class HttpConnectionTest
public void testBadDotDotPath() throws Exception
{
String response = connector.getResponse("GET ../path HTTP/1.0\r\nHost: localhost:80\r\n\n");
checkContains(response, 0, "HTTP/1.1 400 Bad Request");
checkContains(response, 0, "HTTP/1.1 400 ");
checkContains(response, 0, "reason: Bad URI");
}
@ -529,15 +513,7 @@ public class HttpConnectionTest
public void testBadSlashDotDotPath() throws Exception
{
String response = connector.getResponse("GET /../path HTTP/1.0\r\nHost: localhost:80\r\n\n");
checkContains(response, 0, "HTTP/1.1 400 Bad Request");
checkContains(response, 0, "reason: Bad URI");
}
@Test
public void testEncodedBadDotDotPath() throws Exception
{
String response = connector.getResponse("GET %2e%2e/path HTTP/1.0\r\nHost: localhost:80\r\n\n");
checkContains(response, 0, "HTTP/1.1 400 Bad Request");
checkContains(response, 0, "HTTP/1.1 400 ");
checkContains(response, 0, "reason: Bad URI");
}

View File

@ -1811,6 +1811,28 @@ public class RequestTest
assertEquals(0, request.getParameterMap().size());
}
@Test
public void testAmbiguousPaths() throws Exception
{
_handler._checker = (request, response) -> true;
String request = "GET /ambiguous/..;/path HTTP/1.0\r\n" +
"Host: whatever\r\n" +
"\r\n";
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setHttpCompliance(HttpCompliance.RFC7230);
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setHttpCompliance(HttpCompliance.RFC7230_LEGACY);
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setHttpCompliance(HttpCompliance.RFC2616);
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setHttpCompliance(HttpCompliance.RFC2616_LEGACY);
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
}
@Test
public void testPushBuilder() throws Exception
{

View File

@ -224,7 +224,7 @@ public class AsyncContextTest
@Test
public void testDispatchAsyncContextEncodedUrl() throws Exception
{
String request = "GET /ctx/test/hello%2fthere?dispatch=true HTTP/1.1\r\n" +
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" +
@ -248,10 +248,10 @@ public class AsyncContextTest
// async run attributes
assertThat("async run attr servlet path is original", responseBody, containsString("async:run:attr:servletPath:/test"));
assertThat("async run attr path info has correct encoding", responseBody, containsString("async:run:attr:pathInfo:/hello/there"));
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%2fthere"));
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("http servlet mapping servletName is correct", responseBody, containsString("async:run:attr:mapping:servletName:"));
@ -261,7 +261,7 @@ public class AsyncContextTest
@Test
public void testDispatchAsyncContextSelfEncodedUrl() throws Exception
{
String request = "GET /ctx/self/hello%2fthere?dispatch=true HTTP/1.1\r\n" +
String request = "GET /ctx/self/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" +
@ -271,8 +271,8 @@ public class AsyncContextTest
String responseBody = response.getContent();
assertThat("servlet request uri initial", responseBody, containsString("doGet.REQUEST.requestURI:/ctx/self/hello%2fthere"));
assertThat("servlet request uri async", responseBody, containsString("doGet.ASYNC.requestURI:/ctx/self/hello%2fthere"));
assertThat("servlet request uri initial", responseBody, containsString("doGet.REQUEST.requestURI:/ctx/self/hello%20there"));
assertThat("servlet request uri async", responseBody, containsString("doGet.ASYNC.requestURI:/ctx/self/hello%20there"));
}
@Test

View File

@ -41,6 +41,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.DateGenerator;
import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.http.HttpContent;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
@ -48,6 +49,7 @@ import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.LocalConnector;
import org.eclipse.jetty.server.ResourceContentFactory;
import org.eclipse.jetty.server.ResourceService;
@ -110,6 +112,7 @@ public class DefaultServletTest
connector = new LocalConnector(server);
connector.getConnectionFactory(HttpConfiguration.ConnectionFactory.class).getHttpConfiguration().setSendServerVersion(false);
connector.getBean(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.RFC7230_LEGACY); // allow ambiguous path segments
File extraJarResources = MavenTestingUtils.getTestResourceFile(ODD_JAR);
URL[] urls = new URL[]{extraJarResources.toURI().toURL()};

View File

@ -29,6 +29,8 @@ import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.hamcrest.Matchers;
@ -107,6 +109,7 @@ public class RequestURITest
ServerConnector connector = new ServerConnector(server);
connector.setPort(0);
server.addConnector(connector);
connector.getBean(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.RFC7230_LEGACY); // Allow ambiguous segments
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/");

View File

@ -778,11 +778,9 @@ public class URIUtil
}
/**
* Convert a decoded path to a canonical form.
* Convert an encoded path to a canonical form.
* <p>
* All instances of "." and ".." are factored out.
* </p>
* <p>
* Null is returned if the path tries to .. above its root.
* </p>
*
@ -791,31 +789,35 @@ public class URIUtil
*/
public static String canonicalPath(String path)
{
// See https://tools.ietf.org/html/rfc3986#section-5.2.4
if (path == null || path.isEmpty())
return path;
boolean slash = true;
int end = path.length();
int i = 0;
int dots = 0;
loop:
while (i < end)
loop: while (i < end)
{
char c = path.charAt(i);
switch (c)
{
case '/':
slash = true;
dots = 0;
break;
case '.':
if (slash)
if (dots == 0)
{
dots = 1;
break loop;
slash = false;
}
dots = -1;
break;
default:
slash = false;
dots = -1;
}
i++;
@ -827,7 +829,6 @@ public class URIUtil
StringBuilder canonical = new StringBuilder(path.length());
canonical.append(path, 0, i);
int dots = 1;
i++;
while (i <= end)
{
@ -835,14 +836,18 @@ public class URIUtil
switch (c)
{
case '\0':
if (dots == 2)
{
if (canonical.length() < 2)
return null;
canonical.setLength(canonical.length() - 1);
canonical.setLength(canonical.lastIndexOf("/") + 1);
}
break;
case '/':
switch (dots)
{
case 0:
if (c != '\0')
canonical.append(c);
break;
case 1:
break;
@ -854,36 +859,42 @@ public class URIUtil
break;
default:
while (dots-- > 0)
{
canonical.append('.');
}
if (c != '\0')
canonical.append(c);
canonical.append(c);
}
slash = true;
dots = 0;
break;
case '.':
if (dots > 0)
dots++;
else if (slash)
dots = 1;
else
canonical.append('.');
slash = false;
switch (dots)
{
case 0:
dots = 1;
break;
case 1:
dots = 2;
break;
case 2:
canonical.append("...");
dots = -1;
break;
default:
canonical.append('.');
}
break;
default:
while (dots-- > 0)
switch (dots)
{
canonical.append('.');
case 1:
canonical.append('.');
break;
case 2:
canonical.append("..");
break;
default:
}
canonical.append(c);
dots = 0;
slash = false;
dots = -1;
}
i++;

View File

@ -29,6 +29,10 @@ public class URIUtilCanonicalPathTest
{
String[][] canonical =
{
// Examples from RFC
{"/a/b/c/./../../g", "/a/g"},
{"mid/content=5/../6", "mid/6"},
// Basic examples (no changes expected)
{"/hello.html", "/hello.html"},
{"/css/main.css", "/css/main.css"},
@ -51,8 +55,12 @@ public class URIUtilCanonicalPathTest
{"/aaa/./bbb/", "/aaa/bbb/"},
{"/aaa/./bbb", "/aaa/bbb"},
{"./bbb/", "bbb/"},
{"./aaa", "aaa"},
{"./aaa/", "aaa/"},
{"/./aaa/", "/aaa/"},
{"./aaa/../bbb/", "bbb/"},
{"/foo/.", "/foo/"},
{"/foo/./", "/foo/"},
{"./", ""},
{".", ""},
{".//", "/"},
@ -116,6 +124,10 @@ public class URIUtilCanonicalPathTest
{"/foo/.;/bar", "/foo/.;/bar"},
{"/foo/..;/bar", "/foo/..;/bar"},
{"/foo/..;/..;/bar", "/foo/..;/..;/bar"},
// Trailing / is preserved
{"/foo/bar/..", "/foo/"},
{"/foo/bar/../", "/foo/"},
};
ArrayList<Arguments> ret = new ArrayList<>();
@ -130,6 +142,6 @@ public class URIUtilCanonicalPathTest
@MethodSource("data")
public void testCanonicalPath(String input, String expectedResult)
{
assertThat("Canonical", URIUtil.canonicalPath(input), is(expectedResult));
assertThat(URIUtil.canonicalPath(input), is(expectedResult));
}
}