merge from 9.4.x
Signed-off-by: gregw <gregw@webtide.com>
This commit is contained in:
parent
19c32d6087
commit
172cd61878
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()};
|
||||
|
|
|
@ -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("/");
|
||||
|
|
|
@ -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++;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue