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:
parent
99c743c2c6
commit
d1e64f4693
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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("/"))
|
||||||
{
|
{
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -118,8 +118,7 @@ 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);
|
||||||
scenario.start(new LoadHandler(), client ->
|
scenario.start(new LoadHandler(), client ->
|
||||||
|
|
Loading…
Reference in New Issue