Merge remote-tracking branch 'origin/jetty-10.0.x' into jetty-11.0.x

This commit is contained in:
Joakim Erdfelt 2021-06-23 18:14:25 -05:00
commit c1b6e30471
No known key found for this signature in database
GPG Key ID: 2D0E1FB8FE4B68B4
15 changed files with 819 additions and 522 deletions

View File

@ -16,4 +16,4 @@
Eclipse Jetty is an open source project with a long pedigree of contribution. Eclipse Jetty is an open source project with a long pedigree of contribution.
Starting over 20 years ago, Jetty has had many committers over the years and owes much of its success to the people that make up the community. Starting over 20 years ago, Jetty has had many committers over the years and owes much of its success to the people that make up the community.
There are many ways that you may contribute to Jetty and the goal of this guide is help you get there! There are many ways that you may contribute to Jetty, and the goal of this guide is help you get there!

View File

@ -55,7 +55,7 @@ Build related artifacts that release separately, common assembly descriptors, re
Files associated with the development of Jetty -- code styles, formatting, iplogs, etc.:: Files associated with the development of Jetty -- code styles, formatting, iplogs, etc.::
http://git.eclipse.org/c/jetty/org.eclipse.jetty.admin.git http://git.eclipse.org/c/jetty/org.eclipse.jetty.admin.git
[[t-contributing-build]] [[cg-contributing-build]]
=== Maven Build === Maven Build
Eclipse Jetty uses http://maven.apache.org/[Apache Maven] for managing the project metadata and controlling the build. Eclipse Jetty uses http://maven.apache.org/[Apache Maven] for managing the project metadata and controlling the build.
@ -76,7 +76,7 @@ All relevant dependencies should be downloaded into your local repository automa
____ ____
[NOTE] [NOTE]
Jetty has a great many test cases that run through the course of its build. Many of these tests spin up embedded instances of Jetty itself and it is not uncommon to see hundreds or more instances of Jetty start and stop during tests. Jetty has a great many test cases that run through the course of its build. Many of these tests spin up embedded instances of Jetty itself, and it is not uncommon to see hundreds or more instances of Jetty start and stop during tests.
Periodically we find some test cases to be more time dependent than they should be and this results in intermittent test failures. Periodically we find some test cases to be more time dependent than they should be and this results in intermittent test failures.
You can help track these down by opening an https://github.com/eclipse/jetty.project/issues[Issue]. You can help track these down by opening an https://github.com/eclipse/jetty.project/issues[Issue].
____ ____

View File

@ -15,16 +15,16 @@
== Participate in the Documentation == Participate in the Documentation
Another wonderful way to help with Eclipse Jetty is to help contribute to our corpus of documentation. Another wonderful way to help with Eclipse Jetty is to help contribute to our corpus of documentation.
We have taken every every to reduce the barriers to contributing to our documentation and many contributors find our documentation as a low key way to participate and learn the process. We have made an effort to reduce the barriers to contributing to our documentation, and many contributors find our documentation as a low-key way to participate and learn the process.
[[cg-documentation-format]] [[cg-documentation-format]]
=== Source Control and Maven Build. === Source Control and Maven Build.
The Jetty documentation is a module within the overall Jetty project and is build as a part of the standard build process. The Jetty documentation is a module within the overall Jetty project and is build as a part of the standard build process.
As such to checkout the documentation you can follow the link:#t-community-source[same process] as checking out Jetty itself. As such to checkout the documentation you can follow the link:#cg-community-source[same process] as checking out Jetty itself.
As a part of the main Jetty project the documentation is build through the link:#t-community-build[same process] as Jetty. As a part of the main Jetty project the documentation is build through the link:#cg-contributing-build[same process] as Jetty.
However, it is a more independent module and can be worked with much simpler by building strictly the jetty-documentation module. However, it is a more independent module and can be worked with much simpler by building strictly the jetty-documentation module.
[source, screen, subs="{sub-order}"] [source, screen, subs="{sub-order}"]

View File

@ -15,9 +15,8 @@
=== Community === Community
Developers and users alike are welcome to engage the Jetty community. Developers and users alike are welcome to engage the Jetty community.
We all have day jobs here so don't just ask questions and get frustrated if We all have day jobs here so don't just ask questions and get frustrated if no one answers right away.
no one answers right away. Stick around to hear an answer, one is likely coming Stick around to hear an answer, one is likely coming at some point!
at some point!
[[community-mailing-lists]] [[community-mailing-lists]]
==== Mailing Lists ==== Mailing Lists

View File

@ -14,7 +14,7 @@ logging
commons-logging commons-logging
[files] [files]
maven://org.slf4j/jcl-over-slf4j/${slf4j.version}|jcl-over-slf4j-${slf4j.version}.jar maven://org.slf4j/jcl-over-slf4j/${slf4j.version}|lib/logging/jcl-over-slf4j-${slf4j.version}.jar
[lib] [lib]
lib/logging/jcl-over-slf4j-${slf4j.version}.jar lib/logging/jcl-over-slf4j-${slf4j.version}.jar

View File

@ -14,10 +14,10 @@ logging
log4j log4j
[files] [files]
maven://org.slf4j/jcl-over-slf4j/${slf4j.version}|jcl-over-slf4j-${slf4j.version}.jar maven://org.slf4j/log4j-over-slf4j/${slf4j.version}|lib/logging/log4j-over-slf4j-${slf4j.version}.jar
[lib] [lib]
lib/logging/log4j-to-slf4j-${slf4j.version}.jar lib/logging/log4j-over-slf4j-${slf4j.version}.jar
[license] [license]
SLF4J is distributed under the MIT License. SLF4J is distributed under the MIT License.

View File

@ -48,7 +48,7 @@ import org.eclipse.jetty.util.UrlEncoded;
*/ */
public interface HttpURI public interface HttpURI
{ {
enum Ambiguous enum Violation
{ {
/** /**
* URI contains ambiguous path segments e.g. {@code /foo/%2e%2e/bar} * URI contains ambiguous path segments e.g. {@code /foo/%2e%2e/bar}
@ -73,7 +73,12 @@ public interface HttpURI
/** /**
* URI contains ambiguous path parameters within a URI segment e.g. {@code /foo/..;/bar} * URI contains ambiguous path parameters within a URI segment e.g. {@code /foo/..;/bar}
*/ */
PARAM PARAM,
/**
* Contains UTF16 encodings
*/
UTF16
} }
static Mutable build() static Mutable build()
@ -165,6 +170,11 @@ public interface HttpURI
*/ */
boolean isAmbiguous(); boolean isAmbiguous();
/**
* @return True if the URI has any Violations.
*/
boolean hasViolations();
/** /**
* @return True if the URI has a possibly ambiguous segment like '..;' or '%2e%2e' * @return True if the URI has a possibly ambiguous segment like '..;' or '%2e%2e'
*/ */
@ -190,6 +200,8 @@ public interface HttpURI
*/ */
boolean hasAmbiguousEncoding(); boolean hasAmbiguousEncoding();
boolean hasUtf16Encoding();
default URI toURI() default URI toURI()
{ {
try try
@ -215,7 +227,7 @@ public interface HttpURI
private final String _fragment; private final String _fragment;
private String _uri; private String _uri;
private String _decodedPath; private String _decodedPath;
private final EnumSet<Mutable.Ambiguous> _ambiguous = EnumSet.noneOf(Mutable.Ambiguous.class); private final EnumSet<Violation> _violations = EnumSet.noneOf(Violation.class);
private Immutable(Mutable builder) private Immutable(Mutable builder)
{ {
@ -229,7 +241,7 @@ public interface HttpURI
_fragment = builder._fragment; _fragment = builder._fragment;
_uri = builder._uri; _uri = builder._uri;
_decodedPath = builder._decodedPath; _decodedPath = builder._decodedPath;
_ambiguous.addAll(builder._ambiguous); _violations.addAll(builder._violations);
} }
private Immutable(String uri) private Immutable(String uri)
@ -396,37 +408,49 @@ public interface HttpURI
@Override @Override
public boolean isAmbiguous() public boolean isAmbiguous()
{ {
return !_ambiguous.isEmpty(); return !_violations.isEmpty() && !(_violations.size() == 1 && _violations.contains(Violation.UTF16));
}
@Override
public boolean hasViolations()
{
return !_violations.isEmpty();
} }
@Override @Override
public boolean hasAmbiguousSegment() public boolean hasAmbiguousSegment()
{ {
return _ambiguous.contains(Ambiguous.SEGMENT); return _violations.contains(Violation.SEGMENT);
} }
@Override @Override
public boolean hasAmbiguousEmptySegment() public boolean hasAmbiguousEmptySegment()
{ {
return _ambiguous.contains(Ambiguous.EMPTY); return _violations.contains(Violation.EMPTY);
} }
@Override @Override
public boolean hasAmbiguousSeparator() public boolean hasAmbiguousSeparator()
{ {
return _ambiguous.contains(Ambiguous.SEPARATOR); return _violations.contains(Violation.SEPARATOR);
} }
@Override @Override
public boolean hasAmbiguousParameter() public boolean hasAmbiguousParameter()
{ {
return _ambiguous.contains(Ambiguous.PARAM); return _violations.contains(Violation.PARAM);
} }
@Override @Override
public boolean hasAmbiguousEncoding() public boolean hasAmbiguousEncoding()
{ {
return _ambiguous.contains(Ambiguous.ENCODING); return _violations.contains(Violation.ENCODING);
}
@Override
public boolean hasUtf16Encoding()
{
return _violations.contains(Violation.UTF16);
} }
@Override @Override
@ -480,12 +504,18 @@ public interface HttpURI
*/ */
private static final Index<Boolean> __ambiguousSegments = new Index.Builder<Boolean>() private static final Index<Boolean> __ambiguousSegments = new Index.Builder<Boolean>()
.caseSensitive(false) .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) .with(".", Boolean.FALSE)
.with("%2e", Boolean.TRUE)
.with("%u002e", Boolean.TRUE)
.with("..", Boolean.FALSE)
.with(".%2e", Boolean.TRUE)
.with(".%u002e", Boolean.TRUE)
.with("%2e.", Boolean.TRUE)
.with("%2e%2e", Boolean.TRUE)
.with("%2e%u002e", Boolean.TRUE)
.with("%u002e.", Boolean.TRUE)
.with("%u002e%2e", Boolean.TRUE)
.with("%u002e%u002e", Boolean.TRUE)
.build(); .build();
private String _scheme; private String _scheme;
@ -498,7 +528,7 @@ public interface HttpURI
private String _fragment; private String _fragment;
private String _uri; private String _uri;
private String _decodedPath; private String _decodedPath;
private final EnumSet<Ambiguous> _ambiguous = EnumSet.noneOf(Ambiguous.class); private final EnumSet<Violation> _violations = EnumSet.noneOf(Violation.class);
private boolean _emptySegment; private boolean _emptySegment;
private Mutable() private Mutable()
@ -623,7 +653,7 @@ public interface HttpURI
_fragment = null; _fragment = null;
_uri = null; _uri = null;
_decodedPath = null; _decodedPath = null;
_ambiguous.clear(); _violations.clear();
return this; return this;
} }
@ -750,37 +780,49 @@ public interface HttpURI
@Override @Override
public boolean isAmbiguous() public boolean isAmbiguous()
{ {
return !_ambiguous.isEmpty(); return !_violations.isEmpty() && !(_violations.size() == 1 && _violations.contains(Violation.UTF16));
}
@Override
public boolean hasViolations()
{
return !_violations.isEmpty();
} }
@Override @Override
public boolean hasAmbiguousSegment() public boolean hasAmbiguousSegment()
{ {
return _ambiguous.contains(Mutable.Ambiguous.SEGMENT); return _violations.contains(Violation.SEGMENT);
} }
@Override @Override
public boolean hasAmbiguousEmptySegment() public boolean hasAmbiguousEmptySegment()
{ {
return _ambiguous.contains(Ambiguous.EMPTY); return _violations.contains(Violation.EMPTY);
} }
@Override @Override
public boolean hasAmbiguousSeparator() public boolean hasAmbiguousSeparator()
{ {
return _ambiguous.contains(Mutable.Ambiguous.SEPARATOR); return _violations.contains(Violation.SEPARATOR);
} }
@Override @Override
public boolean hasAmbiguousParameter() public boolean hasAmbiguousParameter()
{ {
return _ambiguous.contains(Ambiguous.PARAM); return _violations.contains(Violation.PARAM);
} }
@Override @Override
public boolean hasAmbiguousEncoding() public boolean hasAmbiguousEncoding()
{ {
return _ambiguous.contains(Ambiguous.ENCODING); return _violations.contains(Violation.ENCODING);
}
@Override
public boolean hasUtf16Encoding()
{
return _violations.contains(Violation.UTF16);
} }
public Mutable normalize() public Mutable normalize()
@ -885,9 +927,9 @@ public interface HttpURI
_uri = null; _uri = null;
_decodedPath = uri.getDecodedPath(); _decodedPath = uri.getDecodedPath();
if (uri.hasAmbiguousSeparator()) if (uri.hasAmbiguousSeparator())
_ambiguous.add(Ambiguous.SEPARATOR); _violations.add(Violation.SEPARATOR);
if (uri.hasAmbiguousSegment()) if (uri.hasAmbiguousSegment())
_ambiguous.add(Ambiguous.SEGMENT); _violations.add(Violation.SEGMENT);
return this; return this;
} }
@ -938,9 +980,11 @@ public interface HttpURI
int mark = 0; // the start of the current section being parsed int mark = 0; // the start of the current section being parsed
int pathMark = 0; // the start of the path section int pathMark = 0; // the start of the path section
int segment = 0; // the start of the current segment within the path 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 encodedPath = false; // set to true if the path contains % encoded characters
boolean dot = false; // set to true if the path containers . or .. segments boolean encodedUtf16 = false; // Is the current encoding for UTF16?
int escapedTwo = 0; // state of parsing a %2<x> 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(); int end = uri.length();
_emptySegment = false; _emptySegment = false;
for (int i = 0; i < end; i++) for (int i = 0; i < end; i++)
@ -981,8 +1025,9 @@ public interface HttpURI
state = State.ASTERISK; state = State.ASTERISK;
break; break;
case '%': case '%':
encoded = true; encodedPath = true;
escapedTwo = 1; encodedCharacters = 2;
encodedValue = 0;
mark = pathMark = segment = i; mark = pathMark = segment = i;
state = State.PATH; state = State.PATH;
break; break;
@ -1032,9 +1077,10 @@ public interface HttpURI
state = State.QUERY; state = State.QUERY;
break; break;
case '%': case '%':
// must have be in an encoded path // must have been in an encoded path
encoded = true; encodedPath = true;
escapedTwo = 1; encodedCharacters = 2;
encodedValue = 0;
state = State.PATH; state = State.PATH;
break; break;
case '#': case '#':
@ -1153,6 +1199,34 @@ public interface HttpURI
break; break;
} }
case PATH: case PATH:
{
if (encodedCharacters > 0)
{
if (encodedCharacters == 2 && c == 'u' && !encodedUtf16)
{
_violations.add(Violation.UTF16);
encodedUtf16 = true;
encodedCharacters = 4;
continue;
}
encodedValue = (encodedValue << 4) + TypeUtil.convertHexDigit(c);
if (--encodedCharacters == 0)
{
switch (encodedValue)
{
case '/':
_violations.add(Violation.SEPARATOR);
break;
case '%':
_violations.add(Violation.ENCODING);
break;
default:
break;
}
}
}
else
{ {
switch (c) switch (c)
{ {
@ -1183,27 +1257,15 @@ public interface HttpURI
dot |= segment == i; dot |= segment == i;
break; break;
case '%': case '%':
encoded = true; encodedPath = true;
escapedTwo = 1; encodedUtf16 = false;
break; encodedCharacters = 2;
case '2': encodedValue = 0;
escapedTwo = escapedTwo == 1 ? 2 : 0;
break;
case 'f':
case 'F':
if (escapedTwo == 2)
_ambiguous.add(Ambiguous.SEPARATOR);
escapedTwo = 0;
break;
case '5':
if (escapedTwo == 2)
_ambiguous.add(Ambiguous.ENCODING);
escapedTwo = 0;
break; break;
default: default:
escapedTwo = 0;
break; break;
} }
}
break; break;
} }
case PARAM: case PARAM:
@ -1223,7 +1285,7 @@ public interface HttpURI
state = State.FRAGMENT; state = State.FRAGMENT;
break; break;
case '/': case '/':
encoded = true; encodedPath = true;
segment = i + 1; segment = i + 1;
state = State.PATH; state = State.PATH;
break; break;
@ -1238,11 +1300,26 @@ public interface HttpURI
} }
case QUERY: case QUERY:
{ {
if (c == '#') switch (c)
{ {
case '%':
encodedCharacters = 2;
break;
case 'u':
case 'U':
if (encodedCharacters == 1)
_violations.add(Violation.UTF16);
encodedCharacters = 0;
break;
case '#':
_query = uri.substring(mark, i); _query = uri.substring(mark, i);
mark = i + 1; mark = i + 1;
state = State.FRAGMENT; state = State.FRAGMENT;
encodedCharacters = 0;
break;
default:
encodedCharacters = 0;
break;
} }
break; break;
} }
@ -1300,7 +1377,7 @@ public interface HttpURI
throw new IllegalStateException(state.toString()); throw new IllegalStateException(state.toString());
} }
if (!encoded && !dot) if (!encodedPath && !dot)
{ {
if (_param == null) if (_param == null)
_decodedPath = _path; _decodedPath = _path;
@ -1333,7 +1410,7 @@ public interface HttpURI
// Empty segments are only ambiguous if they are not the last segment // Empty segments are only ambiguous if they are not the last segment
// So if this method is called for any segment and we have previously seen an empty segment, then it was ambiguous // So if this method is called for any segment and we have previously seen an empty segment, then it was ambiguous
if (_emptySegment) if (_emptySegment)
_ambiguous.add(Ambiguous.EMPTY); _violations.add(Violation.EMPTY);
if (end == segment) if (end == segment)
{ {
@ -1344,7 +1421,7 @@ public interface HttpURI
// If this empty segment is the first segment then it is ambiguous. // If this empty segment is the first segment then it is ambiguous.
if (segment == 0) if (segment == 0)
{ {
_ambiguous.add(Ambiguous.EMPTY); _violations.add(Violation.EMPTY);
return; return;
} }
@ -1361,12 +1438,12 @@ public interface HttpURI
if (ambiguous == Boolean.TRUE) if (ambiguous == Boolean.TRUE)
{ {
// The segment is always ambiguous. // The segment is always ambiguous.
_ambiguous.add(Ambiguous.SEGMENT); _violations.add(Violation.SEGMENT);
} }
else if (param && ambiguous == Boolean.FALSE) else if (param && ambiguous == Boolean.FALSE)
{ {
// The segment is ambiguous only when followed by a parameter. // The segment is ambiguous only when followed by a parameter.
_ambiguous.add(Ambiguous.PARAM); _violations.add(Violation.PARAM);
} }
} }
} }

View File

@ -69,7 +69,11 @@ public final class UriCompliance implements ComplianceViolation.Mode
/** /**
* Allow Non canonical ambiguous paths. eg <code>/foo/x%2f%2e%2e%/bar</code> provided to applications as <code>/foo/x/../bar</code> * Allow Non canonical ambiguous paths. eg <code>/foo/x%2f%2e%2e%/bar</code> provided to applications as <code>/foo/x/../bar</code>
*/ */
NON_CANONICAL_AMBIGUOUS_PATHS("https://tools.ietf.org/html/rfc3986#section-3.3", "Non canonical ambiguous paths"); NON_CANONICAL_AMBIGUOUS_PATHS("https://tools.ietf.org/html/rfc3986#section-3.3", "Non canonical ambiguous paths"),
/**
* Allow UTF-16 encoding eg <code>/foo%u2192bar</code>.
*/
UTF16_ENCODINGS("https://www.w3.org/International/iri-edit/draft-duerst-iri.html#anchor29", "UTF16 encoding");
private final String _url; private final String _url;
private final String _description; private final String _description;
@ -109,9 +113,15 @@ public final class UriCompliance implements ComplianceViolation.Mode
/** /**
* LEGACY compliance mode that models Jetty-9.4 behavior by allowing {@link Violation#AMBIGUOUS_PATH_SEGMENT}, * LEGACY compliance mode that models Jetty-9.4 behavior by allowing {@link Violation#AMBIGUOUS_PATH_SEGMENT},
* {@link Violation#AMBIGUOUS_EMPTY_SEGMENT}, {@link Violation#AMBIGUOUS_PATH_SEPARATOR} and {@link Violation#AMBIGUOUS_PATH_ENCODING}. * {@link Violation#AMBIGUOUS_EMPTY_SEGMENT}, {@link Violation#AMBIGUOUS_PATH_SEPARATOR}, {@link Violation#AMBIGUOUS_PATH_ENCODING}
* and {@link Violation#UTF16_ENCODINGS}
*/ */
public static final UriCompliance LEGACY = new UriCompliance("LEGACY", of(Violation.AMBIGUOUS_PATH_SEGMENT, Violation.AMBIGUOUS_PATH_SEPARATOR, Violation.AMBIGUOUS_PATH_ENCODING, Violation.AMBIGUOUS_EMPTY_SEGMENT)); public static final UriCompliance LEGACY = new UriCompliance("LEGACY",
of(Violation.AMBIGUOUS_PATH_SEGMENT,
Violation.AMBIGUOUS_PATH_SEPARATOR,
Violation.AMBIGUOUS_PATH_ENCODING,
Violation.AMBIGUOUS_EMPTY_SEGMENT,
Violation.UTF16_ENCODINGS));
/** /**
* Compliance mode that exactly follows RFC3986, including allowing all additional ambiguous URI Violations, * Compliance mode that exactly follows RFC3986, including allowing all additional ambiguous URI Violations,

View File

@ -1,247 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.http;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.stream.Stream;
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;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
public class HttpURIParseTest
{
public static Stream<Arguments> data()
{
return Stream.of(
// Nothing but path
Arguments.of("path", null, null, "-1", "path", null, null, null),
Arguments.of("path/path", null, null, "-1", "path/path", null, null, null),
Arguments.of("%65ncoded/path", null, null, "-1", "%65ncoded/path", null, null, null),
// Basic path reference
Arguments.of("/path/to/context", null, null, "-1", "/path/to/context", null, null, null),
// Basic with encoded query
Arguments.of("http://example.com/path/to/context;param?query=%22value%22#fragment", "http", "example.com", "-1", "/path/to/context;param", "param", "query=%22value%22", "fragment"),
Arguments.of("http://[::1]/path/to/context;param?query=%22value%22#fragment", "http", "[::1]", "-1", "/path/to/context;param", "param", "query=%22value%22", "fragment"),
// Basic with parameters and query
Arguments.of("http://example.com:8080/path/to/context;param?query=%22value%22#fragment", "http", "example.com", "8080", "/path/to/context;param", "param", "query=%22value%22", "fragment"),
Arguments.of("http://[::1]:8080/path/to/context;param?query=%22value%22#fragment", "http", "[::1]", "8080", "/path/to/context;param", "param", "query=%22value%22", "fragment"),
// Path References
Arguments.of("/path/info", null, null, null, "/path/info", null, null, null),
Arguments.of("/path/info#fragment", null, null, null, "/path/info", null, null, "fragment"),
Arguments.of("/path/info?query", null, null, null, "/path/info", null, "query", null),
Arguments.of("/path/info?query#fragment", null, null, null, "/path/info", null, "query", "fragment"),
Arguments.of("/path/info;param", null, null, null, "/path/info;param", "param", null, null),
Arguments.of("/path/info;param#fragment", null, null, null, "/path/info;param", "param", null, "fragment"),
Arguments.of("/path/info;param?query", null, null, null, "/path/info;param", "param", "query", null),
Arguments.of("/path/info;param?query#fragment", null, null, null, "/path/info;param", "param", "query", "fragment"),
Arguments.of("/path/info;a=b/foo;c=d", null, null, null, "/path/info;a=b/foo;c=d", "c=d", null, null), // TODO #405
// Protocol Less (aka scheme-less) URIs
Arguments.of("//host/path/info", null, "host", null, "/path/info", null, null, null),
Arguments.of("//user@host/path/info", null, "host", null, "/path/info", null, null, null),
Arguments.of("//user@host:8080/path/info", null, "host", "8080", "/path/info", null, null, null),
Arguments.of("//host:8080/path/info", null, "host", "8080", "/path/info", null, null, null),
// Host Less
Arguments.of("http:/path/info", "http", null, null, "/path/info", null, null, null),
Arguments.of("http:/path/info#fragment", "http", null, null, "/path/info", null, null, "fragment"),
Arguments.of("http:/path/info?query", "http", null, null, "/path/info", null, "query", null),
Arguments.of("http:/path/info?query#fragment", "http", null, null, "/path/info", null, "query", "fragment"),
Arguments.of("http:/path/info;param", "http", null, null, "/path/info;param", "param", null, null),
Arguments.of("http:/path/info;param#fragment", "http", null, null, "/path/info;param", "param", null, "fragment"),
Arguments.of("http:/path/info;param?query", "http", null, null, "/path/info;param", "param", "query", null),
Arguments.of("http:/path/info;param?query#fragment", "http", null, null, "/path/info;param", "param", "query", "fragment"),
// Everything and the kitchen sink
Arguments.of("http://user@host:8080/path/info;param?query#fragment", "http", "host", "8080", "/path/info;param", "param", "query", "fragment"),
Arguments.of("xxxxx://user@host:8080/path/info;param?query#fragment", "xxxxx", "host", "8080", "/path/info;param", "param", "query", "fragment"),
// No host, parameter with no content
Arguments.of("http:///;?#", "http", null, null, "/;", "", "", ""),
// Path with query that has no value
Arguments.of("/path/info?a=?query", null, null, null, "/path/info", null, "a=?query", null),
// Path with query alt syntax
Arguments.of("/path/info?a=;query", null, null, null, "/path/info", null, "a=;query", null),
// URI with host character
Arguments.of("/@path/info", null, null, null, "/@path/info", null, null, null),
Arguments.of("/user@path/info", null, null, null, "/user@path/info", null, null, null),
Arguments.of("//user@host/info", null, "host", null, "/info", null, null, null),
Arguments.of("//@host/info", null, "host", null, "/info", null, null, null),
Arguments.of("@host/info", null, null, null, "@host/info", null, null, null),
// Scheme-less, with host and port (overlapping with path)
Arguments.of("//host:8080//", null, "host", "8080", "//", null, null, null),
// File reference
Arguments.of("file:///path/info", "file", null, null, "/path/info", null, null, null),
Arguments.of("file:/path/info", "file", null, null, "/path/info", null, null, null),
// Bad URI (no scheme, no host, no path)
Arguments.of("//", null, null, null, null, null, null, null),
// Simple localhost references
Arguments.of("http://localhost/", "http", "localhost", null, "/", null, null, null),
Arguments.of("http://localhost:8080/", "http", "localhost", "8080", "/", null, null, null),
Arguments.of("http://localhost/?x=y", "http", "localhost", null, "/", null, "x=y", null),
// Simple path with parameter
Arguments.of("/;param", null, null, null, "/;param", "param", null, null),
Arguments.of(";param", null, null, null, ";param", "param", null, null),
// Simple path with query
Arguments.of("/?x=y", null, null, null, "/", null, "x=y", null),
Arguments.of("/?abc=test", null, null, null, "/", null, "abc=test", null),
// Simple path with fragment
Arguments.of("/#fragment", null, null, null, "/", null, null, "fragment"),
// Simple IPv4 host with port (default path)
Arguments.of("http://192.0.0.1:8080/", "http", "192.0.0.1", "8080", "/", null, null, null),
// Simple IPv6 host with port (default path)
Arguments.of("http://[2001:db8::1]:8080/", "http", "[2001:db8::1]", "8080", "/", null, null, null),
// IPv6 authenticated host with port (default path)
Arguments.of("http://user@[2001:db8::1]:8080/", "http", "[2001:db8::1]", "8080", "/", null, null, null),
// Simple IPv6 host no port (default path)
Arguments.of("http://[2001:db8::1]/", "http", "[2001:db8::1]", null, "/", null, null, null),
// Scheme-less IPv6, host with port (default path)
Arguments.of("//[2001:db8::1]:8080/", null, "[2001:db8::1]", "8080", "/", null, null, null),
// Interpreted as relative path of "*" (no host/port/scheme/query/fragment)
Arguments.of("*", null, null, null, "*", null, null, null),
// Path detection Tests (seen from JSP/JSTL and <c:url> use)
Arguments.of("http://host:8080/path/info?q1=v1&q2=v2", "http", "host", "8080", "/path/info", null, "q1=v1&q2=v2", null),
Arguments.of("/path/info?q1=v1&q2=v2", null, null, null, "/path/info", null, "q1=v1&q2=v2", null),
Arguments.of("/info?q1=v1&q2=v2", null, null, null, "/info", null, "q1=v1&q2=v2", null),
Arguments.of("info?q1=v1&q2=v2", null, null, null, "info", null, "q1=v1&q2=v2", null),
Arguments.of("info;q1=v1?q2=v2", null, null, null, "info;q1=v1", "q1=v1", "q2=v2", null),
// Path-less, query only (seen from JSP/JSTL and <c:url> use)
Arguments.of("?q1=v1&q2=v2", null, null, null, "", null, "q1=v1&q2=v2", null)
);
}
@ParameterizedTest
@MethodSource("data")
public void testParseString(String input, String scheme, String host, Integer port, String path, String param, String query, String fragment) throws Exception
{
HttpURI httpUri = HttpURI.from(input);
try
{
new URI(input);
// URI is valid (per java.net.URI parsing)
// Test case sanity check
assertThat("[" + input + "] expected path (test case) cannot be null", path, notNullValue());
// Assert expectations
assertThat("[" + input + "] .scheme", httpUri.getScheme(), is(scheme));
assertThat("[" + input + "] .host", httpUri.getHost(), is(host));
assertThat("[" + input + "] .port", httpUri.getPort(), is(port == null ? -1 : port));
assertThat("[" + input + "] .path", httpUri.getPath(), is(path));
assertThat("[" + input + "] .param", httpUri.getParam(), is(param));
assertThat("[" + input + "] .query", httpUri.getQuery(), is(query));
assertThat("[" + input + "] .fragment", httpUri.getFragment(), is(fragment));
assertThat("[" + input + "] .toString", httpUri.toString(), is(input));
}
catch (URISyntaxException e)
{
// Assert HttpURI values for invalid URI (such as "//")
assertThat("[" + input + "] .scheme", httpUri.getScheme(), is(nullValue()));
assertThat("[" + input + "] .host", httpUri.getHost(), is(nullValue()));
assertThat("[" + input + "] .port", httpUri.getPort(), is(-1));
assertThat("[" + input + "] .path", httpUri.getPath(), is(nullValue()));
assertThat("[" + input + "] .param", httpUri.getParam(), is(nullValue()));
assertThat("[" + input + "] .query", httpUri.getQuery(), is(nullValue()));
assertThat("[" + input + "] .fragment", httpUri.getFragment(), is(nullValue()));
}
}
@ParameterizedTest
@MethodSource("data")
public void testParseURI(String input, String scheme, String host, Integer port, String path, String param, String query, String fragment) throws Exception
{
URI javaUri = null;
try
{
javaUri = new URI(input);
}
catch (URISyntaxException ignore)
{
// Ignore, as URI is invalid anyway
}
assumeTrue(javaUri != null, "Skipping, not a valid input URI: " + input);
HttpURI httpUri = HttpURI.from(javaUri);
assertThat("[" + input + "] .scheme", httpUri.getScheme(), is(scheme));
assertThat("[" + input + "] .host", httpUri.getHost(), is(host));
assertThat("[" + input + "] .port", httpUri.getPort(), is(port == null ? -1 : port));
assertThat("[" + input + "] .path", httpUri.getPath(), is(path));
assertThat("[" + input + "] .param", httpUri.getParam(), is(param));
assertThat("[" + input + "] .query", httpUri.getQuery(), is(query));
assertThat("[" + input + "] .fragment", httpUri.getFragment(), is(fragment));
assertThat("[" + input + "] .toString", httpUri.toString(), is(input));
}
@ParameterizedTest
@MethodSource("data")
public void testCompareToJavaNetURI(String input, String scheme, String host, Integer port, String path, String param, String query, String fragment) throws Exception
{
URI javaUri = null;
try
{
javaUri = new URI(input);
}
catch (URISyntaxException ignore)
{
// Ignore, as URI is invalid anyway
}
assumeTrue(javaUri != null, "Skipping, not a valid input URI");
HttpURI httpUri = HttpURI.from(javaUri);
assertThat("[" + input + "] .scheme", httpUri.getScheme(), is(javaUri.getScheme()));
assertThat("[" + input + "] .host", httpUri.getHost(), is(javaUri.getHost()));
assertThat("[" + input + "] .port", httpUri.getPort(), is(javaUri.getPort()));
assertThat("[" + input + "] .path", httpUri.getPath(), is(javaUri.getRawPath()));
// Not Relevant for java.net.URI -- assertThat("["+input+"] .param", httpUri.getParam(), is(param));
assertThat("[" + input + "] .query", httpUri.getQuery(), is(javaUri.getRawQuery()));
assertThat("[" + input + "] .fragment", httpUri.getFragment(), is(javaUri.getFragment()));
assertThat("[" + input + "] .toString", httpUri.toString(), is(javaUri.toASCIIString()));
}
}

View File

@ -13,11 +13,13 @@
package org.eclipse.jetty.http; package org.eclipse.jetty.http;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays; import java.util.Arrays;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.eclipse.jetty.http.HttpURI.Ambiguous; import org.eclipse.jetty.http.HttpURI.Violation;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.Arguments;
@ -25,12 +27,14 @@ import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
public class HttpURITest public class HttpURITest
{ {
@ -323,109 +327,140 @@ public class HttpURITest
return Arrays.stream(new Object[][] return Arrays.stream(new Object[][]
{ {
// Simple path example // Simple path example
{"http://host/path/info", "/path/info", EnumSet.noneOf(Ambiguous.class)}, {"http://host/path/info", "/path/info", EnumSet.noneOf(Violation.class)},
{"//host/path/info", "/path/info", EnumSet.noneOf(Ambiguous.class)}, {"//host/path/info", "/path/info", EnumSet.noneOf(Violation.class)},
{"/path/info", "/path/info", EnumSet.noneOf(Ambiguous.class)}, {"/path/info", "/path/info", EnumSet.noneOf(Violation.class)},
// Scheme & host containing unusual valid characters
{"ht..tp://host/path/info", "/path/info", EnumSet.noneOf(Violation.class)},
{"ht1.2+..-3.4tp://127.0.0.1:8080/path/info", "/path/info", EnumSet.noneOf(Violation.class)},
{"http://h%2est/path/info", "/path/info", EnumSet.noneOf(Violation.class)},
{"http://h..est/path/info", "/path/info", EnumSet.noneOf(Violation.class)},
// legal non ambiguous relative paths // legal non ambiguous relative paths
{"http://host/../path/info", null, EnumSet.noneOf(Ambiguous.class)}, {"http://host/../path/info", null, EnumSet.noneOf(Violation.class)},
{"http://host/path/../info", "/info", EnumSet.noneOf(Ambiguous.class)}, {"http://host/path/../info", "/info", EnumSet.noneOf(Violation.class)},
{"http://host/path/./info", "/path/info", EnumSet.noneOf(Ambiguous.class)}, {"http://host/path/./info", "/path/info", EnumSet.noneOf(Violation.class)},
{"//host/path/../info", "/info", EnumSet.noneOf(Ambiguous.class)}, {"//host/path/../info", "/info", EnumSet.noneOf(Violation.class)},
{"//host/path/./info", "/path/info", EnumSet.noneOf(Ambiguous.class)}, {"//host/path/./info", "/path/info", EnumSet.noneOf(Violation.class)},
{"/path/../info", "/info", EnumSet.noneOf(Ambiguous.class)}, {"/path/../info", "/info", EnumSet.noneOf(Violation.class)},
{"/path/./info", "/path/info", EnumSet.noneOf(Ambiguous.class)}, {"/path/./info", "/path/info", EnumSet.noneOf(Violation.class)},
{"path/../info", "info", EnumSet.noneOf(Ambiguous.class)}, {"path/../info", "info", EnumSet.noneOf(Violation.class)},
{"path/./info", "path/info", EnumSet.noneOf(Ambiguous.class)}, {"path/./info", "path/info", EnumSet.noneOf(Violation.class)},
// encoded paths
{"/f%6f%6F/bar", "/foo/bar", EnumSet.noneOf(Violation.class)},
{"/f%u006f%u006F/bar", "/foo/bar", EnumSet.of(Violation.UTF16)},
// illegal paths // illegal paths
{"//host/../path/info", null, EnumSet.noneOf(Ambiguous.class)}, {"//host/../path/info", null, EnumSet.noneOf(Violation.class)},
{"/../path/info", null, EnumSet.noneOf(Ambiguous.class)}, {"/../path/info", null, EnumSet.noneOf(Violation.class)},
{"../path/info", null, EnumSet.noneOf(Ambiguous.class)}, {"../path/info", null, EnumSet.noneOf(Violation.class)},
{"/path/%XX/info", null, EnumSet.noneOf(Ambiguous.class)}, {"/path/%XX/info", null, EnumSet.noneOf(Violation.class)},
{"/path/%2/F/info", null, EnumSet.noneOf(Ambiguous.class)}, {"/path/%2/F/info", null, EnumSet.noneOf(Violation.class)},
{"/path/%/info", null, EnumSet.noneOf(Violation.class)},
{"/path/%u000X/info", null, EnumSet.noneOf(Violation.class)},
// ambiguous dot encodings // ambiguous dot encodings
{"scheme://host/path/%2e/info", "/path/./info", EnumSet.of(Ambiguous.SEGMENT)}, {"scheme://host/path/%2e/info", "/path/./info", EnumSet.of(Violation.SEGMENT)},
{"scheme:/path/%2e/info", "/path/./info", EnumSet.of(Ambiguous.SEGMENT)}, {"scheme:/path/%2e/info", "/path/./info", EnumSet.of(Violation.SEGMENT)},
{"/path/%2e/info", "/path/./info", EnumSet.of(Ambiguous.SEGMENT)}, {"/path/%2e/info", "/path/./info", EnumSet.of(Violation.SEGMENT)},
{"path/%2e/info/", "path/./info/", EnumSet.of(Ambiguous.SEGMENT)}, {"path/%2e/info/", "path/./info/", EnumSet.of(Violation.SEGMENT)},
{"/path/%2e%2e/info", "/path/../info", EnumSet.of(Ambiguous.SEGMENT)}, {"/path/%2e%2e/info", "/path/../info", EnumSet.of(Violation.SEGMENT)},
{"/path/%2e%2e;/info", "/path/../info", EnumSet.of(Ambiguous.SEGMENT)}, {"/path/%2e%2e;/info", "/path/../info", EnumSet.of(Violation.SEGMENT)},
{"/path/%2e%2e;param/info", "/path/../info", EnumSet.of(Ambiguous.SEGMENT)}, {"/path/%2e%2e;param/info", "/path/../info", EnumSet.of(Violation.SEGMENT)},
{"/path/%2e%2e;param;other/info;other", "/path/../info", EnumSet.of(Ambiguous.SEGMENT)}, {"/path/%2e%2e;param;other/info;other", "/path/../info", EnumSet.of(Violation.SEGMENT)},
{"%2e/info", "./info", EnumSet.of(Ambiguous.SEGMENT)}, {"%2e/info", "./info", EnumSet.of(Violation.SEGMENT)},
{"%2e%2e/info", "../info", EnumSet.of(Ambiguous.SEGMENT)}, {"%u002e/info", "./info", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
{"%2e%2e;/info", "../info", EnumSet.of(Ambiguous.SEGMENT)}, {"%2e%2e/info", "../info", EnumSet.of(Violation.SEGMENT)},
{"%2e", ".", EnumSet.of(Ambiguous.SEGMENT)}, {"%u002e%u002e/info", "../info", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
{"%2e.", "..", EnumSet.of(Ambiguous.SEGMENT)}, {"%2e%2e;/info", "../info", EnumSet.of(Violation.SEGMENT)},
{".%2e", "..", EnumSet.of(Ambiguous.SEGMENT)}, {"%u002e%u002e;/info", "../info", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
{"%2e%2e", "..", EnumSet.of(Ambiguous.SEGMENT)}, {"%2e", ".", EnumSet.of(Violation.SEGMENT)},
{"%u002e", ".", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
{"%2e.", "..", EnumSet.of(Violation.SEGMENT)},
{"%u002e.", "..", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
{".%2e", "..", EnumSet.of(Violation.SEGMENT)},
{".%u002e", "..", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
{"%2e%2e", "..", EnumSet.of(Violation.SEGMENT)},
{"%u002e%u002e", "..", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
{"%2e%u002e", "..", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
{"%u002e%2e", "..", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
// empty segment treated as ambiguous // empty segment treated as ambiguous
{"/foo//bar", "/foo//bar", EnumSet.of(Ambiguous.EMPTY)}, {"/foo//bar", "/foo//bar", EnumSet.of(Violation.EMPTY)},
{"/foo//../bar", "/foo/bar", EnumSet.of(Ambiguous.EMPTY)}, {"/foo//../bar", "/foo/bar", EnumSet.of(Violation.EMPTY)},
{"/foo///../../../bar", "/bar", EnumSet.of(Ambiguous.EMPTY)}, {"/foo///../../../bar", "/bar", EnumSet.of(Violation.EMPTY)},
{"/foo/./../bar", "/bar", EnumSet.noneOf(Ambiguous.class)}, {"/foo/./../bar", "/bar", EnumSet.noneOf(Violation.class)},
{"/foo//./bar", "/foo//bar", EnumSet.of(Ambiguous.EMPTY)}, {"/foo//./bar", "/foo//bar", EnumSet.of(Violation.EMPTY)},
{"foo/bar", "foo/bar", EnumSet.noneOf(Ambiguous.class)}, {"foo/bar", "foo/bar", EnumSet.noneOf(Violation.class)},
{"foo;/bar", "foo/bar", EnumSet.noneOf(Ambiguous.class)}, {"foo;/bar", "foo/bar", EnumSet.noneOf(Violation.class)},
{";/bar", "/bar", EnumSet.of(Ambiguous.EMPTY)}, {";/bar", "/bar", EnumSet.of(Violation.EMPTY)},
{";?n=v", "", EnumSet.of(Ambiguous.EMPTY)}, {";?n=v", "", EnumSet.of(Violation.EMPTY)},
{"?n=v", "", EnumSet.noneOf(Ambiguous.class)}, {"?n=v", "", EnumSet.noneOf(Violation.class)},
{"#n=v", "", EnumSet.noneOf(Ambiguous.class)}, {"#n=v", "", EnumSet.noneOf(Violation.class)},
{"", "", EnumSet.noneOf(Ambiguous.class)}, {"", "", EnumSet.noneOf(Violation.class)},
{"http:/foo", "/foo", EnumSet.noneOf(Ambiguous.class)}, {"http:/foo", "/foo", EnumSet.noneOf(Violation.class)},
// ambiguous parameter inclusions // ambiguous parameter inclusions
{"/path/.;/info", "/path/./info", EnumSet.of(Ambiguous.PARAM)}, {"/path/.;/info", "/path/./info", EnumSet.of(Violation.PARAM)},
{"/path/.;param/info", "/path/./info", EnumSet.of(Ambiguous.PARAM)}, {"/path/.;param/info", "/path/./info", EnumSet.of(Violation.PARAM)},
{"/path/..;/info", "/path/../info", EnumSet.of(Ambiguous.PARAM)}, {"/path/..;/info", "/path/../info", EnumSet.of(Violation.PARAM)},
{"/path/..;param/info", "/path/../info", EnumSet.of(Ambiguous.PARAM)}, {"/path/..;param/info", "/path/../info", EnumSet.of(Violation.PARAM)},
{".;/info", "./info", EnumSet.of(Ambiguous.PARAM)}, {".;/info", "./info", EnumSet.of(Violation.PARAM)},
{".;param/info", "./info", EnumSet.of(Ambiguous.PARAM)}, {".;param/info", "./info", EnumSet.of(Violation.PARAM)},
{"..;/info", "../info", EnumSet.of(Ambiguous.PARAM)}, {"..;/info", "../info", EnumSet.of(Violation.PARAM)},
{"..;param/info", "../info", EnumSet.of(Ambiguous.PARAM)}, {"..;param/info", "../info", EnumSet.of(Violation.PARAM)},
// ambiguous segment separators // ambiguous segment separators
{"/path/%2f/info", "/path///info", EnumSet.of(Ambiguous.SEPARATOR)}, {"/path/%2f/info", "/path///info", EnumSet.of(Violation.SEPARATOR)},
{"%2f/info", "//info", EnumSet.of(Ambiguous.SEPARATOR)}, {"%2f/info", "//info", EnumSet.of(Violation.SEPARATOR)},
{"%2F/info", "//info", EnumSet.of(Ambiguous.SEPARATOR)}, {"%2F/info", "//info", EnumSet.of(Violation.SEPARATOR)},
{"/path/%2f../info", "/path//../info", EnumSet.of(Ambiguous.SEPARATOR)}, {"/path/%2f../info", "/path//../info", EnumSet.of(Violation.SEPARATOR)},
// ambiguous encoding // ambiguous encoding
{"/path/%25/info", "/path/%/info", EnumSet.of(Ambiguous.ENCODING)}, {"/path/%25/info", "/path/%/info", EnumSet.of(Violation.ENCODING)},
{"%25/info", "%/info", EnumSet.of(Ambiguous.ENCODING)}, {"/path/%u0025/info", "/path/%/info", EnumSet.of(Violation.ENCODING, Violation.UTF16)},
{"/path/%25../info", "/path/%../info", EnumSet.of(Ambiguous.ENCODING)}, {"%25/info", "%/info", EnumSet.of(Violation.ENCODING)},
{"/path/%25../info", "/path/%../info", EnumSet.of(Violation.ENCODING)},
{"/path/%u0025../info", "/path/%../info", EnumSet.of(Violation.ENCODING, Violation.UTF16)},
// combinations // combinations
{"/path/%2f/..;/info", "/path///../info", EnumSet.of(Ambiguous.SEPARATOR, Ambiguous.PARAM)}, {"/path/%2f/..;/info", "/path///../info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM)},
{"/path/%2f/..;/%2e/info", "/path///.././info", EnumSet.of(Ambiguous.SEPARATOR, Ambiguous.PARAM, Ambiguous.SEGMENT)}, {"/path/%u002f/..;/info", "/path///../info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM, Violation.UTF16)},
{"/path/%2f/..;/%2e/info", "/path///.././info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM, Violation.SEGMENT)},
// Non ascii characters // Non ascii characters
// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
{"http://localhost:9000/x\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32", "/x\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32", EnumSet.noneOf(Ambiguous.class)}, {"http://localhost:9000/x\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32", "/x\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32", EnumSet.noneOf(Violation.class)},
{"http://localhost:9000/\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32", "/\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32", EnumSet.noneOf(Ambiguous.class)}, {"http://localhost:9000/\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32", "/\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32", EnumSet.noneOf(Violation.class)},
// @checkstyle-enable-check : AvoidEscapedUnicodeCharactersCheck // @checkstyle-enable-check : AvoidEscapedUnicodeCharactersCheck
}).map(Arguments::of); }).map(Arguments::of);
} }
@ParameterizedTest @ParameterizedTest
@MethodSource("decodePathTests") @MethodSource("decodePathTests")
public void testDecodedPath(String input, String decodedPath, EnumSet<Ambiguous> expected) public void testDecodedPath(String input, String decodedPath, EnumSet<Violation> expected)
{ {
try try
{ {
HttpURI uri = HttpURI.from(input); HttpURI uri = HttpURI.from(input);
assertThat(uri.getDecodedPath(), is(decodedPath)); assertThat(uri.getDecodedPath(), is(decodedPath));
assertThat(uri.isAmbiguous(), is(!expected.isEmpty())); EnumSet<Violation> ambiguous = EnumSet.copyOf(expected);
assertThat(uri.hasAmbiguousSegment(), is(expected.contains(Ambiguous.SEGMENT))); ambiguous.retainAll(EnumSet.complementOf(EnumSet.of(Violation.UTF16)));
assertThat(uri.hasAmbiguousSeparator(), is(expected.contains(Ambiguous.SEPARATOR)));
assertThat(uri.hasAmbiguousParameter(), is(expected.contains(Ambiguous.PARAM))); assertThat(uri.isAmbiguous(), is(!ambiguous.isEmpty()));
assertThat(uri.hasAmbiguousEncoding(), is(expected.contains(Ambiguous.ENCODING))); assertThat(uri.hasAmbiguousSegment(), is(ambiguous.contains(Violation.SEGMENT)));
assertThat(uri.hasAmbiguousSeparator(), is(ambiguous.contains(Violation.SEPARATOR)));
assertThat(uri.hasAmbiguousParameter(), is(ambiguous.contains(Violation.PARAM)));
assertThat(uri.hasAmbiguousEncoding(), is(ambiguous.contains(Violation.ENCODING)));
assertThat(uri.hasUtf16Encoding(), is(expected.contains(Violation.UTF16)));
} }
catch (Exception e) catch (Exception e)
{ {
if (decodedPath != null)
e.printStackTrace();
assertThat(decodedPath, nullValue()); assertThat(decodedPath, nullValue());
} }
} }
@ -435,13 +470,13 @@ public class HttpURITest
return Arrays.stream(new Object[][] return Arrays.stream(new Object[][]
{ {
// Simple path example // Simple path example
{"/path/info", "/path/info", EnumSet.noneOf(Ambiguous.class)}, {"/path/info", "/path/info", EnumSet.noneOf(Violation.class)},
// legal non ambiguous relative paths // legal non ambiguous relative paths
{"/path/../info", "/info", EnumSet.noneOf(Ambiguous.class)}, {"/path/../info", "/info", EnumSet.noneOf(Violation.class)},
{"/path/./info", "/path/info", EnumSet.noneOf(Ambiguous.class)}, {"/path/./info", "/path/info", EnumSet.noneOf(Violation.class)},
{"path/../info", "info", EnumSet.noneOf(Ambiguous.class)}, {"path/../info", "info", EnumSet.noneOf(Violation.class)},
{"path/./info", "path/info", EnumSet.noneOf(Ambiguous.class)}, {"path/./info", "path/info", EnumSet.noneOf(Violation.class)},
// illegal paths // illegal paths
{"/../path/info", null, null}, {"/../path/info", null, null},
@ -450,82 +485,82 @@ public class HttpURITest
{"/path/%2/F/info", null, null}, {"/path/%2/F/info", null, null},
// ambiguous dot encodings // ambiguous dot encodings
{"/path/%2e/info", "/path/./info", EnumSet.of(Ambiguous.SEGMENT)}, {"/path/%2e/info", "/path/./info", EnumSet.of(Violation.SEGMENT)},
{"path/%2e/info/", "path/./info/", EnumSet.of(Ambiguous.SEGMENT)}, {"path/%2e/info/", "path/./info/", EnumSet.of(Violation.SEGMENT)},
{"/path/%2e%2e/info", "/path/../info", EnumSet.of(Ambiguous.SEGMENT)}, {"/path/%2e%2e/info", "/path/../info", EnumSet.of(Violation.SEGMENT)},
{"/path/%2e%2e;/info", "/path/../info", EnumSet.of(Ambiguous.SEGMENT)}, {"/path/%2e%2e;/info", "/path/../info", EnumSet.of(Violation.SEGMENT)},
{"/path/%2e%2e;param/info", "/path/../info", EnumSet.of(Ambiguous.SEGMENT)}, {"/path/%2e%2e;param/info", "/path/../info", EnumSet.of(Violation.SEGMENT)},
{"/path/%2e%2e;param;other/info;other", "/path/../info", EnumSet.of(Ambiguous.SEGMENT)}, {"/path/%2e%2e;param;other/info;other", "/path/../info", EnumSet.of(Violation.SEGMENT)},
{"%2e/info", "./info", EnumSet.of(Ambiguous.SEGMENT)}, {"%2e/info", "./info", EnumSet.of(Violation.SEGMENT)},
{"%2e%2e/info", "../info", EnumSet.of(Ambiguous.SEGMENT)}, {"%2e%2e/info", "../info", EnumSet.of(Violation.SEGMENT)},
{"%2e%2e;/info", "../info", EnumSet.of(Ambiguous.SEGMENT)}, {"%2e%2e;/info", "../info", EnumSet.of(Violation.SEGMENT)},
{"%2e", ".", EnumSet.of(Ambiguous.SEGMENT)}, {"%2e", ".", EnumSet.of(Violation.SEGMENT)},
{"%2e.", "..", EnumSet.of(Ambiguous.SEGMENT)}, {"%2e.", "..", EnumSet.of(Violation.SEGMENT)},
{".%2e", "..", EnumSet.of(Ambiguous.SEGMENT)}, {".%2e", "..", EnumSet.of(Violation.SEGMENT)},
{"%2e%2e", "..", EnumSet.of(Ambiguous.SEGMENT)}, {"%2e%2e", "..", EnumSet.of(Violation.SEGMENT)},
// empty segment treated as ambiguous // empty segment treated as ambiguous
{"/", "/", EnumSet.noneOf(Ambiguous.class)}, {"/", "/", EnumSet.noneOf(Violation.class)},
{"/#", "/", EnumSet.noneOf(Ambiguous.class)}, {"/#", "/", EnumSet.noneOf(Violation.class)},
{"/path", "/path", EnumSet.noneOf(Ambiguous.class)}, {"/path", "/path", EnumSet.noneOf(Violation.class)},
{"/path/", "/path/", EnumSet.noneOf(Ambiguous.class)}, {"/path/", "/path/", EnumSet.noneOf(Violation.class)},
{"//", "//", EnumSet.of(Ambiguous.EMPTY)}, {"//", "//", EnumSet.of(Violation.EMPTY)},
{"/foo//", "/foo//", EnumSet.of(Ambiguous.EMPTY)}, {"/foo//", "/foo//", EnumSet.of(Violation.EMPTY)},
{"/foo//bar", "/foo//bar", EnumSet.of(Ambiguous.EMPTY)}, {"/foo//bar", "/foo//bar", EnumSet.of(Violation.EMPTY)},
{"//foo/bar", "//foo/bar", EnumSet.of(Ambiguous.EMPTY)}, {"//foo/bar", "//foo/bar", EnumSet.of(Violation.EMPTY)},
{"/foo?bar", "/foo", EnumSet.noneOf(Ambiguous.class)}, {"/foo?bar", "/foo", EnumSet.noneOf(Violation.class)},
{"/foo#bar", "/foo", EnumSet.noneOf(Ambiguous.class)}, {"/foo#bar", "/foo", EnumSet.noneOf(Violation.class)},
{"/foo;bar", "/foo", EnumSet.noneOf(Ambiguous.class)}, {"/foo;bar", "/foo", EnumSet.noneOf(Violation.class)},
{"/foo/?bar", "/foo/", EnumSet.noneOf(Ambiguous.class)}, {"/foo/?bar", "/foo/", EnumSet.noneOf(Violation.class)},
{"/foo/#bar", "/foo/", EnumSet.noneOf(Ambiguous.class)}, {"/foo/#bar", "/foo/", EnumSet.noneOf(Violation.class)},
{"/foo/;param", "/foo/", EnumSet.noneOf(Ambiguous.class)}, {"/foo/;param", "/foo/", EnumSet.noneOf(Violation.class)},
{"/foo/;param/bar", "/foo//bar", EnumSet.of(Ambiguous.EMPTY)}, {"/foo/;param/bar", "/foo//bar", EnumSet.of(Violation.EMPTY)},
{"/foo//bar", "/foo//bar", EnumSet.of(Ambiguous.EMPTY)}, {"/foo//bar", "/foo//bar", EnumSet.of(Violation.EMPTY)},
{"/foo//bar//", "/foo//bar//", EnumSet.of(Ambiguous.EMPTY)}, {"/foo//bar//", "/foo//bar//", EnumSet.of(Violation.EMPTY)},
{"//foo//bar//", "//foo//bar//", EnumSet.of(Ambiguous.EMPTY)}, {"//foo//bar//", "//foo//bar//", EnumSet.of(Violation.EMPTY)},
{"/foo//../bar", "/foo/bar", EnumSet.of(Ambiguous.EMPTY)}, {"/foo//../bar", "/foo/bar", EnumSet.of(Violation.EMPTY)},
{"/foo///../../../bar", "/bar", EnumSet.of(Ambiguous.EMPTY)}, {"/foo///../../../bar", "/bar", EnumSet.of(Violation.EMPTY)},
{"/foo/./../bar", "/bar", EnumSet.noneOf(Ambiguous.class)}, {"/foo/./../bar", "/bar", EnumSet.noneOf(Violation.class)},
{"/foo//./bar", "/foo//bar", EnumSet.of(Ambiguous.EMPTY)}, {"/foo//./bar", "/foo//bar", EnumSet.of(Violation.EMPTY)},
{"foo/bar", "foo/bar", EnumSet.noneOf(Ambiguous.class)}, {"foo/bar", "foo/bar", EnumSet.noneOf(Violation.class)},
{"foo;/bar", "foo/bar", EnumSet.noneOf(Ambiguous.class)}, {"foo;/bar", "foo/bar", EnumSet.noneOf(Violation.class)},
{";/bar", "/bar", EnumSet.of(Ambiguous.EMPTY)}, {";/bar", "/bar", EnumSet.of(Violation.EMPTY)},
{";?n=v", "", EnumSet.of(Ambiguous.EMPTY)}, {";?n=v", "", EnumSet.of(Violation.EMPTY)},
{"?n=v", "", EnumSet.noneOf(Ambiguous.class)}, {"?n=v", "", EnumSet.noneOf(Violation.class)},
{"#n=v", "", EnumSet.noneOf(Ambiguous.class)}, {"#n=v", "", EnumSet.noneOf(Violation.class)},
{"", "", EnumSet.noneOf(Ambiguous.class)}, {"", "", EnumSet.noneOf(Violation.class)},
// ambiguous parameter inclusions // ambiguous parameter inclusions
{"/path/.;/info", "/path/./info", EnumSet.of(Ambiguous.PARAM)}, {"/path/.;/info", "/path/./info", EnumSet.of(Violation.PARAM)},
{"/path/.;param/info", "/path/./info", EnumSet.of(Ambiguous.PARAM)}, {"/path/.;param/info", "/path/./info", EnumSet.of(Violation.PARAM)},
{"/path/..;/info", "/path/../info", EnumSet.of(Ambiguous.PARAM)}, {"/path/..;/info", "/path/../info", EnumSet.of(Violation.PARAM)},
{"/path/..;param/info", "/path/../info", EnumSet.of(Ambiguous.PARAM)}, {"/path/..;param/info", "/path/../info", EnumSet.of(Violation.PARAM)},
{".;/info", "./info", EnumSet.of(Ambiguous.PARAM)}, {".;/info", "./info", EnumSet.of(Violation.PARAM)},
{".;param/info", "./info", EnumSet.of(Ambiguous.PARAM)}, {".;param/info", "./info", EnumSet.of(Violation.PARAM)},
{"..;/info", "../info", EnumSet.of(Ambiguous.PARAM)}, {"..;/info", "../info", EnumSet.of(Violation.PARAM)},
{"..;param/info", "../info", EnumSet.of(Ambiguous.PARAM)}, {"..;param/info", "../info", EnumSet.of(Violation.PARAM)},
// ambiguous segment separators // ambiguous segment separators
{"/path/%2f/info", "/path///info", EnumSet.of(Ambiguous.SEPARATOR)}, {"/path/%2f/info", "/path///info", EnumSet.of(Violation.SEPARATOR)},
{"%2f/info", "//info", EnumSet.of(Ambiguous.SEPARATOR)}, {"%2f/info", "//info", EnumSet.of(Violation.SEPARATOR)},
{"%2F/info", "//info", EnumSet.of(Ambiguous.SEPARATOR)}, {"%2F/info", "//info", EnumSet.of(Violation.SEPARATOR)},
{"/path/%2f../info", "/path//../info", EnumSet.of(Ambiguous.SEPARATOR)}, {"/path/%2f../info", "/path//../info", EnumSet.of(Violation.SEPARATOR)},
// ambiguous encoding // ambiguous encoding
{"/path/%25/info", "/path/%/info", EnumSet.of(Ambiguous.ENCODING)}, {"/path/%25/info", "/path/%/info", EnumSet.of(Violation.ENCODING)},
{"%25/info", "%/info", EnumSet.of(Ambiguous.ENCODING)}, {"%25/info", "%/info", EnumSet.of(Violation.ENCODING)},
{"/path/%25../info", "/path/%../info", EnumSet.of(Ambiguous.ENCODING)}, {"/path/%25../info", "/path/%../info", EnumSet.of(Violation.ENCODING)},
// combinations // combinations
{"/path/%2f/..;/info", "/path///../info", EnumSet.of(Ambiguous.SEPARATOR, Ambiguous.PARAM)}, {"/path/%2f/..;/info", "/path///../info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM)},
{"/path/%2f/..;/%2e/info", "/path///.././info", EnumSet.of(Ambiguous.SEPARATOR, Ambiguous.PARAM, Ambiguous.SEGMENT)}, {"/path/%2f/..;/%2e/info", "/path///.././info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM, Violation.SEGMENT)},
{"/path/%2f/%25/..;/%2e//info", "/path///%/.././/info", EnumSet.of(Ambiguous.SEPARATOR, Ambiguous.PARAM, Ambiguous.SEGMENT, Ambiguous.ENCODING, Ambiguous.EMPTY)}, {"/path/%2f/%25/..;/%2e//info", "/path///%/.././/info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM, Violation.SEGMENT, Violation.ENCODING, Violation.EMPTY)},
}).map(Arguments::of); }).map(Arguments::of);
} }
@ParameterizedTest @ParameterizedTest
@MethodSource("testPathQueryTests") @MethodSource("testPathQueryTests")
public void testPathQuery(String input, String decodedPath, EnumSet<Ambiguous> expected) public void testPathQuery(String input, String decodedPath, EnumSet<Violation> expected)
{ {
// If expected is null then it is a bad URI and should throw. // If expected is null then it is a bad URI and should throw.
if (expected == null) if (expected == null)
@ -537,10 +572,225 @@ public class HttpURITest
HttpURI uri = HttpURI.build().pathQuery(input); HttpURI uri = HttpURI.build().pathQuery(input);
assertThat(uri.getDecodedPath(), is(decodedPath)); assertThat(uri.getDecodedPath(), is(decodedPath));
assertThat(uri.isAmbiguous(), is(!expected.isEmpty())); assertThat(uri.isAmbiguous(), is(!expected.isEmpty()));
assertThat(uri.hasAmbiguousEmptySegment(), is(expected.contains(Ambiguous.EMPTY))); assertThat(uri.hasAmbiguousEmptySegment(), is(expected.contains(Violation.EMPTY)));
assertThat(uri.hasAmbiguousSegment(), is(expected.contains(Ambiguous.SEGMENT))); assertThat(uri.hasAmbiguousSegment(), is(expected.contains(Violation.SEGMENT)));
assertThat(uri.hasAmbiguousSeparator(), is(expected.contains(Ambiguous.SEPARATOR))); assertThat(uri.hasAmbiguousSeparator(), is(expected.contains(Violation.SEPARATOR)));
assertThat(uri.hasAmbiguousParameter(), is(expected.contains(Ambiguous.PARAM))); assertThat(uri.hasAmbiguousParameter(), is(expected.contains(Violation.PARAM)));
assertThat(uri.hasAmbiguousEncoding(), is(expected.contains(Ambiguous.ENCODING))); assertThat(uri.hasAmbiguousEncoding(), is(expected.contains(Violation.ENCODING)));
}
public static Stream<Arguments> parseData()
{
return Stream.of(
// Nothing but path
Arguments.of("path", null, null, "-1", "path", null, null, null),
Arguments.of("path/path", null, null, "-1", "path/path", null, null, null),
Arguments.of("%65ncoded/path", null, null, "-1", "%65ncoded/path", null, null, null),
// Basic path reference
Arguments.of("/path/to/context", null, null, "-1", "/path/to/context", null, null, null),
// Basic with encoded query
Arguments.of("http://example.com/path/to/context;param?query=%22value%22#fragment", "http", "example.com", "-1", "/path/to/context;param", "param", "query=%22value%22", "fragment"),
Arguments.of("http://[::1]/path/to/context;param?query=%22value%22#fragment", "http", "[::1]", "-1", "/path/to/context;param", "param", "query=%22value%22", "fragment"),
// Basic with parameters and query
Arguments.of("http://example.com:8080/path/to/context;param?query=%22value%22#fragment", "http", "example.com", "8080", "/path/to/context;param", "param", "query=%22value%22", "fragment"),
Arguments.of("http://[::1]:8080/path/to/context;param?query=%22value%22#fragment", "http", "[::1]", "8080", "/path/to/context;param", "param", "query=%22value%22", "fragment"),
// Path References
Arguments.of("/path/info", null, null, null, "/path/info", null, null, null),
Arguments.of("/path/info#fragment", null, null, null, "/path/info", null, null, "fragment"),
Arguments.of("/path/info?query", null, null, null, "/path/info", null, "query", null),
Arguments.of("/path/info?query#fragment", null, null, null, "/path/info", null, "query", "fragment"),
Arguments.of("/path/info;param", null, null, null, "/path/info;param", "param", null, null),
Arguments.of("/path/info;param#fragment", null, null, null, "/path/info;param", "param", null, "fragment"),
Arguments.of("/path/info;param?query", null, null, null, "/path/info;param", "param", "query", null),
Arguments.of("/path/info;param?query#fragment", null, null, null, "/path/info;param", "param", "query", "fragment"),
Arguments.of("/path/info;a=b/foo;c=d", null, null, null, "/path/info;a=b/foo;c=d", "c=d", null, null), // TODO #405
// Protocol Less (aka scheme-less) URIs
Arguments.of("//host/path/info", null, "host", null, "/path/info", null, null, null),
Arguments.of("//user@host/path/info", null, "host", null, "/path/info", null, null, null),
Arguments.of("//user@host:8080/path/info", null, "host", "8080", "/path/info", null, null, null),
Arguments.of("//host:8080/path/info", null, "host", "8080", "/path/info", null, null, null),
// Host Less
Arguments.of("http:/path/info", "http", null, null, "/path/info", null, null, null),
Arguments.of("http:/path/info#fragment", "http", null, null, "/path/info", null, null, "fragment"),
Arguments.of("http:/path/info?query", "http", null, null, "/path/info", null, "query", null),
Arguments.of("http:/path/info?query#fragment", "http", null, null, "/path/info", null, "query", "fragment"),
Arguments.of("http:/path/info;param", "http", null, null, "/path/info;param", "param", null, null),
Arguments.of("http:/path/info;param#fragment", "http", null, null, "/path/info;param", "param", null, "fragment"),
Arguments.of("http:/path/info;param?query", "http", null, null, "/path/info;param", "param", "query", null),
Arguments.of("http:/path/info;param?query#fragment", "http", null, null, "/path/info;param", "param", "query", "fragment"),
// Everything and the kitchen sink
Arguments.of("http://user@host:8080/path/info;param?query#fragment", "http", "host", "8080", "/path/info;param", "param", "query", "fragment"),
Arguments.of("xxxxx://user@host:8080/path/info;param?query#fragment", "xxxxx", "host", "8080", "/path/info;param", "param", "query", "fragment"),
// No host, parameter with no content
Arguments.of("http:///;?#", "http", null, null, "/;", "", "", ""),
// Path with query that has no value
Arguments.of("/path/info?a=?query", null, null, null, "/path/info", null, "a=?query", null),
// Path with query alt syntax
Arguments.of("/path/info?a=;query", null, null, null, "/path/info", null, "a=;query", null),
// URI with host character
Arguments.of("/@path/info", null, null, null, "/@path/info", null, null, null),
Arguments.of("/user@path/info", null, null, null, "/user@path/info", null, null, null),
Arguments.of("//user@host/info", null, "host", null, "/info", null, null, null),
Arguments.of("//@host/info", null, "host", null, "/info", null, null, null),
Arguments.of("@host/info", null, null, null, "@host/info", null, null, null),
// Scheme-less, with host and port (overlapping with path)
Arguments.of("//host:8080//", null, "host", "8080", "//", null, null, null),
// File reference
Arguments.of("file:///path/info", "file", null, null, "/path/info", null, null, null),
Arguments.of("file:/path/info", "file", null, null, "/path/info", null, null, null),
// Bad URI (no scheme, no host, no path)
Arguments.of("//", null, null, null, null, null, null, null),
// Simple localhost references
Arguments.of("http://localhost/", "http", "localhost", null, "/", null, null, null),
Arguments.of("http://localhost:8080/", "http", "localhost", "8080", "/", null, null, null),
Arguments.of("http://localhost/?x=y", "http", "localhost", null, "/", null, "x=y", null),
// Simple path with parameter
Arguments.of("/;param", null, null, null, "/;param", "param", null, null),
Arguments.of(";param", null, null, null, ";param", "param", null, null),
// Simple path with query
Arguments.of("/?x=y", null, null, null, "/", null, "x=y", null),
Arguments.of("/?abc=test", null, null, null, "/", null, "abc=test", null),
// Simple path with fragment
Arguments.of("/#fragment", null, null, null, "/", null, null, "fragment"),
// Simple IPv4 host with port (default path)
Arguments.of("http://192.0.0.1:8080/", "http", "192.0.0.1", "8080", "/", null, null, null),
// Simple IPv6 host with port (default path)
Arguments.of("http://[2001:db8::1]:8080/", "http", "[2001:db8::1]", "8080", "/", null, null, null),
// IPv6 authenticated host with port (default path)
Arguments.of("http://user@[2001:db8::1]:8080/", "http", "[2001:db8::1]", "8080", "/", null, null, null),
// Simple IPv6 host no port (default path)
Arguments.of("http://[2001:db8::1]/", "http", "[2001:db8::1]", null, "/", null, null, null),
// Scheme-less IPv6, host with port (default path)
Arguments.of("//[2001:db8::1]:8080/", null, "[2001:db8::1]", "8080", "/", null, null, null),
// Interpreted as relative path of "*" (no host/port/scheme/query/fragment)
Arguments.of("*", null, null, null, "*", null, null, null),
// Path detection Tests (seen from JSP/JSTL and <c:url> use)
Arguments.of("http://host:8080/path/info?q1=v1&q2=v2", "http", "host", "8080", "/path/info", null, "q1=v1&q2=v2", null),
Arguments.of("/path/info?q1=v1&q2=v2", null, null, null, "/path/info", null, "q1=v1&q2=v2", null),
Arguments.of("/info?q1=v1&q2=v2", null, null, null, "/info", null, "q1=v1&q2=v2", null),
Arguments.of("info?q1=v1&q2=v2", null, null, null, "info", null, "q1=v1&q2=v2", null),
Arguments.of("info;q1=v1?q2=v2", null, null, null, "info;q1=v1", "q1=v1", "q2=v2", null),
// Path-less, query only (seen from JSP/JSTL and <c:url> use)
Arguments.of("?q1=v1&q2=v2", null, null, null, "", null, "q1=v1&q2=v2", null)
);
}
@ParameterizedTest
@MethodSource("parseData")
public void testParseString(String input, String scheme, String host, Integer port, String path, String param, String query, String fragment)
{
HttpURI httpUri = HttpURI.from(input);
try
{
new URI(input);
// URI is valid (per java.net.URI parsing)
// Test case sanity check
assertThat("[" + input + "] expected path (test case) cannot be null", path, notNullValue());
// Assert expectations
assertThat("[" + input + "] .scheme", httpUri.getScheme(), is(scheme));
assertThat("[" + input + "] .host", httpUri.getHost(), is(host));
assertThat("[" + input + "] .port", httpUri.getPort(), is(port == null ? -1 : port));
assertThat("[" + input + "] .path", httpUri.getPath(), is(path));
assertThat("[" + input + "] .param", httpUri.getParam(), is(param));
assertThat("[" + input + "] .query", httpUri.getQuery(), is(query));
assertThat("[" + input + "] .fragment", httpUri.getFragment(), is(fragment));
assertThat("[" + input + "] .toString", httpUri.toString(), is(input));
}
catch (URISyntaxException e)
{
// Assert HttpURI values for invalid URI (such as "//")
assertThat("[" + input + "] .scheme", httpUri.getScheme(), is(nullValue()));
assertThat("[" + input + "] .host", httpUri.getHost(), is(nullValue()));
assertThat("[" + input + "] .port", httpUri.getPort(), is(-1));
assertThat("[" + input + "] .path", httpUri.getPath(), is(nullValue()));
assertThat("[" + input + "] .param", httpUri.getParam(), is(nullValue()));
assertThat("[" + input + "] .query", httpUri.getQuery(), is(nullValue()));
assertThat("[" + input + "] .fragment", httpUri.getFragment(), is(nullValue()));
}
}
@ParameterizedTest
@MethodSource("parseData")
public void testParseURI(String input, String scheme, String host, Integer port, String path, String param, String query, String fragment) throws Exception
{
URI javaUri = null;
try
{
javaUri = new URI(input);
}
catch (URISyntaxException ignore)
{
// Ignore, as URI is invalid anyway
}
assumeTrue(javaUri != null, "Skipping, not a valid input URI: " + input);
HttpURI httpUri = HttpURI.from(javaUri);
assertThat("[" + input + "] .scheme", httpUri.getScheme(), is(scheme));
assertThat("[" + input + "] .host", httpUri.getHost(), is(host));
assertThat("[" + input + "] .port", httpUri.getPort(), is(port == null ? -1 : port));
assertThat("[" + input + "] .path", httpUri.getPath(), is(path));
assertThat("[" + input + "] .param", httpUri.getParam(), is(param));
assertThat("[" + input + "] .query", httpUri.getQuery(), is(query));
assertThat("[" + input + "] .fragment", httpUri.getFragment(), is(fragment));
assertThat("[" + input + "] .toString", httpUri.toString(), is(input));
}
@ParameterizedTest
@MethodSource("parseData")
public void testCompareToJavaNetURI(String input, String scheme, String host, Integer port, String path, String param, String query, String fragment) throws Exception
{
URI javaUri = null;
try
{
javaUri = new URI(input);
}
catch (URISyntaxException ignore)
{
// Ignore, as URI is invalid anyway
}
assumeTrue(javaUri != null, "Skipping, not a valid input URI");
HttpURI httpUri = HttpURI.from(javaUri);
assertThat("[" + input + "] .scheme", httpUri.getScheme(), is(javaUri.getScheme()));
assertThat("[" + input + "] .host", httpUri.getHost(), is(javaUri.getHost()));
assertThat("[" + input + "] .port", httpUri.getPort(), is(javaUri.getPort()));
assertThat("[" + input + "] .path", httpUri.getPath(), is(javaUri.getRawPath()));
// Not Relevant for java.net.URI -- assertThat("["+input+"] .param", httpUri.getParam(), is(param));
assertThat("[" + input + "] .query", httpUri.getQuery(), is(javaUri.getRawQuery()));
assertThat("[" + input + "] .fragment", httpUri.getFragment(), is(javaUri.getFragment()));
assertThat("[" + input + "] .toString", httpUri.toString(), is(javaUri.toASCIIString()));
} }
} }

View File

@ -1687,23 +1687,32 @@ public class Request implements HttpServletRequest
_method = request.getMethod(); _method = request.getMethod();
_httpFields = request.getFields(); _httpFields = request.getFields();
final HttpURI uri = request.getURI(); final HttpURI uri = request.getURI();
boolean ambiguous = false;
UriCompliance compliance = null; UriCompliance compliance = null;
boolean ambiguous = uri.isAmbiguous(); if (uri.hasViolations())
{
ambiguous = uri.isAmbiguous();
compliance = _channel == null || _channel.getHttpConfiguration() == null ? null : _channel.getHttpConfiguration().getUriCompliance();
if (compliance != null)
{
if (!compliance.allows(UriCompliance.Violation.UTF16_ENCODINGS) && uri.hasUtf16Encoding())
throw new BadMessageException("UTF16 % encoding not supported");
if (ambiguous) if (ambiguous)
{ {
compliance = _channel == null || _channel.getHttpConfiguration() == null ? null : _channel.getHttpConfiguration().getUriCompliance(); if (uri.hasAmbiguousSegment() && !compliance.allows(UriCompliance.Violation.AMBIGUOUS_PATH_SEGMENT))
if (uri.hasAmbiguousSegment() && (compliance == null || !compliance.allows(UriCompliance.Violation.AMBIGUOUS_PATH_SEGMENT)))
throw new BadMessageException("Ambiguous segment in URI"); throw new BadMessageException("Ambiguous segment in URI");
if (uri.hasAmbiguousEmptySegment() && (compliance == null || !compliance.allows(UriCompliance.Violation.AMBIGUOUS_EMPTY_SEGMENT))) if (uri.hasAmbiguousEmptySegment() && !compliance.allows(UriCompliance.Violation.AMBIGUOUS_EMPTY_SEGMENT))
throw new BadMessageException("Ambiguous empty segment in URI"); throw new BadMessageException("Ambiguous empty segment in URI");
if (uri.hasAmbiguousSeparator() && (compliance == null || !compliance.allows(UriCompliance.Violation.AMBIGUOUS_PATH_SEPARATOR))) if (uri.hasAmbiguousSeparator() && !compliance.allows(UriCompliance.Violation.AMBIGUOUS_PATH_SEPARATOR))
throw new BadMessageException("Ambiguous segment in URI"); throw new BadMessageException("Ambiguous segment in URI");
if (uri.hasAmbiguousParameter() && (compliance == null || !compliance.allows(UriCompliance.Violation.AMBIGUOUS_PATH_PARAMETER))) if (uri.hasAmbiguousParameter() && !compliance.allows(UriCompliance.Violation.AMBIGUOUS_PATH_PARAMETER))
throw new BadMessageException("Ambiguous path parameter in URI"); throw new BadMessageException("Ambiguous path parameter in URI");
if (uri.hasAmbiguousEncoding() && (compliance == null || !compliance.allows(UriCompliance.Violation.AMBIGUOUS_PATH_ENCODING))) if (uri.hasAmbiguousEncoding() && !compliance.allows(UriCompliance.Violation.AMBIGUOUS_PATH_ENCODING))
throw new BadMessageException("Ambiguous path encoding in URI"); throw new BadMessageException("Ambiguous path encoding in URI");
} }
}
}
if (uri.isAbsolute() && uri.hasAuthority() && uri.getPath() != null) if (uri.isAbsolute() && uri.hasAuthority() && uri.getPath() != null)
{ {

View File

@ -14,7 +14,6 @@
package org.eclipse.jetty.server; package org.eclipse.jetty.server;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileReader; import java.io.FileReader;
import java.io.IOException; import java.io.IOException;
@ -37,7 +36,6 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream; import java.util.stream.Stream;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.MultipartConfigElement; import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.ServletInputStream; import jakarta.servlet.ServletInputStream;
@ -69,8 +67,6 @@ import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.server.session.Session; import org.eclipse.jetty.server.session.Session;
import org.eclipse.jetty.server.session.SessionData; import org.eclipse.jetty.server.session.SessionData;
import org.eclipse.jetty.server.session.SessionHandler; import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.BufferUtil;
@ -78,7 +74,6 @@ import org.eclipse.jetty.util.IO;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -1638,6 +1633,19 @@ public class RequestTest
assertEquals(0, request.getParameterMap().size()); assertEquals(0, request.getParameterMap().size());
} }
@Test
public void testEncoding() throws Exception
{
_handler._checker = (request, response) -> "/foo/bar".equals(request.getPathInfo());
String request = "GET /f%6f%6F/b%u0061r HTTP/1.0\r\n" +
"Host: whatever\r\n" +
"\r\n";
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.DEFAULT);
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.LEGACY);
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
}
@Test @Test
public void testAmbiguousParameters() throws Exception public void testAmbiguousParameters() throws Exception
{ {

View File

@ -470,8 +470,7 @@ public class URIUtil
char u = path.charAt(i + 1); char u = path.charAt(i + 1);
if (u == 'u') if (u == 'u')
{ {
// TODO remove %u support in jetty-10 // UTF16 encoding is only supported with UriCompliance.Violation.UTF16_ENCODINGS.
// this is wrong. This is a codepoint not a char
builder.append((char)(0xffff & TypeUtil.parseInt(path, i + 2, 4, 16))); builder.append((char)(0xffff & TypeUtil.parseInt(path, i + 2, 4, 16)));
i += 5; i += 5;
} }
@ -558,8 +557,7 @@ public class URIUtil
char u = path.charAt(i + 1); char u = path.charAt(i + 1);
if (u == 'u') if (u == 'u')
{ {
// TODO remove %u encoding support in jetty-10 // UTF16 encoding is only supported with UriCompliance.Violation.UTF16_ENCODINGS.
// This is wrong. This is a codepoint not a char
builder.append((char)(0xffff & TypeUtil.parseInt(path, i + 2, 4, 16))); builder.append((char)(0xffff & TypeUtil.parseInt(path, i + 2, 4, 16)));
i += 5; i += 5;
} }

View File

@ -309,16 +309,22 @@ public class WebAppContextTest
assertThat(HttpTester.parseResponse(connector.getResponse("GET /test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.OK_200)); assertThat(HttpTester.parseResponse(connector.getResponse("GET /test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.OK_200));
assertThat(HttpTester.parseResponse(connector.getResponse("GET /%2e/%2e/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.OK_200)); assertThat(HttpTester.parseResponse(connector.getResponse("GET /%2e/%2e/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.OK_200));
assertThat(HttpTester.parseResponse(connector.getResponse("GET /%u002e/%u002e/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.OK_200));
assertThat(HttpTester.parseResponse(connector.getResponse("GET /foo/%2e%2e/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.OK_200)); assertThat(HttpTester.parseResponse(connector.getResponse("GET /foo/%2e%2e/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.OK_200));
assertThat(HttpTester.parseResponse(connector.getResponse("GET /foo/%u002e%u002e/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.OK_200));
assertThat(HttpTester.parseResponse(connector.getResponse("GET /WEB-INF HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404)); assertThat(HttpTester.parseResponse(connector.getResponse("GET /WEB-INF HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404));
assertThat(HttpTester.parseResponse(connector.getResponse("GET /WEB-INF/ HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404)); assertThat(HttpTester.parseResponse(connector.getResponse("GET /WEB-INF/ HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404));
assertThat(HttpTester.parseResponse(connector.getResponse("GET /WEB-INF/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404)); assertThat(HttpTester.parseResponse(connector.getResponse("GET /WEB-INF/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404));
assertThat(HttpTester.parseResponse(connector.getResponse("GET /web-inf/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404)); assertThat(HttpTester.parseResponse(connector.getResponse("GET /web-inf/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404));
assertThat(HttpTester.parseResponse(connector.getResponse("GET /%2e/WEB-INF/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404)); assertThat(HttpTester.parseResponse(connector.getResponse("GET /%2e/WEB-INF/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404));
assertThat(HttpTester.parseResponse(connector.getResponse("GET /%u002e/WEB-INF/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404));
assertThat(HttpTester.parseResponse(connector.getResponse("GET /%2e/%2e/WEB-INF/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404)); assertThat(HttpTester.parseResponse(connector.getResponse("GET /%2e/%2e/WEB-INF/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404));
assertThat(HttpTester.parseResponse(connector.getResponse("GET /%u002e/%u002e/WEB-INF/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404));
assertThat(HttpTester.parseResponse(connector.getResponse("GET /foo/%2e%2e/WEB-INF/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404)); assertThat(HttpTester.parseResponse(connector.getResponse("GET /foo/%2e%2e/WEB-INF/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404));
assertThat(HttpTester.parseResponse(connector.getResponse("GET /foo/%u002e%u002e/WEB-INF/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404));
assertThat(HttpTester.parseResponse(connector.getResponse("GET /%2E/WEB-INF/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404)); assertThat(HttpTester.parseResponse(connector.getResponse("GET /%2E/WEB-INF/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404));
assertThat(HttpTester.parseResponse(connector.getResponse("GET /%u002E/WEB-INF/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404));
assertThat(HttpTester.parseResponse(connector.getResponse("GET //WEB-INF/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404)); assertThat(HttpTester.parseResponse(connector.getResponse("GET //WEB-INF/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404));
assertThat(HttpTester.parseResponse(connector.getResponse("GET /WEB-INF%2ftest.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404)); assertThat(HttpTester.parseResponse(connector.getResponse("GET /WEB-INF%2ftest.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404));
} }

View File

@ -0,0 +1,187 @@
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.tests.distribution;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.http.HttpStatus;
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.containsString;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class LoggingOptionsTests extends AbstractJettyHomeTest
{
public static Stream<Arguments> validLoggingModules()
{
return Stream.of(
Arguments.of("logging-jetty",
Arrays.asList(
"\\$\\{jetty.home\\}/lib/logging/slf4j-api-.*\\.jar",
"\\$\\{jetty.home\\}/lib/logging/jetty-slf4j-impl-.*\\.jar"),
Arrays.asList(
"logging/slf4j",
"logging-jetty"
)
),
Arguments.of("logging-logback",
Arrays.asList(
"\\$\\{jetty.home\\}/lib/logging/slf4j-api-.*\\.jar",
"\\$\\{jetty.base\\}/lib/logging/logback-classic-.*\\.jar",
"\\$\\{jetty.base\\}/lib/logging/logback-core-.*\\.jar"
),
Arrays.asList(
"logging/slf4j",
"logging-logback"
)
),
Arguments.of("logging-jul",
Arrays.asList(
"\\$\\{jetty.home\\}/lib/logging/slf4j-api-.*\\.jar",
"\\$\\{jetty.base\\}/lib/logging/slf4j-jdk14-.*\\.jar"
),
Arrays.asList(
"logging/slf4j",
"logging-jul"
)
),
Arguments.of("logging-log4j1",
Arrays.asList(
"\\$\\{jetty.home\\}/lib/logging/slf4j-api-.*\\.jar",
"\\$\\{jetty.base\\}/lib/logging/slf4j-log4j12-.*\\.jar"
),
Arrays.asList(
"logging/slf4j",
"logging-log4j1"
)
),
Arguments.of("logging-log4j2",
Arrays.asList(
"\\$\\{jetty.home\\}/lib/logging/slf4j-api-.*\\.jar",
"\\$\\{jetty.base\\}/lib/logging/log4j-slf4j18-impl-.*\\.jar",
"\\$\\{jetty.base\\}/lib/logging/log4j-api-.*\\.jar",
"\\$\\{jetty.base\\}/lib/logging/log4j-core-.*\\.jar"
),
Arrays.asList(
"logging/slf4j",
"logging-log4j2"
)
),
// Disabled, as slf4j noop is not supported by output/log monitoring of AbstractJettyHomeTest
/* Arguments.of("logging-noop",
Arrays.asList(
"\\$\\{jetty.home\\}/lib/logging/slf4j-api-.*\\.jar"
), Arrays.asList(
"logging/slf4j",
"logging-log4j2"
)
),*/
Arguments.of("logging-logback,logging-jcl-capture,logging-jul-capture,logging-log4j1-capture",
Arrays.asList(
"\\$\\{jetty.home\\}/lib/logging/slf4j-api-.*\\.jar",
"\\$\\{jetty.base\\}/lib/logging/logback-classic-.*\\.jar",
"\\$\\{jetty.base\\}/lib/logging/logback-core-.*\\.jar",
"\\$\\{jetty.base\\}/lib/logging/jcl-over-slf4j-.*\\.jar",
"\\$\\{jetty.base\\}/lib/logging/jul-to-slf4j-.*\\.jar",
"\\$\\{jetty.base\\}/lib/logging/log4j-over-slf4j-.*\\.jar"
),
Arrays.asList(
"logging/slf4j",
"logging-logback",
"logging-jcl-capture",
"logging-jul-capture",
"logging-log4j1-capture"
)
)
);
}
@ParameterizedTest(name = "[{index}] {0}")
@MethodSource("validLoggingModules")
public void testLoggingConfiguration(String loggingModules, List<String> expectedClasspathEntries, List<String> expectedEnabledModules) throws Exception
{
Path jettyBase = newTestJettyBaseDirectory();
String jettyVersion = System.getProperty("jettyVersion");
JettyHomeTester distribution = JettyHomeTester.Builder.newInstance()
.jettyVersion(jettyVersion)
.jettyBase(jettyBase)
.mavenLocalRepository(System.getProperty("mavenRepoPath"))
.build();
String[] args1 = {
"--approve-all-licenses",
"--add-modules=resources,server,http,webapp,deploy,jsp,servlets",
"--add-modules=" + loggingModules
};
try (JettyHomeTester.Run installRun = distribution.start(args1))
{
assertTrue(installRun.awaitFor(10, TimeUnit.SECONDS));
assertEquals(0, installRun.getExitValue());
try (JettyHomeTester.Run listConfigRun = distribution.start("--list-config"))
{
assertTrue(listConfigRun.awaitFor(10, TimeUnit.SECONDS));
assertEquals(0, listConfigRun.getExitValue());
List<String> rawConfigLogs = new ArrayList<>();
rawConfigLogs.addAll(listConfigRun.getLogs());
for (String expectedEnabledModule : expectedEnabledModules)
{
containsEntryWith("Expected Enabled Module", rawConfigLogs, expectedEnabledModule);
}
for (String expectedClasspathEntry : expectedClasspathEntries)
{
containsEntryWith("Expected Classpath Entry", rawConfigLogs, expectedClasspathEntry);
}
}
File war = distribution.resolveArtifact("org.eclipse.jetty.demos:demo-jsp-webapp:war:" + jettyVersion);
distribution.installWarFile(war, "test");
int port = distribution.freePort();
try (JettyHomeTester.Run requestRun = distribution.start("jetty.http.port=" + port))
{
assertTrue(requestRun.awaitConsoleLogsFor("Started Server@", 10, TimeUnit.SECONDS));
startHttpClient();
ContentResponse response = client.GET("http://localhost:" + port + "/test/index.jsp");
assertEquals(HttpStatus.OK_200, response.getStatus());
assertThat(response.getContentAsString(), containsString("JSP Examples"));
assertThat(response.getContentAsString(), not(containsString("<%")));
}
}
}
private void containsEntryWith(String reason, List<String> logs, String expectedEntry)
{
Pattern pat = Pattern.compile(expectedEntry);
assertThat("Count of matches for [" + expectedEntry + "]", logs.stream().filter(pat.asPredicate()).count(), greaterThanOrEqualTo(1L));
}
}