Added a `UriCompliance.Violation.USER_INFO` to deprecate user info in `HttpURI` (#12012)

As per [RFC9110](https://datatracker.ietf.org/doc/html/rfc9110#name-deprecation-of-userinfo-in-) user info is deprecated in server implementations.
The new violation for USER_DATA is included by default in 12.0.x, but will be removed in 12.1.x
This commit is contained in:
Greg Wilkins 2024-07-10 09:02:55 +10:00 committed by GitHub
parent 803fe5c86b
commit c880d9309b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 386 additions and 76 deletions

View File

@ -16,7 +16,6 @@ package org.eclipse.jetty.http;
import java.io.Serial;
import java.io.Serializable;
import java.net.URI;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
@ -65,12 +64,15 @@ import org.eclipse.jetty.util.URIUtil;
* Thus this class avoid and/or detects such ambiguities. Furthermore, by decoding characters and
* removing parameters before relative path normalization, ambiguous paths will be resolved in such
* a way to be non-standard-but-non-ambiguous to down stream interpretation of the decoded path string.
* The violations are recorded and available by API such as {@link #hasAmbiguousSegment()} so that requests
* containing them may be rejected in case the non-standard-but-non-ambiguous interpretations
* are not satisfactory for a given compliance configuration.
* </p>
* <p>
* Implementations that wish to process ambiguous URI paths must configure the compliance modes
* This class collates any {@link UriCompliance.Violation violations} against the specification
* and/or best practises in the {@link #getViolations()}. Users of this class should check against a
* configured {@link UriCompliance} mode if the {@code HttpURI} is suitable for use
* (see {@link UriCompliance#checkUriCompliance(UriCompliance, HttpURI, ComplianceViolation.Listener)}).
* </p>
* <p>
* For example, implementations that wish to process ambiguous URI paths must configure the compliance modes
* to accept them and then perform their own decoding of {@link #getPath()}.
* </p>
* <p>
@ -549,16 +551,41 @@ public interface HttpURI
.with("%u002e%u002e", Boolean.TRUE)
.build();
/**
* Encoded character sequences that violate the Servlet 6.0 spec
* https://jakarta.ee/specifications/servlet/6.0/jakarta-servlet-spec-6.0.html#uri-path-canonicalization
*/
private static final boolean[] __suspiciousPathCharacters;
/**
* Unencoded US-ASCII character sequences not allowed by HTTP or URI specs in path segments.
*/
private static final boolean[] __illegalPathCharacters;
private static final boolean[] __unreservedPctEncodedSubDelims;
private static final boolean[] __pathCharacters;
private static boolean isDigit(char c)
{
return (c >= '0') && (c <= '9');
}
private static boolean isHexDigit(char c)
{
return (((c >= 'a') && (c <= 'f')) || // ALPHA (lower)
((c >= 'A') && (c <= 'F')) || // ALPHA (upper)
((c >= '0') && (c <= '9')));
}
private static boolean isUnreserved(char c)
{
return (((c >= 'a') && (c <= 'z')) || // ALPHA (lower)
((c >= 'A') && (c <= 'Z')) || // ALPHA (upper)
((c >= '0') && (c <= '9')) || // DIGIT
(c == '-') || (c == '.') || (c == '_') || (c == '~'));
}
private static boolean isSubDelim(char c)
{
return c == '!' || c == '$' || c == '&' || c == '\'' || c == '(' || c == ')' || c == '*' || c == '+' || c == ',' || c == ';' || c == '=';
}
static boolean isUnreservedPctEncodedOrSubDelim(char c)
{
return c < __unreservedPctEncodedSubDelims.length && __unreservedPctEncodedSubDelims[c];
}
static
{
@ -588,39 +615,32 @@ public interface HttpURI
// gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
// sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
// / "*" / "+" / "," / ";" / "="
//
// authority = [ userinfo "@" ] host [ ":" port ]
// userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
// host = IP-literal / IPv4address / reg-name
// port = *DIGIT
//
// reg-name = *( unreserved / pct-encoded / sub-delims )
//
// we are limited to US-ASCII per https://datatracker.ietf.org/doc/html/rfc3986#section-2
boolean[] illegalChars = new boolean[128];
Arrays.fill(illegalChars, true);
// pct-encoded
illegalChars['%'] = false;
// unreserved
for (int i = 0; i < illegalChars.length; i++)
__unreservedPctEncodedSubDelims = new boolean[128];
__pathCharacters = new boolean[128];
for (int i = 0; i < __pathCharacters.length; i++)
{
if (((i >= 'a') && (i <= 'z')) || // ALPHA (lower)
((i >= 'A') && (i <= 'Z')) || // ALPHA (upper)
((i >= '0') && (i <= '9')) || // DIGIT
(i == '-') || (i == '.') || (i == '_') || (i == '_') || (i == '~')
)
{
illegalChars[i] = false;
}
char c = (char)i;
__unreservedPctEncodedSubDelims[i] = isUnreserved(c) || c == '%' || isSubDelim(c);
__pathCharacters[i] = __unreservedPctEncodedSubDelims[i] || c == ':' || c == '@';
}
// reserved
String reserved = ":/?#[]@!$&'()*+,=";
for (char c: reserved.toCharArray())
illegalChars[c] = false;
__illegalPathCharacters = illegalChars;
// anything else in the US-ASCII space is not allowed
// suspicious path characters
boolean[] suspicious = new boolean[128];
Arrays.fill(suspicious, false);
suspicious['\\'] = true;
suspicious[0x7F] = true;
__suspiciousPathCharacters = new boolean[128];
__suspiciousPathCharacters['\\'] = true;
__suspiciousPathCharacters[0x7F] = true;
for (int i = 0; i <= 0x1F; i++)
suspicious[i] = true;
__suspiciousPathCharacters = suspicious;
__suspiciousPathCharacters[i] = true;
}
private String _scheme;
@ -650,6 +670,8 @@ public interface HttpURI
_uri = null;
_scheme = baseURI.getScheme();
_user = baseURI.getUser();
if (_user != null)
_violations = EnumSet.of(Violation.USER_INFO);
_host = baseURI.getHost();
_port = baseURI.getPort();
if (pathQuery != null)
@ -661,6 +683,8 @@ public interface HttpURI
_uri = null;
_scheme = baseURI.getScheme();
_user = baseURI.getUser();
if (_user != null)
_violations = EnumSet.of(Violation.USER_INFO);
_host = baseURI.getHost();
_port = baseURI.getPort();
if (path != null)
@ -686,6 +710,8 @@ public interface HttpURI
_host = "";
_port = uri.getPort();
_user = uri.getUserInfo();
if (_user != null)
_violations = EnumSet.of(Violation.USER_INFO);
String path = uri.getRawPath();
if (path != null)
parse(State.PATH, path);
@ -1086,6 +1112,10 @@ public interface HttpURI
public Mutable user(String user)
{
_user = user;
if (user == null)
removeViolation(Violation.USER_INFO);
else
addViolation(Violation.USER_INFO);
_uri = null;
return this;
}
@ -1095,12 +1125,13 @@ public interface HttpURI
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 encodedPath = false; // set to true if the path contains % encoded characters
boolean encoded = false; // set to true if the string contains % encoded characters
boolean encodedUtf16 = false; // Is the current encoding for UTF16?
int encodedCharacters = 0; // partial state of parsing a % encoded character<x>
int encodedValue = 0; // the partial encoded value
boolean dot = false; // set to true if the path contains . or .. segments
int end = uri.length();
boolean password = false;
_emptySegment = false;
for (int i = 0; i < end; i++)
{
@ -1140,7 +1171,7 @@ public interface HttpURI
state = State.ASTERISK;
break;
case '%':
encodedPath = true;
encoded = true;
encodedCharacters = 2;
encodedValue = 0;
mark = pathMark = segment = i;
@ -1194,7 +1225,7 @@ public interface HttpURI
break;
case '%':
// must have been in an encoded path
encodedPath = true;
encoded = true;
encodedCharacters = 2;
encodedValue = 0;
state = State.PATH;
@ -1244,27 +1275,53 @@ public interface HttpURI
switch (c)
{
case '/':
if (encodedCharacters > 0 || password)
throw new IllegalArgumentException("Bad authority");
_host = uri.substring(mark, i);
pathMark = mark = i;
segment = mark + 1;
state = State.PATH;
encoded = false;
break;
case ':':
if (encodedCharacters > 0 || password)
throw new IllegalArgumentException("Bad authority");
if (i > mark)
_host = uri.substring(mark, i);
mark = i + 1;
state = State.PORT;
break;
case '@':
if (_user != null)
if (encodedCharacters > 0)
throw new IllegalArgumentException("Bad authority");
_user = uri.substring(mark, i);
addViolation(Violation.USER_INFO);
password = false;
encoded = false;
mark = i + 1;
break;
case '[':
if (i != mark)
throw new IllegalArgumentException("Bad authority");
state = State.IPV6;
break;
case '%':
if (encodedCharacters > 0)
throw new IllegalArgumentException("Bad authority");
encoded = true;
encodedCharacters = 2;
break;
default:
if (encodedCharacters > 0)
{
encodedCharacters--;
if (!isHexDigit(c))
throw new IllegalArgumentException("Bad authority");
}
else if (!isUnreservedPctEncodedOrSubDelim(c))
{
throw new IllegalArgumentException("Bad authority");
}
break;
}
break;
@ -1289,7 +1346,11 @@ public interface HttpURI
state = State.PATH;
}
break;
case ':':
break;
default:
if (!isHexDigit(c))
throw new IllegalArgumentException("Bad authority");
break;
}
break;
@ -1302,6 +1363,7 @@ public interface HttpURI
throw new IllegalArgumentException("Bad authority");
// It wasn't a port, but a password!
_user = _host + ":" + uri.substring(mark, i);
addViolation(Violation.USER_INFO);
mark = i + 1;
state = State.HOST;
}
@ -1312,6 +1374,22 @@ public interface HttpURI
segment = i + 1;
state = State.PATH;
}
else if (!isDigit(c))
{
if (isUnreservedPctEncodedOrSubDelim(c))
{
// must be a password
password = true;
state = State.HOST;
if (_host != null)
{
mark = mark - _host.length() - 1;
_host = null;
}
break;
}
throw new IllegalArgumentException("Bad authority");
}
break;
}
case PATH:
@ -1353,18 +1431,18 @@ public interface HttpURI
switch (c)
{
case ';':
checkSegment(uri, dot || encodedPath, segment, i, true);
checkSegment(uri, dot || encoded, segment, i, true);
mark = i + 1;
state = State.PARAM;
break;
case '?':
checkSegment(uri, dot || encodedPath, segment, i, false);
checkSegment(uri, dot || encoded, segment, i, false);
_path = uri.substring(pathMark, i);
mark = i + 1;
state = State.QUERY;
break;
case '#':
checkSegment(uri, dot || encodedPath, segment, i, false);
checkSegment(uri, dot || encoded, segment, i, false);
_path = uri.substring(pathMark, i);
mark = i + 1;
state = State.FRAGMENT;
@ -1372,21 +1450,21 @@ public interface HttpURI
case '/':
// There is no leading segment when parsing only a path that starts with slash.
if (i != 0)
checkSegment(uri, dot || encodedPath, segment, i, false);
checkSegment(uri, dot || encoded, segment, i, false);
segment = i + 1;
break;
case '.':
dot |= segment == i;
break;
case '%':
encodedPath = true;
encoded = true;
encodedUtf16 = false;
encodedCharacters = 2;
encodedValue = 0;
break;
default:
// The RFC does not allow unencoded path characters that are outside the ABNF
if (c > __illegalPathCharacters.length || __illegalPathCharacters[c])
if (c > __pathCharacters.length || !__pathCharacters[c])
addViolation(Violation.ILLEGAL_PATH_CHARACTERS);
if (c < __suspiciousPathCharacters.length && __suspiciousPathCharacters[c])
addViolation(Violation.SUSPICIOUS_PATH_CHARACTERS);
@ -1412,7 +1490,7 @@ public interface HttpURI
state = State.FRAGMENT;
break;
case '/':
encodedPath = true;
encoded = true;
segment = i + 1;
state = State.PATH;
break;
@ -1477,7 +1555,7 @@ public interface HttpURI
_param = uri.substring(mark, end);
break;
case PATH:
checkSegment(uri, dot || encodedPath, segment, end, false);
checkSegment(uri, dot || encoded, segment, end, false);
_path = uri.substring(pathMark, end);
break;
case QUERY:
@ -1490,7 +1568,7 @@ public interface HttpURI
throw new IllegalStateException(state.toString());
}
if (!encodedPath && !dot)
if (!encoded && !dot)
{
if (_param == null)
_canonicalPath = _path;
@ -1576,5 +1654,12 @@ public interface HttpURI
else
_violations.add(violation);
}
private void removeViolation(Violation violation)
{
if (_violations == null)
return;
_violations.remove(violation);
}
}
}

View File

@ -109,7 +109,12 @@ public final class UriCompliance implements ComplianceViolation.Mode
* Allow path characters not allowed in the path portion of the URI and HTTP specs.
* <p>This would allow characters that fall outside of the {@code unreserved / pct-encoded / sub-delims / ":" / "@"} ABNF</p>
*/
ILLEGAL_PATH_CHARACTERS("https://datatracker.ietf.org/doc/html/rfc3986#section-3.3", "Illegal Path Character");
ILLEGAL_PATH_CHARACTERS("https://datatracker.ietf.org/doc/html/rfc3986#section-3.3", "Illegal Path Character"),
/**
* Allow user info in the authority portion of the URI and HTTP specs.
*/
USER_INFO("https://datatracker.ietf.org/doc/html/rfc9110#name-deprecation-of-userinfo-in-", "Deprecated User Info");
private final String _url;
private final String _description;
@ -175,7 +180,8 @@ public final class UriCompliance implements ComplianceViolation.Mode
Violation.AMBIGUOUS_PATH_SEPARATOR,
Violation.AMBIGUOUS_PATH_ENCODING,
Violation.AMBIGUOUS_EMPTY_SEGMENT,
Violation.UTF16_ENCODINGS));
Violation.UTF16_ENCODINGS,
Violation.USER_INFO));
/**
* Compliance mode that allows all URI Violations, including allowing ambiguous paths in non-canonical form, and illegal characters

View File

@ -34,6 +34,7 @@ import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.sameInstance;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -59,6 +60,7 @@ public class HttpURITest
assertThat(uri.getScheme(), is("http"));
assertThat(uri.getUser(), is("user:password"));
assertTrue(uri.hasViolation(Violation.USER_INFO));
assertThat(uri.getHost(), is("host"));
assertThat(uri.getPort(), is(8888));
assertThat(uri.getPath(), is("/ignored/../p%61th;ignored/info;param"));
@ -81,6 +83,7 @@ public class HttpURITest
assertThat(uri.getScheme(), is("https"));
assertThat(uri.getUser(), nullValue());
assertFalse(uri.hasViolation(Violation.USER_INFO));
assertThat(uri.getHost(), is("[::1]"));
assertThat(uri.getPort(), is(8080));
assertThat(uri.getPath(), is("/some%20encoded/evening;id=12345"));
@ -98,6 +101,7 @@ public class HttpURITest
assertThat(uri.getScheme(), is("http"));
assertThat(uri.getUser(), is("user:password"));
assertTrue(uri.hasViolation(Violation.USER_INFO));
assertThat(uri.getHost(), is("host"));
assertThat(uri.getPort(), is(8888));
assertThat(uri.getPath(), is("/ignored/../p%61th;ignored/info;param"));
@ -155,11 +159,8 @@ public class HttpURITest
assertThat(uri.getHost(), is("foo"));
assertThat(uri.getPath(), is("/bar"));
// We do allow nulls if not encoded. This can be used for testing 2nd line of defence.
builder.uri("http://fo\000/bar");
uri = builder.asImmutable();
assertThat(uri.getHost(), is("fo\000"));
assertThat(uri.getPath(), is("/bar"));
// We do not allow nulls if not encoded.
assertThrows(IllegalArgumentException.class, () -> builder.uri("http://fo\000/bar").asImmutable());
}
@Test
@ -327,6 +328,7 @@ public class HttpURITest
assertEquals("http://user:password@example.com:8888/blah", uri.toString());
assertEquals(uri.getAuthority(), "example.com:8888");
assertEquals(uri.getUser(), "user:password");
assertTrue(uri.hasViolation(Violation.USER_INFO));
}
@Test
@ -1198,4 +1200,36 @@ public class HttpURITest
HttpURI httpURI = HttpURI.from(scheme, server, port, null);
assertThat(httpURI.asString(), is(expectedStr));
}
public static Stream<String> badAuthorities()
{
return Stream.of(
"http://#host/path",
"https:// host/path",
"https://h st/path",
"https://h\000st/path",
"https://h%GGst/path",
"https://host%/path",
"https://host%0/path",
"https://host%u001f/path",
"https://host%:8080/path",
"https://host%0:8080/path",
"https://user%@host/path",
"https://user%0@host/path",
"https://host:notport/path",
"https://user@host:notport/path",
"https://user:password@host:notport/path",
"https://user @host.com/",
"https://user#@host.com/",
"https://[notIpv6]/",
"https://bad[0::1::2::3::4]/"
);
}
@ParameterizedTest
@MethodSource("badAuthorities")
public void testBadAuthority(String uri)
{
assertThrows(IllegalArgumentException.class, () -> HttpURI.from(uri));
}
}

View File

@ -79,6 +79,7 @@ public class HttpConfiguration implements Dumpable
private long _minResponseDataRate;
private HttpCompliance _httpCompliance = HttpCompliance.RFC7230;
private UriCompliance _uriCompliance = UriCompliance.DEFAULT;
private UriCompliance _redirectUriCompliance = null; // TODO default to UriCompliance.DEFAULT in 12.1 ?;
private CookieCompliance _requestCookieCompliance = CookieCompliance.RFC6265;
private CookieCompliance _responseCookieCompliance = CookieCompliance.RFC6265;
private MultiPartCompliance _multiPartCompliance = MultiPartCompliance.RFC7578;
@ -159,6 +160,7 @@ public class HttpConfiguration implements Dumpable
_notifyRemoteAsyncErrors = config._notifyRemoteAsyncErrors;
_relativeRedirectAllowed = config._relativeRedirectAllowed;
_uriCompliance = config._uriCompliance;
_redirectUriCompliance = config._redirectUriCompliance;
_serverAuthority = config._serverAuthority;
_localAddress = config._localAddress;
}
@ -598,11 +600,24 @@ public class HttpConfiguration implements Dumpable
return _uriCompliance;
}
public UriCompliance getRedirectUriCompliance()
{
return _redirectUriCompliance;
}
public void setUriCompliance(UriCompliance uriCompliance)
{
_uriCompliance = uriCompliance;
}
/**
* @param uriCompliance The {@link UriCompliance} to apply in {@link Response#toRedirectURI(Request, String)} or {@code null}.
*/
public void setRedirectUriCompliance(UriCompliance uriCompliance)
{
_redirectUriCompliance = uriCompliance;
}
/**
* @return The CookieCompliance used for parsing request {@code Cookie} headers.
* @see #getResponseCookieCompliance()

View File

@ -33,6 +33,7 @@ import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.Trailers;
import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.QuietException;
@ -301,25 +302,32 @@ public interface Response extends Content.Sink
return;
}
if (consumeAvailable)
try
{
while (true)
if (consumeAvailable)
{
Content.Chunk chunk = response.getRequest().read();
if (chunk == null)
while (true)
{
response.getHeaders().put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE);
break;
Content.Chunk chunk = response.getRequest().read();
if (chunk == null)
{
response.getHeaders().put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE);
break;
}
chunk.release();
if (chunk.isLast())
break;
}
chunk.release();
if (chunk.isLast())
break;
}
}
response.getHeaders().put(HttpHeader.LOCATION, toRedirectURI(request, location));
response.setStatus(code);
response.write(true, null, callback);
response.getHeaders().put(HttpHeader.LOCATION, toRedirectURI(request, location));
response.setStatus(code);
response.write(true, null, callback);
}
catch (Throwable failure)
{
callback.failed(failure);
}
}
/**
@ -333,6 +341,8 @@ public interface Response extends Content.Sink
*/
static String toRedirectURI(Request request, String location)
{
HttpConfiguration httpConfiguration = request.getConnectionMetaData().getHttpConfiguration();
// is the URI absolute already?
if (!URIUtil.hasScheme(location))
{
@ -353,12 +363,19 @@ public interface Response extends Content.Sink
throw new IllegalStateException("redirect path cannot be above root");
// if relative redirects are not allowed?
if (!request.getConnectionMetaData().getHttpConfiguration().isRelativeRedirectAllowed())
{
if (!httpConfiguration.isRelativeRedirectAllowed())
// make the location an absolute URI
location = URIUtil.newURI(uri.getScheme(), Request.getServerName(request), Request.getServerPort(request), location, null);
}
}
UriCompliance redirectCompliance = httpConfiguration.getRedirectUriCompliance();
if (redirectCompliance != null)
{
String violations = UriCompliance.checkUriCompliance(redirectCompliance, HttpURI.from(location), null);
if (StringUtil.isNotBlank(violations))
throw new IllegalArgumentException(violations);
}
return location;
}

View File

@ -22,6 +22,7 @@ import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
@ -100,6 +101,76 @@ public class RequestTest
assertEquals(HttpStatus.BAD_REQUEST_400, response.getStatus());
}
public static Stream<Arguments> getUriTests()
{
return Stream.of(
Arguments.of(UriCompliance.DEFAULT, "/", 200, "local"),
Arguments.of(UriCompliance.DEFAULT, "https://local/", 200, "local"),
Arguments.of(UriCompliance.DEFAULT, "https://other/", 400, "Authority!=Host"),
Arguments.of(UriCompliance.UNSAFE, "https://other/", 200, "other"),
Arguments.of(UriCompliance.DEFAULT, "https://user@local/", 400, "Deprecated User Info"),
Arguments.of(UriCompliance.LEGACY, "https://user@local/", 200, "local"),
Arguments.of(UriCompliance.LEGACY, "https://user@local:port/", 400, "Bad Request"),
Arguments.of(UriCompliance.LEGACY, "https://user@local:8080/", 400, "Authority!=Host"),
Arguments.of(UriCompliance.UNSAFE, "https://user@local:8080/", 200, "local:8080"),
Arguments.of(UriCompliance.DEFAULT, "https://user:password@local/", 400, "Deprecated User Info"),
Arguments.of(UriCompliance.LEGACY, "https://user:password@local/", 200, "local"),
Arguments.of(UriCompliance.DEFAULT, "https://user@other/", 400, "Deprecated User Info"),
Arguments.of(UriCompliance.LEGACY, "https://user@other/", 400, "Authority!=Host"),
Arguments.of(UriCompliance.DEFAULT, "https://user:password@other/", 400, "Deprecated User Info"),
Arguments.of(UriCompliance.LEGACY, "https://user:password@other/", 400, "Authority!=Host"),
Arguments.of(UriCompliance.UNSAFE, "https://user:password@other/", 200, "other"),
Arguments.of(UriCompliance.DEFAULT, "/%2F/", 400, "Ambiguous URI path separator"),
Arguments.of(UriCompliance.UNSAFE, "/%2F/", 200, "local")
);
}
@ParameterizedTest
@MethodSource("getUriTests")
public void testGETUris(UriCompliance compliance, String uri, int status, String content) throws Exception
{
server.stop();
for (Connector connector: server.getConnectors())
{
HttpConnectionFactory httpConnectionFactory = connector.getConnectionFactory(HttpConnectionFactory.class);
if (httpConnectionFactory != null)
{
HttpConfiguration httpConfiguration = httpConnectionFactory.getHttpConfiguration();
httpConfiguration.setUriCompliance(compliance);
if (compliance == UriCompliance.UNSAFE)
httpConfiguration.setHttpCompliance(HttpCompliance.RFC2616_LEGACY);
}
}
server.setHandler(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback)
{
String msg = String.format("authority=\"%s\"", request.getHttpURI().getAuthority());
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain;charset=utf-8");
Content.Sink.write(response, true, msg, callback);
return true;
}
});
server.start();
String request = """
GET %s HTTP/1.1\r
Host: local\r
Connection: close\r
\r
""".formatted(uri);
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(status));
if (content != null)
{
if (status == 200)
assertThat(response.getContent(), is("authority=\"%s\"".formatted(content)));
else
assertThat(response.getContent(), containsString(content));
}
}
@Test
public void testAmbiguousPathSep() throws Exception
{

View File

@ -16,6 +16,7 @@ package org.eclipse.jetty.server;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.stream.Stream;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpField;
@ -24,15 +25,21 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.SetCookieParser;
import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.component.LifeCycle;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
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.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -389,6 +396,81 @@ public class ResponseTest
assertThat(response.get(HttpHeader.LOCATION), is("/somewhere/else"));
}
public static Stream<Arguments> redirectComplianceTest()
{
return Stream.of(
Arguments.of(null, "http://[bad]:xyz/", HttpStatus.FOUND_302, null),
Arguments.of(UriCompliance.UNSAFE, "http://[bad]:xyz/", HttpStatus.INTERNAL_SERVER_ERROR_500, "Bad authority"),
Arguments.of(UriCompliance.DEFAULT, "http://[bad]:xyz/", HttpStatus.INTERNAL_SERVER_ERROR_500, "Bad authority"),
Arguments.of(null, "http://user:password@host.com/", HttpStatus.FOUND_302, null),
Arguments.of(UriCompliance.DEFAULT, "http://user:password@host.com/", HttpStatus.INTERNAL_SERVER_ERROR_500, "Deprecated User Info"),
Arguments.of(UriCompliance.LEGACY, "http://user:password@host.com/", HttpStatus.FOUND_302, null),
Arguments.of(null, "http://host.com/very%2Funsafe", HttpStatus.FOUND_302, null),
Arguments.of(UriCompliance.LEGACY, "http://host.com/very%2Funsafe", HttpStatus.FOUND_302, null),
Arguments.of(UriCompliance.DEFAULT, "http://host.com/very%2Funsafe", HttpStatus.INTERNAL_SERVER_ERROR_500, "Ambiguous")
);
}
@ParameterizedTest
@MethodSource("redirectComplianceTest")
public void testRedirectCompliance(UriCompliance compliance, String location, int status, String content) throws Exception
{
try (StacklessLogging ignored = new StacklessLogging(Response.class))
{
server.getConnectors()[0].getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setRedirectUriCompliance(compliance);
server.getConnectors()[0].getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setRelativeRedirectAllowed(true);
server.setHandler(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback)
{
Response.sendRedirect(request, response, callback, location);
return true;
}
});
server.start();
String request = """
GET /path HTTP/1.0\r
Host: hostname\r
\r
""";
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(status));
if (HttpStatus.isRedirection(status))
assertThat(response.get(HttpHeader.LOCATION), is(location));
if (content != null)
assertThat(response.getContent(), containsString(content));
}
}
@Test
public void testAuthorityUserNotAllowedWithNonRelativeRedirect() throws Exception
{
server.getConnectors()[0].getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setRelativeRedirectAllowed(false);
server.getConnectors()[0].getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.LEGACY);
server.getConnectors()[0].getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setRedirectUriCompliance(UriCompliance.DEFAULT);
server.setHandler(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback)
{
Response.sendRedirect(request, response, callback, "/somewhere/else");
return true;
}
});
server.start();
String request = """
GET http://user:password@hostname:8888/path HTTP/1.0\r
Host: hostname:8888\r
\r
""";
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertEquals(HttpStatus.MOVED_TEMPORARILY_302, response.getStatus());
assertThat(response.get(HttpHeader.LOCATION), is("http://hostname:8888/somewhere/else"));
}
@Test
public void testXPoweredByDefault() throws Exception
{