Fixes #8014 - Review HttpRequest URI construction. (#8015)

Fixes #8014 - Review HttpRequest URI construction.

Now always adding a "/" before the path, if not already present.
Disabled flakey HTTP/3 test.
Parse CONNECT URIs as Authority

Co-authored-by: Greg Wilkins <gregw@webtide.com>
This commit is contained in:
Simone Bordet 2022-05-26 10:13:17 +02:00 committed by GitHub
parent 99c743c2c6
commit d1e64f4693
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 152 additions and 24 deletions

View File

@ -195,6 +195,8 @@ public class HttpRequest implements Request
String rawPath = uri.getRawPath(); String rawPath = uri.getRawPath();
if (rawPath == null) if (rawPath == null)
rawPath = ""; rawPath = "";
if (!rawPath.startsWith("/"))
rawPath = "/" + rawPath;
this.path = rawPath; this.path = rawPath;
String query = uri.getRawQuery(); String query = uri.getRawQuery();
if (query != null) if (query != null)
@ -949,14 +951,14 @@ public class HttpRequest implements Request
return result; return result;
} }
private URI newURI(String uri) private URI newURI(String path)
{ {
try try
{ {
// Handle specially the "OPTIONS *" case, since it is possible to create a URI from "*" (!). // Handle specially the "OPTIONS *" case, since it is possible to create a URI from "*" (!).
if ("*".equals(uri)) if ("*".equals(path))
return null; return null;
URI result = new URI(uri); URI result = new URI(path);
return result.isOpaque() ? null : result; return result.isOpaque() ? null : result;
} }
catch (URISyntaxException x) catch (URISyntaxException x)

View File

@ -24,8 +24,10 @@ import java.net.SocketException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
@ -36,6 +38,8 @@ import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.toolchain.test.Net; import org.eclipse.jetty.toolchain.test.Net;
import org.eclipse.jetty.util.Fields; import org.eclipse.jetty.util.Fields;
@ -77,6 +81,52 @@ public class HttpClientURITest extends AbstractHttpClientServerTest
assertEquals(HttpStatus.OK_200, request.send().getStatus()); assertEquals(HttpStatus.OK_200, request.send().getStatus());
} }
@ParameterizedTest
@ArgumentsSource(ScenarioProvider.class)
public void testPathWithPathParameter(Scenario scenario) throws Exception
{
AtomicReference<CountDownLatch> serverLatchRef = new AtomicReference<>();
start(scenario, new EmptyServerHandler()
{
@Override
protected void service(String target, org.eclipse.jetty.server.Request jettyRequest, HttpServletRequest request, HttpServletResponse response)
{
if (jettyRequest.getHttpURI().hasAmbiguousEmptySegment())
response.setStatus(400);
serverLatchRef.get().countDown();
}
});
// Allow empty segments to test them.
connector.getContainedBeans(HttpConfiguration.class)
.forEach(httpConfig -> httpConfig.setUriCompliance(UriCompliance.from("DEFAULT,AMBIGUOUS_EMPTY_SEGMENT")));
serverLatchRef.set(new CountDownLatch(1));
ContentResponse response1 = client.newRequest("localhost", connector.getLocalPort())
.scheme(scenario.getScheme())
.path("/url;p=v")
.send();
assertEquals(HttpStatus.OK_200, response1.getStatus());
assertTrue(serverLatchRef.get().await(5, TimeUnit.SECONDS));
// Ambiguous empty segment.
serverLatchRef.set(new CountDownLatch(1));
ContentResponse response2 = client.newRequest("localhost", connector.getLocalPort())
.scheme(scenario.getScheme())
.path(";p=v/url")
.send();
assertEquals(HttpStatus.BAD_REQUEST_400, response2.getStatus());
assertTrue(serverLatchRef.get().await(5, TimeUnit.SECONDS));
// Ambiguous empty segment.
serverLatchRef.set(new CountDownLatch(1));
ContentResponse response3 = client.newRequest("localhost", connector.getLocalPort())
.scheme(scenario.getScheme())
.path(";@host.org/url")
.send();
assertEquals(HttpStatus.BAD_REQUEST_400, response3.getStatus());
assertTrue(serverLatchRef.get().await(5, TimeUnit.SECONDS));
}
@ParameterizedTest @ParameterizedTest
@ArgumentsSource(ScenarioProvider.class) @ArgumentsSource(ScenarioProvider.class)
public void testIDNHost(Scenario scenario) throws Exception public void testIDNHost(Scenario scenario) throws Exception

View File

@ -119,7 +119,7 @@ public interface HttpURI
static Immutable from(String method, String uri) static Immutable from(String method, String uri)
{ {
if (HttpMethod.CONNECT.is(method)) if (HttpMethod.CONNECT.is(method))
return new Immutable(uri); return HttpURI.build().uri(method, uri).asImmutable();
if (uri.startsWith("/")) if (uri.startsWith("/"))
return HttpURI.build().pathQuery(uri).asImmutable(); return HttpURI.build().pathQuery(uri).asImmutable();
return HttpURI.from(uri); return HttpURI.from(uri);
@ -628,6 +628,8 @@ public interface HttpURI
*/ */
public Mutable authority(String host, int port) public Mutable authority(String host, int port)
{ {
if (host != null && !isPathValidForAuthority(_path))
throw new IllegalArgumentException("Relative path with authority");
_user = null; _user = null;
_host = host; _host = host;
_port = port; _port = port;
@ -636,12 +638,14 @@ public interface HttpURI
} }
/** /**
* @param hostport the host and port combined * @param hostPort the host and port combined
* @return this mutable * @return this mutable
*/ */
public Mutable authority(String hostport) public Mutable authority(String hostPort)
{ {
HostPort hp = new HostPort(hostport); if (hostPort != null && !isPathValidForAuthority(_path))
throw new IllegalArgumentException("Relative path with authority");
HostPort hp = new HostPort(hostPort);
_user = null; _user = null;
_host = hp.getHost(); _host = hp.getHost();
_port = hp.getPort(); _port = hp.getPort();
@ -649,6 +653,15 @@ public interface HttpURI
return this; return this;
} }
private boolean isPathValidForAuthority(String path)
{
if (path == null)
return true;
if (path.isEmpty() || "*".equals(path))
return true;
return path.startsWith("/");
}
public Mutable clear() public Mutable clear()
{ {
_scheme = null; _scheme = null;
@ -775,6 +788,8 @@ public interface HttpURI
public Mutable host(String host) public Mutable host(String host)
{ {
if (host != null && !isPathValidForAuthority(_path))
throw new IllegalArgumentException("Relative path with authority");
_host = host; _host = host;
_uri = null; _uri = null;
return this; return this;
@ -834,10 +849,12 @@ public interface HttpURI
/** /**
* @param path the path * @param path the path
* @return this Mutuble * @return this Mutable
*/ */
public Mutable path(String path) public Mutable path(String path)
{ {
if (hasAuthority() && !isPathValidForAuthority(path))
throw new IllegalArgumentException("Relative path with authority");
_uri = null; _uri = null;
_path = path; _path = path;
_decodedPath = null; _decodedPath = null;
@ -846,6 +863,8 @@ public interface HttpURI
public Mutable pathQuery(String pathQuery) public Mutable pathQuery(String pathQuery)
{ {
if (hasAuthority() && !isPathValidForAuthority(pathQuery))
throw new IllegalArgumentException("Relative path with authority");
_uri = null; _uri = null;
_path = null; _path = null;
_decodedPath = null; _decodedPath = null;
@ -911,10 +930,7 @@ public interface HttpURI
_query = uri.getQuery(); _query = uri.getQuery();
_uri = null; _uri = null;
_decodedPath = uri.getDecodedPath(); _decodedPath = uri.getDecodedPath();
if (uri.hasAmbiguousSeparator()) _violations.addAll(uri.getViolations());
_violations.add(Violation.AMBIGUOUS_PATH_SEPARATOR);
if (uri.hasAmbiguousSegment())
_violations.add(Violation.AMBIGUOUS_PATH_SEGMENT);
return this; return this;
} }
@ -931,8 +947,7 @@ public interface HttpURI
if (HttpMethod.CONNECT.is(method)) if (HttpMethod.CONNECT.is(method))
{ {
clear(); clear();
_uri = uri; parse(State.HOST, uri);
_path = uri;
} }
else if (uri.startsWith("/")) else if (uri.startsWith("/"))
{ {

View File

@ -177,6 +177,32 @@ public class HttpURITest
assertThat(uri.getPath(), is("/bar")); assertThat(uri.getPath(), is("/bar"));
} }
@Test
public void testCONNECT()
{
HttpURI uri;
uri = HttpURI.from("CONNECT", "host:80");
assertThat(uri.getHost(), is("host"));
assertThat(uri.getPort(), is(80));
assertThat(uri.getPath(), nullValue());
uri = HttpURI.from("CONNECT", "host");
assertThat(uri.getHost(), is("host"));
assertThat(uri.getPort(), is(-1));
assertThat(uri.getPath(), nullValue());
uri = HttpURI.from("CONNECT", "192.168.0.1:8080");
assertThat(uri.getHost(), is("192.168.0.1"));
assertThat(uri.getPort(), is(8080));
assertThat(uri.getPath(), nullValue());
uri = HttpURI.from("CONNECT", "[::1]:8080");
assertThat(uri.getHost(), is("[::1]"));
assertThat(uri.getPort(), is(8080));
assertThat(uri.getPath(), nullValue());
}
@Test @Test
public void testAt() public void testAt()
{ {
@ -824,4 +850,46 @@ public class HttpURITest
HttpURI httpURI = HttpURI.build(input); HttpURI httpURI = HttpURI.build(input);
assertThat("[" + input + "] .query", httpURI.getQuery(), is(expectedQuery)); assertThat("[" + input + "] .query", httpURI.getQuery(), is(expectedQuery));
} }
@Test
public void testRelativePathWithAuthority()
{
assertThrows(IllegalArgumentException.class, () -> HttpURI.build()
.authority("host")
.path("path"));
assertThrows(IllegalArgumentException.class, () -> HttpURI.build()
.authority("host", 8080)
.path(";p=v/url"));
assertThrows(IllegalArgumentException.class, () -> HttpURI.build()
.host("host")
.path(";"));
assertThrows(IllegalArgumentException.class, () -> HttpURI.build()
.path("path")
.authority("host"));
assertThrows(IllegalArgumentException.class, () -> HttpURI.build()
.path(";p=v/url")
.authority("host", 8080));
assertThrows(IllegalArgumentException.class, () -> HttpURI.build()
.path(";")
.host("host"));
HttpURI.Mutable uri = HttpURI.build()
.path("*")
.authority("host");
assertEquals("//host*", uri.asString());
uri = HttpURI.build()
.authority("host")
.path("*");
assertEquals("//host*", uri.asString());
uri = HttpURI.build()
.path("")
.authority("host");
assertEquals("//host", uri.asString());
uri = HttpURI.build()
.authority("host")
.path("");
assertEquals("//host", uri.asString());
}
} }

View File

@ -33,8 +33,6 @@ import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.EndPoint;
@ -193,12 +191,7 @@ public class ConnectHandler extends HandlerWrapper
String tunnelProtocol = jettyRequest.getMetaData().getProtocol(); String tunnelProtocol = jettyRequest.getMetaData().getProtocol();
if (HttpMethod.CONNECT.is(request.getMethod()) && tunnelProtocol == null) if (HttpMethod.CONNECT.is(request.getMethod()) && tunnelProtocol == null)
{ {
String serverAddress = target; String serverAddress = jettyRequest.getHttpURI().getAuthority();
if (HttpVersion.HTTP_2.is(request.getProtocol()))
{
HttpURI httpURI = jettyRequest.getHttpURI();
serverAddress = httpURI.getHost() + ":" + httpURI.getPort();
}
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("CONNECT request for {}", serverAddress); LOG.debug("CONNECT request for {}", serverAddress);
handleConnect(jettyRequest, request, response, serverAddress); handleConnect(jettyRequest, request, response, serverAddress);

View File

@ -1749,6 +1749,7 @@ public class Request implements HttpServletRequest
if (field instanceof HostPortHttpField) if (field instanceof HostPortHttpField)
{ {
HostPortHttpField authority = (HostPortHttpField)field; HostPortHttpField authority = (HostPortHttpField)field;
builder.host(authority.getHost()).port(authority.getPort()); builder.host(authority.getHost()).port(authority.getPort());
} }
else else

View File

@ -118,7 +118,6 @@ public class HttpClientLoadTest extends AbstractTest<HttpClientLoadTest.LoadTran
public void testConcurrent(Transport transport) throws Exception public void testConcurrent(Transport transport) throws Exception
{ {
// TODO: cannot run HTTP/3 (or UDP) in Jenkins. // TODO: cannot run HTTP/3 (or UDP) in Jenkins.
if ("ci".equals(System.getProperty("env")))
Assumptions.assumeTrue(transport != Transport.H3); Assumptions.assumeTrue(transport != Transport.H3);
init(transport); init(transport);