diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectorHttpClientTransport.java b/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectorHttpClientTransport.java index 0c234daa160f..2f7c41582cc0 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectorHttpClientTransport.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectorHttpClientTransport.java @@ -72,7 +72,7 @@ public void connect(InetSocketAddress address, Map context) context.put(ClientConnector.CLIENT_CONNECTION_FACTORY_CONTEXT_KEY, destination.getClientConnectionFactory()); @SuppressWarnings("unchecked") Promise promise = (Promise)context.get(HTTP_CONNECTION_PROMISE_CONTEXT_KEY); - context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, new Promise.Wrapper<>(promise)); + context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, promise); connector.connect(address, context); } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java index c53e8e667107..3d6035dc92e7 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java @@ -509,13 +509,26 @@ private URI checkHost(URI uri) * @param port the destination port * @return the destination * @see #getDestinations() + * @deprecated use {@link #resolveDestination(Request)} instead */ + @Deprecated public Destination getDestination(String scheme, String host, int port) { Origin origin = createOrigin(scheme, host, port); return resolveDestination(new HttpDestination.Key(origin, null)); } + public Destination resolveDestination(Request request) + { + Origin origin = createOrigin(request.getScheme(), request.getHost(), request.getPort()); + HttpClientTransport transport = getTransport(); + HttpDestination.Key destinationKey = transport.newDestinationKey((HttpRequest)request, origin); + HttpDestination destination = resolveDestination(destinationKey); + if (LOG.isDebugEnabled()) + LOG.debug("Resolved {} for {}", destination, request); + return destination; + } + private Origin createOrigin(String scheme, String host, int port) { if (!HttpScheme.HTTP.is(scheme) && !HttpScheme.HTTPS.is(scheme) && @@ -529,7 +542,7 @@ private Origin createOrigin(String scheme, String host, int port) return new Origin(scheme, host, port); } - private HttpDestination resolveDestination(HttpDestination.Key key) + HttpDestination resolveDestination(HttpDestination.Key key) { HttpDestination destination = destinations.get(key); if (destination == null) @@ -568,16 +581,7 @@ public List getDestinations() protected void send(final HttpRequest request, List listeners) { - Origin origin = createOrigin(request.getScheme(), request.getHost(), request.getPort()); - HttpClientTransport transport = getTransport(); - HttpDestination.Key destinationKey = null; - if (transport instanceof HttpClientTransport.Dynamic) - destinationKey = ((HttpClientTransport.Dynamic)transport).newDestinationKey(request, origin); - if (destinationKey == null) - destinationKey = new HttpDestination.Key(origin, null); - if (LOG.isDebugEnabled()) - LOG.debug("Selected {} for {}", destinationKey, request); - HttpDestination destination = resolveDestination(destinationKey); + HttpDestination destination = (HttpDestination)resolveDestination(request); destination.send(request, listeners); } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClientTransport.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClientTransport.java index 68aa5772fdca..248403bba03d 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClientTransport.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClientTransport.java @@ -50,6 +50,15 @@ public interface HttpClientTransport extends ClientConnectionFactory */ public void setHttpClient(HttpClient client); + /** + * Creates a new Key with the given request and origin. + * + * @param request the request that triggers the creation of the Key + * @param origin the origin of the server for the request + * @return a Key that identifies a destination + */ + public HttpDestination.Key newDestinationKey(HttpRequest request, Origin origin); + /** * Creates a new, transport-specific, {@link HttpDestination} object. *

@@ -78,20 +87,4 @@ public interface HttpClientTransport extends ClientConnectionFactory * @param factory the factory for ConnectionPool instances */ public void setConnectionPoolFactory(ConnectionPool.Factory factory); - - /** - * Specifies whether a {@link HttpClientTransport} is dynamic. - */ - @FunctionalInterface - public interface Dynamic - { - /** - * Creates a new Key with the given request and origin. - * - * @param request the request that triggers the creation of the Key - * @param origin the origin of the server for the request - * @return a Key that identifies a destination - */ - public HttpDestination.Key newDestinationKey(HttpRequest request, Origin origin); - } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpProxy.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpProxy.java index 07511ff3c5b3..919e016c844b 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpProxy.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpProxy.java @@ -20,14 +20,15 @@ import java.io.IOException; import java.net.URI; +import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; +import java.util.Objects; import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.api.Destination; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; -import org.eclipse.jetty.client.http.HttpConnectionOverHTTP; +import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpScheme; @@ -37,7 +38,6 @@ import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.util.ssl.SslContextFactory; public class HttpProxy extends ProxyConfiguration.Proxy { @@ -50,7 +50,12 @@ public HttpProxy(String host, int port) public HttpProxy(Origin.Address address, boolean secure) { - super(address, secure); + this(address, secure, new HttpDestination.Protocol(List.of("http/1.1"), false)); + } + + public HttpProxy(Origin.Address address, boolean secure, HttpDestination.Protocol protocol) + { + super(address, secure, Objects.requireNonNull(protocol)); } @Override @@ -61,9 +66,14 @@ public ClientConnectionFactory newClientConnectionFactory(ClientConnectionFactor @Override public URI getURI() + { + return URI.create(newOrigin().asString()); + } + + private Origin newOrigin() { String scheme = isSecure() ? HttpScheme.HTTPS.asString() : HttpScheme.HTTP.asString(); - return URI.create(new Origin(scheme, getAddress()).asString()); + return new Origin(scheme, getAddress()); } private class HttpProxyClientConnectionFactory implements ClientConnectionFactory @@ -79,33 +89,26 @@ private HttpProxyClientConnectionFactory(ClientConnectionFactory connectionFacto public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map context) throws IOException { HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY); - SslContextFactory sslContextFactory = destination.getHttpClient().getSslContextFactory(); - if (destination.isSecure()) + HttpDestination.Protocol serverProtocol = destination.getKey().getProtocol(); + boolean sameProtocol = proxySpeaksServerProtocol(serverProtocol); + if (destination.isSecure() || !sameProtocol) { - if (sslContextFactory != null) + @SuppressWarnings("unchecked") + Promise promise = (Promise)context.get(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY); + Promise wrapped = promise; + if (promise instanceof Promise.Wrapper) + wrapped = ((Promise.Wrapper)promise).unwrap(); + if (wrapped instanceof TunnelPromise) { - @SuppressWarnings("unchecked") - Promise promise = (Promise)context.get(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY); - Promise wrapped = promise; - if (promise instanceof Promise.Wrapper) - wrapped = ((Promise.Wrapper)promise).unwrap(); - if (wrapped instanceof TunnelPromise) - { - ((TunnelPromise)wrapped).setEndPoint(endPoint); - return connectionFactory.newConnection(endPoint, context); - } - else - { - // Replace the promise with the proxy promise that creates the tunnel to the server. - CreateTunnelPromise tunnelPromise = new CreateTunnelPromise(connectionFactory, endPoint, promise, context); - context.put(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY, tunnelPromise); - return connectionFactory.newConnection(endPoint, context); - } + // In case the server closes the tunnel (e.g. proxy authentication + // required: 407 + Connection: close), we will open another tunnel + // so we need to tell the promise about the new EndPoint. + ((TunnelPromise)wrapped).setEndPoint(endPoint); + return connectionFactory.newConnection(endPoint, context); } else { - throw new IOException("Cannot tunnel request, missing " + - SslContextFactory.class.getName() + " in " + HttpClient.class.getName()); + return newProxyConnection(endPoint, context); } } else @@ -113,6 +116,34 @@ public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map serverProtocol.getProtocols().stream().anyMatch(p::equalsIgnoreCase)); + } + + private org.eclipse.jetty.io.Connection newProxyConnection(EndPoint endPoint, Map context) throws IOException + { + // Replace the promise with the proxy promise that creates the tunnel to the server. + @SuppressWarnings("unchecked") + Promise promise = (Promise)context.get(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY); + CreateTunnelPromise tunnelPromise = new CreateTunnelPromise(connectionFactory, endPoint, promise, context); + context.put(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY, tunnelPromise); + + // Replace the destination with the proxy destination. + HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY); + HttpClient client = destination.getHttpClient(); + HttpDestination proxyDestination = client.resolveDestination(new HttpDestination.Key(newOrigin(), getProtocol())); + context.put(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY, proxyDestination); + try + { + return connectionFactory.newConnection(endPoint, context); + } + finally + { + context.put(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY, destination); + } + } } /** @@ -139,6 +170,8 @@ private CreateTunnelPromise(ClientConnectionFactory connectionFactory, EndPoint @Override public void succeeded(Connection connection) { + // Replace the promise back with the original. + context.put(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY, promise); HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY); tunnel(destination, connection); } @@ -154,61 +187,28 @@ private void tunnel(HttpDestination destination, Connection connection) String target = destination.getOrigin().getAddress().asString(); Origin.Address proxyAddress = destination.getConnectAddress(); HttpClient httpClient = destination.getHttpClient(); - long connectTimeout = httpClient.getConnectTimeout(); - Request connect = httpClient.newRequest(proxyAddress.getHost(), proxyAddress.getPort()) - .method(HttpMethod.CONNECT) - .path(target) - .header(HttpHeader.HOST, target) - .idleTimeout(2 * connectTimeout, TimeUnit.MILLISECONDS) - .timeout(connectTimeout, TimeUnit.MILLISECONDS); + Request connect = new TunnelRequest(httpClient, proxyAddress) + .method(HttpMethod.CONNECT) + .path(target) + .header(HttpHeader.HOST, target); ProxyConfiguration.Proxy proxy = destination.getProxy(); - if (proxy != null && proxy.isSecure()) + if (proxy.isSecure()) connect.scheme(HttpScheme.HTTPS.asString()); - final HttpConversation conversation = ((HttpRequest)connect).getConversation(); - conversation.setAttribute(EndPoint.class.getName(), endPoint); - connect.attribute(Connection.class.getName(), new ProxyConnection(destination, connection, promise)); - - connection.send(connect, result -> - { - // The EndPoint may have changed during the conversation, get the latest. - EndPoint endPoint = (EndPoint)conversation.getAttribute(EndPoint.class.getName()); - if (result.isSucceeded()) - { - Response response = result.getResponse(); - if (response.getStatus() == HttpStatus.OK_200) - { - tunnelSucceeded(endPoint); - } - else - { - HttpResponseException failure = new HttpResponseException("Unexpected " + response + - " for " + result.getRequest(), response); - tunnelFailed(endPoint, failure); - } - } - else - { - tunnelFailed(endPoint, result.getFailure()); - } - }); + connection.send(connect, new TunnelListener(connect)); } private void tunnelSucceeded(EndPoint endPoint) { try { - // Replace the promise back with the original - context.put(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY, promise); HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY); - HttpClient client = destination.getHttpClient(); - ClientConnectionFactory sslConnectionFactory = client.newSslClientConnectionFactory(connectionFactory); - HttpConnectionOverHTTP oldConnection = (HttpConnectionOverHTTP)endPoint.getConnection(); - org.eclipse.jetty.io.Connection newConnection = sslConnectionFactory.newConnection(endPoint, context); - // Creating the connection will link the new Connection the EndPoint, - // but we need the old Connection linked for the upgrade to do its job. - endPoint.setConnection(oldConnection); + ClientConnectionFactory connectionFactory = this.connectionFactory; + if (destination.isSecure()) + connectionFactory = destination.newSslClientConnectionFactory(connectionFactory); + var oldConnection = endPoint.getConnection(); + var newConnection = connectionFactory.newConnection(endPoint, context); endPoint.upgrade(newConnection); if (LOG.isDebugEnabled()) LOG.debug("HTTP tunnel established: {} over {}", oldConnection, newConnection); @@ -224,6 +224,40 @@ private void tunnelFailed(EndPoint endPoint, Throwable failure) endPoint.close(); promise.failed(failure); } + + private class TunnelListener extends Response.Listener.Adapter + { + private final HttpConversation conversation; + + private TunnelListener(Request request) + { + this.conversation = ((HttpRequest)request).getConversation(); + } + + @Override + public void onHeaders(Response response) + { + // The EndPoint may have changed during the conversation, get the latest. + EndPoint endPoint = (EndPoint)conversation.getAttribute(EndPoint.class.getName()); + if (response.getStatus() == HttpStatus.OK_200) + { + tunnelSucceeded(endPoint); + } + else + { + HttpResponseException failure = new HttpResponseException("Unexpected " + response + + " for " + response.getRequest(), response); + tunnelFailed(endPoint, failure); + } + } + + @Override + public void onComplete(Result result) + { + if (result.isFailed()) + tunnelFailed(endPoint, result.getFailure()); + } + } } private class ProxyConnection implements Connection @@ -296,4 +330,12 @@ private void setEndPoint(EndPoint endPoint) conversation.setAttribute(EndPoint.class.getName(), endPoint); } } + + public static class TunnelRequest extends HttpRequest + { + private TunnelRequest(HttpClient client, Origin.Address address) + { + super(client, new HttpConversation(), URI.create("http://" + address.asString())); + } + } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java index 7646656649e8..daf6a32dca01 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java @@ -609,7 +609,7 @@ public Decoder(ResponseNotifier notifier, HttpResponse response, ContentDecoder } @Override - protected Action process() throws Throwable + protected Action process() { while (true) { diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java index c2d0d02df88e..9230088e738e 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java @@ -76,6 +76,7 @@ public class HttpRequest implements Request private String query; private String method = HttpMethod.GET.asString(); private HttpVersion version = HttpVersion.HTTP_1_1; + private boolean versionExplicit; private long idleTimeout = -1; private long timeout; private long timeoutAt; @@ -215,10 +216,16 @@ public HttpVersion getVersion() return version; } + public boolean isVersionExplicit() + { + return versionExplicit; + } + @Override public Request version(HttpVersion version) { this.version = Objects.requireNonNull(version); + this.versionExplicit = true; return this; } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/ProxyAuthenticationProtocolHandler.java b/jetty-client/src/main/java/org/eclipse/jetty/client/ProxyAuthenticationProtocolHandler.java index f55ab074ae9b..076a655902de 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/ProxyAuthenticationProtocolHandler.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/ProxyAuthenticationProtocolHandler.java @@ -73,7 +73,7 @@ protected HttpHeader getAuthorizationHeader() @Override protected URI getAuthenticationURI(Request request) { - HttpDestination destination = (HttpDestination)getHttpClient().getDestination(request.getScheme(), request.getHost(), request.getPort()); + HttpDestination destination = (HttpDestination)getHttpClient().resolveDestination(request); ProxyConfiguration.Proxy proxy = destination.getProxy(); return proxy != null ? proxy.getURI() : request.getURI(); } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/ProxyConfiguration.java b/jetty-client/src/main/java/org/eclipse/jetty/client/ProxyConfiguration.java index d8e8acbbc050..f44e30a0c4c9 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/ProxyConfiguration.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/ProxyConfiguration.java @@ -64,11 +64,13 @@ public abstract static class Proxy private final Set excluded = new HashSet<>(); private final Origin.Address address; private final boolean secure; + private final HttpDestination.Protocol protocol; - protected Proxy(Origin.Address address, boolean secure) + protected Proxy(Origin.Address address, boolean secure, HttpDestination.Protocol protocol) { this.address = address; this.secure = secure; + this.protocol = protocol; } /** @@ -87,6 +89,11 @@ public boolean isSecure() return secure; } + public HttpDestination.Protocol getProtocol() + { + return protocol; + } + /** * @return the list of origins that must be proxied * @see #matches(Origin) diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/Socks4Proxy.java b/jetty-client/src/main/java/org/eclipse/jetty/client/Socks4Proxy.java index 677867f1f6af..e20402f2a713 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/Socks4Proxy.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/Socks4Proxy.java @@ -45,7 +45,7 @@ public Socks4Proxy(String host, int port) public Socks4Proxy(Origin.Address address, boolean secure) { - super(address, secure); + super(address, secure, null); } @Override @@ -64,7 +64,7 @@ public Socks4ProxyClientConnectionFactory(ClientConnectionFactory connectionFact } @Override - public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map context) throws IOException + public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map context) { HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY); Executor executor = destination.getHttpClient().getExecutor(); @@ -195,10 +195,9 @@ private void tunnel() try { HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY); - HttpClient client = destination.getHttpClient(); ClientConnectionFactory connectionFactory = this.connectionFactory; if (destination.isSecure()) - connectionFactory = client.newSslClientConnectionFactory(connectionFactory); + connectionFactory = destination.newSslClientConnectionFactory(connectionFactory); org.eclipse.jetty.io.Connection newConnection = connectionFactory.newConnection(getEndPoint(), context); getEndPoint().upgrade(newConnection); if (LOG.isDebugEnabled()) diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Destination.java b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Destination.java index 2325657c0fbe..a663e2d895fa 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Destination.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Destination.java @@ -29,7 +29,7 @@ * {@link Destination} holds a pool of {@link Connection}s, but allows to create unpooled * connections if the application wants full control over connection management via {@link #newConnection(Promise)}. *

- * {@link Destination}s may be obtained via {@link HttpClient#getDestination(String, String, int)} + * {@link Destination}s may be obtained via {@link HttpClient#resolveDestination(Request)} */ public interface Destination { diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/dynamic/HttpClientTransportDynamic.java b/jetty-client/src/main/java/org/eclipse/jetty/client/dynamic/HttpClientTransportDynamic.java index 27cb983c197d..4deca44721f1 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/dynamic/HttpClientTransportDynamic.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/dynamic/HttpClientTransportDynamic.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -78,7 +79,7 @@ * version, or request headers, or request attributes, or even request path) by overriding * {@link #newDestinationKey(HttpRequest, Origin)}.

*/ -public class HttpClientTransportDynamic extends AbstractConnectorHttpClientTransport implements HttpClientTransport.Dynamic +public class HttpClientTransportDynamic extends AbstractConnectorHttpClientTransport { private final List factoryInfos; private final List protocols; @@ -102,42 +103,48 @@ public HttpClientTransportDynamic(ClientConnector connector, ClientConnectionFac super(connector); addBean(connector); if (factoryInfos.length == 0) - throw new IllegalArgumentException("Missing ClientConnectionFactory"); + factoryInfos = new Info[]{HttpClientConnectionFactory.HTTP11}; this.factoryInfos = Arrays.asList(factoryInfos); this.protocols = Arrays.stream(factoryInfos) - .flatMap(info -> info.getProtocols().stream()) - .distinct() - .collect(Collectors.toList()); + .flatMap(info -> info.getProtocols().stream()) + .distinct() + .map(p -> p.toLowerCase(Locale.ENGLISH)) + .collect(Collectors.toList()); for (ClientConnectionFactory.Info factoryInfo : factoryInfos) { addBean(factoryInfo); } setConnectionPoolFactory(destination -> - new MultiplexConnectionPool(destination, destination.getHttpClient().getMaxConnectionsPerDestination(), destination, 1)); + new MultiplexConnectionPool(destination, destination.getHttpClient().getMaxConnectionsPerDestination(), destination, 1)); } @Override public HttpDestination.Key newDestinationKey(HttpRequest request, Origin origin) { boolean ssl = HttpScheme.HTTPS.is(request.getScheme()); + String http1 = "http/1.1"; String http2 = ssl ? "h2" : "h2c"; List protocols = List.of(); - if (request.getVersion() == HttpVersion.HTTP_2) + if (request.isVersionExplicit()) { - // The application is explicitly asking for HTTP/2, so exclude HTTP/1.1. - if (this.protocols.contains(http2)) - protocols = List.of(http2); + HttpVersion version = request.getVersion(); + String desired = version == HttpVersion.HTTP_2 ? http2 : http1; + if (this.protocols.contains(desired)) + protocols = List.of(desired); } else { // Preserve the order of protocols chosen by the application. + // We need to keep multiple protocols in case the protocol + // is negotiated: e.g. [http/1.1, h2] negotiates [h2], but + // here we don't know yet what will be negotiated. protocols = this.protocols.stream() - .filter(p -> p.equals("http/1.1") || p.equals(http2)) + .filter(p -> p.equals(http1) || p.equals(http2)) .collect(Collectors.toList()); } if (protocols.isEmpty()) return new HttpDestination.Key(origin, null); - return new HttpDestination.Key(origin, new HttpDestination.Protocol(protocols, ssl)); + return new HttpDestination.Key(origin, new HttpDestination.Protocol(protocols, ssl && protocols.contains(http2))); } @Override @@ -166,7 +173,7 @@ public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map new IOException("Cannot find " + ClientConnectionFactory.class.getSimpleName() + " for " + protocol)); + .orElseThrow(() -> new IOException("Cannot find " + ClientConnectionFactory.class.getSimpleName() + " for " + protocol)); } } return factoryInfo.getClientConnectionFactory().newConnection(endPoint, context); @@ -184,7 +191,7 @@ protected Connection newNegotiatedConnection(EndPoint endPoint, Map protocols = List.of(protocol); Info factoryInfo = findClientConnectionFactoryInfo(protocols) - .orElseThrow(() -> new IOException("Cannot find " + ClientConnectionFactory.class.getSimpleName() + " for negotiated protocol " + protocol)); + .orElseThrow(() -> new IOException("Cannot find " + ClientConnectionFactory.class.getSimpleName() + " for negotiated protocol " + protocol)); return factoryInfo.getClientConnectionFactory().newConnection(endPoint, context); } catch (Throwable failure) @@ -197,7 +204,7 @@ protected Connection newNegotiatedConnection(EndPoint endPoint, Map findClientConnectionFactoryInfo(List protocols) { return factoryInfos.stream() - .filter(info -> info.matches(protocols)) - .findFirst(); + .filter(info -> info.matches(protocols)) + .findFirst(); } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpClientTransportOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpClientTransportOverHTTP.java index da1aaf23d831..8186352c09d1 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpClientTransportOverHTTP.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpClientTransportOverHTTP.java @@ -19,12 +19,15 @@ package org.eclipse.jetty.client.http; import java.io.IOException; +import java.util.List; import java.util.Map; import org.eclipse.jetty.client.AbstractConnectorHttpClientTransport; import org.eclipse.jetty.client.DuplexConnectionPool; import org.eclipse.jetty.client.DuplexHttpDestination; import org.eclipse.jetty.client.HttpDestination; +import org.eclipse.jetty.client.HttpRequest; +import org.eclipse.jetty.client.Origin; import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.EndPoint; @@ -35,6 +38,8 @@ @ManagedObject("The HTTP/1.1 client transport") public class HttpClientTransportOverHTTP extends AbstractConnectorHttpClientTransport { + public static final HttpDestination.Protocol HTTP11 = new HttpDestination.Protocol(List.of("http/1.1"), false); + public HttpClientTransportOverHTTP() { this(Math.max(1, ProcessorUtils.availableProcessors() / 2)); @@ -52,6 +57,12 @@ public HttpClientTransportOverHTTP(ClientConnector connector) setConnectionPoolFactory(destination -> new DuplexConnectionPool(destination, getHttpClient().getMaxConnectionsPerDestination(), destination)); } + @Override + public HttpDestination.Key newDestinationKey(HttpRequest request, Origin origin) + { + return new HttpDestination.Key(origin, HTTP11); + } + @Override public HttpDestination newHttpDestination(HttpDestination.Key key) { diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpConnectionOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpConnectionOverHTTP.java index fcc2bb2b67fc..12e98ade8963 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpConnectionOverHTTP.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpConnectionOverHTTP.java @@ -20,6 +20,7 @@ import java.nio.ByteBuffer; import java.nio.channels.AsynchronousCloseException; +import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -28,6 +29,7 @@ import org.eclipse.jetty.client.HttpConnection; import org.eclipse.jetty.client.HttpDestination; import org.eclipse.jetty.client.HttpExchange; +import org.eclipse.jetty.client.HttpProxy; import org.eclipse.jetty.client.IConnection; import org.eclipse.jetty.client.SendFailure; import org.eclipse.jetty.client.api.Connection; @@ -256,6 +258,18 @@ public SendFailure send(HttpExchange exchange) return send(channel, exchange); } + @Override + protected void normalizeRequest(Request request) + { + super.normalizeRequest(request); + if (request instanceof HttpProxy.TunnelRequest) + { + long connectTimeout = getHttpClient().getConnectTimeout(); + request.timeout(connectTimeout, TimeUnit.MILLISECONDS) + .idleTimeout(2 * connectTimeout, TimeUnit.MILLISECONDS); + } + } + @Override public void close() { diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java index 0df897bd79b1..9febba733de8 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java @@ -260,6 +260,12 @@ public boolean headerComplete() if (exchange == null) return false; + if (HttpMethod.CONNECT.is(exchange.getRequest().getMethod())) + { + // Store the EndPoint even in case of non-200 responses. + exchange.getRequest().getConversation().setAttribute(EndPoint.class.getName(), getHttpConnection().getEndPoint()); + } + return !responseHeaders(exchange); } diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ClientConnectionCloseTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ClientConnectionCloseTest.java index ca6137238f41..ef1b15114734 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/ClientConnectionCloseTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ClientConnectionCloseTest.java @@ -23,7 +23,6 @@ import java.nio.ByteBuffer; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import javax.servlet.ServletException; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -55,7 +54,7 @@ public void test_ClientConnectionClose_ServerConnectionClose_ClientClosesAfterEx start(scenario, new AbstractHandler() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { baseRequest.setHandled(true); @@ -85,22 +84,23 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques String host = "localhost"; int port = connector.getLocalPort(); - HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); - DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); - - ContentResponse response = client.newRequest(host, port) + var request = client.newRequest(host, port) .scheme(scenario.getScheme()) .header(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString()) .content(new StringContentProvider("0")) - .onRequestSuccess(request -> + .onRequestSuccess(r -> { + HttpDestination destination = (HttpDestination)client.resolveDestination(r); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); HttpConnectionOverHTTP connection = (HttpConnectionOverHTTP)connectionPool.getActiveConnections().iterator().next(); assertFalse(connection.getEndPoint().isOutputShutdown()); - }) - .send(); + }); + ContentResponse response = request.send(); assertEquals(HttpStatus.OK_200, response.getStatus()); assertArrayEquals(data, response.getContent()); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, connectionPool.getConnectionCount()); } @@ -111,7 +111,7 @@ public void test_ClientConnectionClose_ServerDoesNotRespond_ClientIdleTimeout(Sc start(scenario, new AbstractHandler() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) { baseRequest.setHandled(true); request.startAsync(); @@ -122,27 +122,28 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques String host = "localhost"; int port = connector.getLocalPort(); - HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); - DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); - CountDownLatch resultLatch = new CountDownLatch(1); long idleTimeout = 1000; - client.newRequest(host, port) + var request = client.newRequest(host, port) .scheme(scenario.getScheme()) .header(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString()) .idleTimeout(idleTimeout, TimeUnit.MILLISECONDS) - .onRequestSuccess(request -> + .onRequestSuccess(r -> { + HttpDestination destination = (HttpDestination)client.resolveDestination(r); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); HttpConnectionOverHTTP connection = (HttpConnectionOverHTTP)connectionPool.getActiveConnections().iterator().next(); assertFalse(connection.getEndPoint().isOutputShutdown()); - }) - .send(result -> - { - if (result.isFailed()) - resultLatch.countDown(); }); + request.send(result -> + { + if (result.isFailed()) + resultLatch.countDown(); + }); assertTrue(resultLatch.await(2 * idleTimeout, TimeUnit.MILLISECONDS)); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, connectionPool.getConnectionCount()); } @@ -154,7 +155,7 @@ public void test_ClientConnectionClose_ServerPartialResponse_ClientIdleTimeout(S start(scenario, new AbstractHandler() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { baseRequest.setHandled(true); @@ -183,30 +184,31 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques String host = "localhost"; int port = connector.getLocalPort(); - HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); - DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); - DeferredContentProvider content = new DeferredContentProvider(ByteBuffer.allocate(8)); CountDownLatch resultLatch = new CountDownLatch(1); - client.newRequest(host, port) + var request = client.newRequest(host, port) .scheme(scenario.getScheme()) .header(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString()) .content(content) .idleTimeout(idleTimeout, TimeUnit.MILLISECONDS) - .onRequestSuccess(request -> + .onRequestSuccess(r -> { + HttpDestination destination = (HttpDestination)client.resolveDestination(r); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); HttpConnectionOverHTTP connection = (HttpConnectionOverHTTP)connectionPool.getActiveConnections().iterator().next(); assertFalse(connection.getEndPoint().isOutputShutdown()); - }) - .send(result -> - { - if (result.isFailed()) - resultLatch.countDown(); }); + request.send(result -> + { + if (result.isFailed()) + resultLatch.countDown(); + }); content.offer(ByteBuffer.allocate(8)); content.close(); assertTrue(resultLatch.await(2 * idleTimeout, TimeUnit.MILLISECONDS)); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, connectionPool.getConnectionCount()); } @@ -217,7 +219,7 @@ public void test_ClientConnectionClose_ServerNoConnectionClose_ClientCloses(Scen start(scenario, new AbstractHandler() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { baseRequest.setHandled(true); response.setContentLength(0); @@ -238,21 +240,22 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques String host = "localhost"; int port = connector.getLocalPort(); - HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); - DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); - - ContentResponse response = client.newRequest(host, port) + var request = client.newRequest(host, port) .scheme(scenario.getScheme()) .header(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString()) - .onRequestSuccess(request -> + .onRequestSuccess(r -> { + HttpDestination destination = (HttpDestination)client.resolveDestination(r); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); HttpConnectionOverHTTP connection = (HttpConnectionOverHTTP)connectionPool.getActiveConnections().iterator().next(); assertFalse(connection.getEndPoint().isOutputShutdown()); }) - .onResponseHeaders(r -> r.getHeaders().remove(HttpHeader.CONNECTION)) - .send(); + .onResponseHeaders(r -> r.getHeaders().remove(HttpHeader.CONNECTION)); + ContentResponse response = request.send(); assertEquals(HttpStatus.OK_200, response.getStatus()); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, connectionPool.getConnectionCount()); } } diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientCustomProxyTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientCustomProxyTest.java index ea4d68982aba..f98421b91e60 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientCustomProxyTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientCustomProxyTest.java @@ -116,7 +116,7 @@ private class CAFEBABEProxy extends ProxyConfiguration.Proxy { private CAFEBABEProxy(Origin.Address address, boolean secure) { - super(address, secure); + super(address, secure, null); } @Override diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientExplicitConnectionTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientExplicitConnectionTest.java index 762c002bf835..3e1a76c4d6a6 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientExplicitConnectionTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientExplicitConnectionTest.java @@ -45,12 +45,12 @@ public void testExplicitConnection(Scenario scenario) throws Exception { start(scenario, new EmptyServerHandler()); - Destination destination = client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); + Request request = client.newRequest("localhost", connector.getLocalPort()).scheme(scenario.getScheme()); + Destination destination = client.resolveDestination(request); FuturePromise futureConnection = new FuturePromise<>(); destination.newConnection(futureConnection); try (Connection connection = futureConnection.get(5, TimeUnit.SECONDS)) { - Request request = client.newRequest(destination.getHost(), destination.getPort()).scheme(scenario.getScheme()); FutureResponseListener listener = new FutureResponseListener(request); connection.send(request, listener); ContentResponse response = listener.get(5, TimeUnit.SECONDS); @@ -71,11 +71,11 @@ public void testExplicitConnectionIsClosedOnRemoteClose(Scenario scenario) throw { start(scenario, new EmptyServerHandler()); - Destination destination = client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); + Request request = client.newRequest("localhost", connector.getLocalPort()).scheme(scenario.getScheme()); + Destination destination = client.resolveDestination(request); FuturePromise futureConnection = new FuturePromise<>(); destination.newConnection(futureConnection); Connection connection = futureConnection.get(5, TimeUnit.SECONDS); - Request request = client.newRequest(destination.getHost(), destination.getPort()).scheme(scenario.getScheme()); FutureResponseListener listener = new FutureResponseListener(request); connection.send(request, listener); ContentResponse response = listener.get(5, TimeUnit.SECONDS); @@ -105,14 +105,14 @@ public void testExplicitConnectionResponseListeners(Scenario scenario) throws Ex { start(scenario, new EmptyServerHandler()); - Destination destination = client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); + CountDownLatch responseLatch = new CountDownLatch(1); + Request request = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scenario.getScheme()) + .onResponseSuccess(response -> responseLatch.countDown()); + Destination destination = client.resolveDestination(request); FuturePromise futureConnection = new FuturePromise<>(); destination.newConnection(futureConnection); Connection connection = futureConnection.get(5, TimeUnit.SECONDS); - CountDownLatch responseLatch = new CountDownLatch(1); - Request request = client.newRequest(destination.getHost(), destination.getPort()) - .scheme(scenario.getScheme()) - .onResponseSuccess(response -> responseLatch.countDown()); FutureResponseListener listener = new FutureResponseListener(request); connection.send(request, listener); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java index cc21b0b9df7e..3dcd33f60ffb 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java @@ -120,10 +120,11 @@ public void testStoppingClosesConnections(Scenario scenario) throws Exception String host = "localhost"; int port = connector.getLocalPort(); String path = "/"; - Response response = client.GET(scenario.getScheme() + "://" + host + ":" + port + path); + Request request = client.newRequest(scenario.getScheme() + "://" + host + ":" + port + path); + Response response = request.send(); assertEquals(200, response.getStatus()); - HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); long start = System.nanoTime(); @@ -182,7 +183,7 @@ public void test_GET_ResponseWithoutContent(Scenario scenario) throws Exception @ArgumentsSource(ScenarioProvider.class) public void test_GET_ResponseWithContent(Scenario scenario) throws Exception { - final byte[] data = new byte[]{0, 1, 2, 3, 4, 5, 6, 7}; + byte[] data = new byte[]{0, 1, 2, 3, 4, 5, 6, 7}; start(scenario, new AbstractHandler() { @Override @@ -206,8 +207,8 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, @ArgumentsSource(ScenarioProvider.class) public void test_GET_WithParameters_ResponseWithContent(Scenario scenario) throws Exception { - final String paramName1 = "a"; - final String paramName2 = "b"; + String paramName1 = "a"; + String paramName2 = "b"; start(scenario, new AbstractHandler() { @Override @@ -239,8 +240,8 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, @ArgumentsSource(ScenarioProvider.class) public void test_GET_WithParametersMultiValued_ResponseWithContent(Scenario scenario) throws Exception { - final String paramName1 = "a"; - final String paramName2 = "b"; + String paramName1 = "a"; + String paramName2 = "b"; start(scenario, new AbstractHandler() { @Override @@ -278,8 +279,8 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, @ArgumentsSource(ScenarioProvider.class) public void test_POST_WithParameters(Scenario scenario) throws Exception { - final String paramName = "a"; - final String paramValue = "\u20AC"; + String paramName = "a"; + String paramValue = "\u20AC"; start(scenario, new AbstractHandler() { @Override @@ -310,9 +311,9 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, @ArgumentsSource(ScenarioProvider.class) public void test_PUT_WithParameters(Scenario scenario) throws Exception { - final String paramName = "a"; - final String paramValue = "\u20AC"; - final String encodedParamValue = URLEncoder.encode(paramValue, "UTF-8"); + String paramName = "a"; + String paramValue = "\u20AC"; + String encodedParamValue = URLEncoder.encode(paramValue, "UTF-8"); start(scenario, new AbstractHandler() { @Override @@ -344,9 +345,9 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, @ArgumentsSource(ScenarioProvider.class) public void test_POST_WithParameters_WithContent(Scenario scenario) throws Exception { - final byte[] content = {0, 1, 2, 3}; - final String paramName = "a"; - final String paramValue = "\u20AC"; + byte[] content = {0, 1, 2, 3}; + String paramName = "a"; + String paramValue = "\u20AC"; start(scenario, new AbstractHandler() { @Override @@ -389,7 +390,7 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, } }); - final byte[] content = {0, 1, 2, 3}; + byte[] content = {0, 1, 2, 3}; ContentResponse response = client.POST(scenario.getScheme() + "://localhost:" + connector.getLocalPort()) .onRequestContent((request, buffer) -> { @@ -420,20 +421,18 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, } }); - final AtomicInteger progress = new AtomicInteger(); + AtomicInteger progress = new AtomicInteger(); ContentResponse response = client.POST(scenario.getScheme() + "://localhost:" + connector.getLocalPort()) - .onRequestContent((request, buffer) -> - { - byte[] bytes = new byte[buffer.remaining()]; - assertEquals(1, bytes.length); - buffer.get(bytes); - assertEquals(bytes[0], progress.getAndIncrement()); - }) - .content(new BytesContentProvider(new byte[]{0}, new byte[]{1}, new byte[]{ - 2 - }, new byte[]{3}, new byte[]{4})) - .timeout(5, TimeUnit.SECONDS) - .send(); + .onRequestContent((request, buffer) -> + { + byte[] bytes = new byte[buffer.remaining()]; + assertEquals(1, bytes.length); + buffer.get(bytes); + assertEquals(bytes[0], progress.getAndIncrement()); + }) + .content(new BytesContentProvider(new byte[]{0}, new byte[]{1}, new byte[]{2}, new byte[]{3}, new byte[]{4})) + .timeout(5, TimeUnit.SECONDS) + .send(); assertNotNull(response); assertEquals(200, response.getStatus()); @@ -448,8 +447,8 @@ public void test_QueuedRequest_IsSent_WhenPreviousRequestSucceeded(Scenario scen client.setMaxConnectionsPerDestination(1); - final CountDownLatch latch = new CountDownLatch(1); - final CountDownLatch successLatch = new CountDownLatch(2); + CountDownLatch latch = new CountDownLatch(1); + CountDownLatch successLatch = new CountDownLatch(2); client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .onRequestBegin(request -> @@ -509,7 +508,7 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, try (StacklessLogging stackless = new StacklessLogging(org.eclipse.jetty.server.HttpChannel.class)) { - final CountDownLatch latch = new CountDownLatch(2); + CountDownLatch latch = new CountDownLatch(2); client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .path("/one") @@ -568,10 +567,10 @@ protected void doNonErrorHandle(String target, org.eclipse.jetty.server.Request } } - final CountDownLatch latch = new CountDownLatch(3); - final AtomicLong exchangeTime = new AtomicLong(); - final AtomicLong requestTime = new AtomicLong(); - final AtomicLong responseTime = new AtomicLong(); + CountDownLatch latch = new CountDownLatch(3); + AtomicLong exchangeTime = new AtomicLong(); + AtomicLong requestTime = new AtomicLong(); + AtomicLong responseTime = new AtomicLong(); client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .file(file) @@ -623,7 +622,7 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, } }); - final CountDownLatch latch = new CountDownLatch(1); + CountDownLatch latch = new CountDownLatch(1); client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) // The second ByteBuffer set to null will throw an exception @@ -678,14 +677,14 @@ public void test_ExchangeIsComplete_WhenRequestFails_WithNoResponse(Scenario sce { start(scenario, new EmptyServerHandler()); - final CountDownLatch latch = new CountDownLatch(1); - final String host = "localhost"; - final int port = connector.getLocalPort(); + CountDownLatch latch = new CountDownLatch(1); + String host = "localhost"; + int port = connector.getLocalPort(); client.newRequest(host, port) .scheme(scenario.getScheme()) - .onRequestBegin(request -> + .onRequestBegin(r -> { - HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); + HttpDestination destination = (HttpDestination)client.resolveDestination(r); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); connectionPool.getActiveConnections().iterator().next().close(); }) @@ -706,7 +705,7 @@ public void onComplete(Result result) @DisabledIfSystemProperty(named = "env", matches = "ci") // TODO: SLOW, needs review public void test_Request_IdleTimeout(Scenario scenario) throws Exception { - final long idleTimeout = 1000; + long idleTimeout = 1000; start(scenario, new AbstractHandler() { @Override @@ -724,8 +723,8 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, } }); - final String host = "localhost"; - final int port = connector.getLocalPort(); + String host = "localhost"; + int port = connector.getLocalPort(); assertThrows(TimeoutException.class, () -> client.newRequest(host, port) .scheme(scenario.getScheme()) @@ -763,7 +762,7 @@ public void testSendToIPv6Address(Scenario scenario) throws Exception @ArgumentsSource(ScenarioProvider.class) public void testHeaderProcessing(Scenario scenario) throws Exception { - final String headerName = "X-Header-Test"; + String headerName = "X-Header-Test"; start(scenario, new AbstractHandler() { @Override @@ -792,7 +791,7 @@ public void testAllHeadersDiscarded(Scenario scenario) throws Exception start(scenario, new EmptyServerHandler()); int count = 10; - final CountDownLatch latch = new CountDownLatch(count); + CountDownLatch latch = new CountDownLatch(count); for (int i = 0; i < count; ++i) { client.newRequest("localhost", connector.getLocalPort()) @@ -821,7 +820,7 @@ public void onComplete(Result result) @ArgumentsSource(ScenarioProvider.class) public void test_HEAD_With_ResponseContentLength(Scenario scenario) throws Exception { - final int length = 1024; + int length = 1024; start(scenario, new AbstractHandler() { @Override @@ -870,7 +869,7 @@ public void testConnectThrowsUnknownHostException(Scenario scenario) throws Exce start(scenario, new EmptyServerHandler()); - final CountDownLatch latch = new CountDownLatch(1); + CountDownLatch latch = new CountDownLatch(1); client.newRequest(host, port) .send(result -> { @@ -926,7 +925,7 @@ public void failed(Throwable x) @ArgumentsSource(ScenarioProvider.class) public void testCustomUserAgent(Scenario scenario) throws Exception { - final String userAgent = "Test/1.0"; + String userAgent = "Test/1.0"; start(scenario, new AbstractHandler() { @Override @@ -1009,7 +1008,7 @@ public void testRequestListenerForMultipleEventsIsInvokedOncePerEvent(Scenario s { start(scenario, new EmptyServerHandler()); - final AtomicInteger counter = new AtomicInteger(); + AtomicInteger counter = new AtomicInteger(); Request.Listener listener = new Request.Listener() { @Override @@ -1081,8 +1080,8 @@ public void testResponseListenerForMultipleEventsIsInvokedOncePerEvent(Scenario { start(scenario, new EmptyServerHandler()); - final AtomicInteger counter = new AtomicInteger(); - final CountDownLatch latch = new CountDownLatch(1); + AtomicInteger counter = new AtomicInteger(); + CountDownLatch latch = new CountDownLatch(1); Response.Listener listener = new Response.Listener() { @Override @@ -1161,7 +1160,7 @@ public void onComplete(Result result) @ArgumentsSource(ScenarioProvider.class) public void setOnCompleteCallbackWithBlockingSend(Scenario scenario) throws Exception { - final byte[] content = new byte[512]; + byte[] content = new byte[512]; new Random().nextBytes(content); start(scenario, new AbstractHandler() { @@ -1173,7 +1172,7 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, } }); - final Exchanger ex = new Exchanger<>(); + Exchanger ex = new Exchanger<>(); BufferingResponseListener listener = new BufferingResponseListener() { @Override @@ -1204,7 +1203,7 @@ public void onComplete(Result result) @ArgumentsSource(ScenarioProvider.class) public void testCustomHostHeader(Scenario scenario) throws Exception { - final String host = "localhost"; + String host = "localhost"; start(scenario, new AbstractHandler() { @Override @@ -1266,18 +1265,17 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, } }); + long timeout = 5000; + Request request = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scenario.getScheme()) + .version(HttpVersion.HTTP_1_0) + .header(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE.asString()) + .timeout(timeout, TimeUnit.MILLISECONDS); FuturePromise promise = new FuturePromise<>(); - Destination destination = client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); + Destination destination = client.resolveDestination(request); destination.newConnection(promise); try (Connection connection = promise.get(5, TimeUnit.SECONDS)) { - long timeout = 5000; - Request request = client.newRequest(destination.getHost(), destination.getPort()) - .scheme(destination.getScheme()) - .version(HttpVersion.HTTP_1_0) - .header(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE.asString()) - .timeout(timeout, TimeUnit.MILLISECONDS); - FutureResponseListener listener = new FutureResponseListener(request); connection.send(request, listener); ContentResponse response = listener.get(2 * timeout, TimeUnit.MILLISECONDS); @@ -1312,7 +1310,7 @@ public void testHTTP10WithKeepAliveAndNoContent(Scenario scenario) throws Except @ArgumentsSource(ScenarioProvider.class) public void testLongPollIsAbortedWhenClientIsStopped(Scenario scenario) throws Exception { - final CountDownLatch latch = new CountDownLatch(1); + CountDownLatch latch = new CountDownLatch(1); start(scenario, new AbstractHandler() { @Override @@ -1324,7 +1322,7 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, } }); - final CountDownLatch completeLatch = new CountDownLatch(1); + CountDownLatch completeLatch = new CountDownLatch(1); client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .send(result -> @@ -1343,7 +1341,7 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, @ParameterizedTest @ArgumentsSource(ScenarioProvider.class) - public void testSmallContentDelimitedByEOFWithSlowRequestHTTP10(Scenario scenario) throws Exception + public void testSmallContentDelimitedByEOFWithSlowRequestHTTP10(Scenario scenario) { Assumptions.assumeTrue(HttpScheme.HTTP.is(scenario.getScheme())); @@ -1356,7 +1354,7 @@ public void testSmallContentDelimitedByEOFWithSlowRequestHTTP10(Scenario scenari @ParameterizedTest @ArgumentsSource(NonSslScenarioProvider.class) - public void testBigContentDelimitedByEOFWithSlowRequestHTTP10(Scenario scenario) throws Exception + public void testBigContentDelimitedByEOFWithSlowRequestHTTP10(Scenario scenario) { ExecutionException e = assertThrows(ExecutionException.class, () -> testContentDelimitedByEOFWithSlowRequest(scenario, HttpVersion.HTTP_1_0, 128 * 1024)); @@ -1379,7 +1377,7 @@ public void testBigContentDelimitedByEOFWithSlowRequestHTTP11(Scenario scenario) testContentDelimitedByEOFWithSlowRequest(scenario, HttpVersion.HTTP_1_1, 128 * 1024); } - private void testContentDelimitedByEOFWithSlowRequest(final Scenario scenario, final HttpVersion version, int length) throws Exception + private void testContentDelimitedByEOFWithSlowRequest(Scenario scenario, HttpVersion version, int length) throws Exception { // This test is crafted in a way that the response completes before the request is fully written. // With SSL, the response coming down will close the SSLEngine so it would not be possible to @@ -1388,7 +1386,7 @@ private void testContentDelimitedByEOFWithSlowRequest(final Scenario scenario, f // This is a limit of Java's SSL implementation that does not allow half closes. Assumptions.assumeTrue(HttpScheme.HTTP.is(scenario.getScheme())); - final byte[] data = new byte[length]; + byte[] data = new byte[length]; new Random().nextBytes(data); start(scenario, new AbstractHandler() { @@ -1424,8 +1422,8 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, @ArgumentsSource(ScenarioProvider.class) public void testRequestRetries(Scenario scenario) throws Exception { - final int maxRetries = 3; - final AtomicInteger requests = new AtomicInteger(); + int maxRetries = 3; + AtomicInteger requests = new AtomicInteger(); start(scenario, new AbstractHandler() { @Override @@ -1438,7 +1436,7 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, } }); - final CountDownLatch latch = new CountDownLatch(1); + CountDownLatch latch = new CountDownLatch(1); new RetryListener(client, scenario.getScheme(), "localhost", connector.getLocalPort(), maxRetries) { @Override @@ -1466,9 +1464,9 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, } }); - final AtomicReference callbackRef = new AtomicReference<>(); - final CountDownLatch contentLatch = new CountDownLatch(1); - final CountDownLatch completeLatch = new CountDownLatch(1); + AtomicReference callbackRef = new AtomicReference<>(); + CountDownLatch contentLatch = new CountDownLatch(1); + CountDownLatch completeLatch = new CountDownLatch(1); client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .send(new Response.Listener.Adapter() @@ -1514,7 +1512,7 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, } }); - final AtomicBoolean open = new AtomicBoolean(); + AtomicBoolean open = new AtomicBoolean(); ClientConnector clientConnector = new ClientConnector(); clientConnector.setSslContextFactory(scenario.newClientSslContextFactory()); client = new HttpClient(new HttpClientTransportOverHTTP(clientConnector) @@ -1535,7 +1533,7 @@ public void onOpen() }); client.start(); - final CountDownLatch latch = new CountDownLatch(2); + CountDownLatch latch = new CountDownLatch(2); client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .onRequestBegin(request -> @@ -1567,7 +1565,7 @@ public void testCONNECTWithHTTP10(Scenario scenario) throws Exception .method(HttpMethod.CONNECT) .version(HttpVersion.HTTP_1_0); FuturePromise promise = new FuturePromise<>(); - client.getDestination("http", host, port).newConnection(promise); + client.resolveDestination(request).newConnection(promise); Connection connection = promise.get(5, TimeUnit.SECONDS); FutureResponseListener listener = new FutureResponseListener(request); connection.send(request, listener); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientUploadDuringServerShutdown.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientUploadDuringServerShutdownTest.java similarity index 94% rename from jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientUploadDuringServerShutdown.java rename to jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientUploadDuringServerShutdownTest.java index 5af26fcd6a6c..550d5e9e6167 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientUploadDuringServerShutdown.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientUploadDuringServerShutdownTest.java @@ -45,7 +45,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -public class HttpClientUploadDuringServerShutdown +public class HttpClientUploadDuringServerShutdownTest { /** * A server used in conjunction with {@link ClientSide}. @@ -117,8 +117,8 @@ public static void main(String[] args) throws Exception { int length = 16 * 1024 * 1024 + random.nextInt(16 * 1024 * 1024); client.newRequest("localhost", 8888) - .content(new BytesContentProvider(new byte[length])) - .send(result -> latch.countDown()); + .content(new BytesContentProvider(new byte[length])) + .send(result -> latch.countDown()); long sleep = 1 + random.nextInt(10); TimeUnit.MILLISECONDS.sleep(sleep); } @@ -229,10 +229,10 @@ protected boolean abort(Throwable failure) // is being closed is used to send the request. assertTrue(sendLatch.await(5, TimeUnit.SECONDS)); - final CountDownLatch completeLatch = new CountDownLatch(1); - client.newRequest("localhost", connector.getLocalPort()) + CountDownLatch completeLatch = new CountDownLatch(1); + var request = client.newRequest("localhost", connector.getLocalPort()) .timeout(10, TimeUnit.SECONDS) - .onRequestBegin(request -> + .onRequestBegin(r -> { try { @@ -243,12 +243,12 @@ protected boolean abort(Throwable failure) { x.printStackTrace(); } - }) - .send(result -> completeLatch.countDown()); + }); + request.send(result -> completeLatch.countDown()); assertTrue(completeLatch.await(5, TimeUnit.SECONDS)); - HttpDestination destination = (HttpDestination)client.getDestination("http", "localhost", connector.getLocalPort()); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); DuplexConnectionPool pool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, pool.getConnectionCount()); assertEquals(0, pool.getIdleConnections().size()); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java index 5089df3d7285..de20a5005479 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java @@ -18,14 +18,12 @@ package org.eclipse.jetty.client; -import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Collection; import java.util.Queue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -67,20 +65,19 @@ public void test_SuccessfulRequest_ReturnsConnection(Scenario scenario) throws E String host = "localhost"; int port = connector.getLocalPort(); - HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); + CountDownLatch headersLatch = new CountDownLatch(1); + CountDownLatch successLatch = new CountDownLatch(3); + Request request = client.newRequest(host, port).scheme(scenario.getScheme()); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); - final Collection idleConnections = connectionPool.getIdleConnections(); + Collection idleConnections = connectionPool.getIdleConnections(); assertEquals(0, idleConnections.size()); - final Collection activeConnections = connectionPool.getActiveConnections(); + Collection activeConnections = connectionPool.getActiveConnections(); assertEquals(0, activeConnections.size()); - final CountDownLatch headersLatch = new CountDownLatch(1); - final CountDownLatch successLatch = new CountDownLatch(3); - client.newRequest(host, port) - .scheme(scenario.getScheme()) - .onRequestSuccess(request -> successLatch.countDown()) + request.onRequestSuccess(r -> successLatch.countDown()) .onResponseHeaders(response -> { assertEquals(0, idleConnections.size()); @@ -118,18 +115,19 @@ public void test_FailedRequest_RemovesConnection(Scenario scenario) throws Excep String host = "localhost"; int port = connector.getLocalPort(); - HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); + CountDownLatch beginLatch = new CountDownLatch(1); + CountDownLatch failureLatch = new CountDownLatch(2); + Request request = client.newRequest(host, port).scheme(scenario.getScheme()); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); - final Collection idleConnections = connectionPool.getIdleConnections(); + Collection idleConnections = connectionPool.getIdleConnections(); assertEquals(0, idleConnections.size()); - final Collection activeConnections = connectionPool.getActiveConnections(); + Collection activeConnections = connectionPool.getActiveConnections(); assertEquals(0, activeConnections.size()); - final CountDownLatch beginLatch = new CountDownLatch(1); - final CountDownLatch failureLatch = new CountDownLatch(2); - client.newRequest(host, port).scheme(scenario.getScheme()).listener(new Request.Listener.Adapter() + request.listener(new Request.Listener.Adapter() { @Override public void onBegin(Request request) @@ -143,7 +141,8 @@ public void onFailure(Request request, Throwable failure) { failureLatch.countDown(); } - }).send(new Response.Listener.Adapter() + }) + .send(new Response.Listener.Adapter() { @Override public void onComplete(Result result) @@ -170,51 +169,50 @@ public void test_BadRequest_RemovesConnection(Scenario scenario) throws Exceptio String host = "localhost"; int port = connector.getLocalPort(); - HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); + CountDownLatch successLatch = new CountDownLatch(3); + Request request = client.newRequest(host, port).scheme(scenario.getScheme()); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); - final Queue idleConnections = connectionPool.getIdleConnections(); + Queue idleConnections = connectionPool.getIdleConnections(); assertEquals(0, idleConnections.size()); - final Collection activeConnections = connectionPool.getActiveConnections(); + Collection activeConnections = connectionPool.getActiveConnections(); assertEquals(0, activeConnections.size()); - final CountDownLatch successLatch = new CountDownLatch(3); - client.newRequest(host, port) - .scheme(scenario.getScheme()) - .listener(new Request.Listener.Adapter() + request.listener(new Request.Listener.Adapter() + { + @Override + public void onBegin(Request request) { - @Override - public void onBegin(Request request) - { - // Remove the host header, this will make the request invalid - request.header(HttpHeader.HOST, null); - } + // Remove the host header, this will make the request invalid + request.header(HttpHeader.HOST, null); + } - @Override - public void onSuccess(Request request) - { - successLatch.countDown(); - } - }) - .send(new Response.Listener.Adapter() + @Override + public void onSuccess(Request request) { - @Override - public void onSuccess(Response response) - { - assertEquals(400, response.getStatus()); - // 400 response also come with a Connection: close, - // so the connection is closed and removed - successLatch.countDown(); - } + successLatch.countDown(); + } + }) + .send(new Response.Listener.Adapter() + { + @Override + public void onSuccess(Response response) + { + assertEquals(400, response.getStatus()); + // 400 response also come with a Connection: close, + // so the connection is closed and removed + successLatch.countDown(); + } - @Override - public void onComplete(Result result) - { - assertFalse(result.isFailed()); - successLatch.countDown(); - } - }); + @Override + public void onComplete(Result result) + { + assertFalse(result.isFailed()); + successLatch.countDown(); + } + }); assertTrue(successLatch.await(30, TimeUnit.SECONDS)); @@ -232,65 +230,64 @@ public void test_BadRequest_WithSlowRequest_RemovesConnection(Scenario scenario) String host = "localhost"; int port = connector.getLocalPort(); - HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); + CountDownLatch successLatch = new CountDownLatch(3); + Request request = client.newRequest(host, port).scheme(scenario.getScheme()); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); - final Collection idleConnections = connectionPool.getIdleConnections(); + Collection idleConnections = connectionPool.getIdleConnections(); assertEquals(0, idleConnections.size()); - final Collection activeConnections = connectionPool.getActiveConnections(); + Collection activeConnections = connectionPool.getActiveConnections(); assertEquals(0, activeConnections.size()); - final long delay = 1000; - final CountDownLatch successLatch = new CountDownLatch(3); - client.newRequest(host, port) - .scheme(scenario.getScheme()) - .listener(new Request.Listener.Adapter() + long delay = 1000; + request.listener(new Request.Listener.Adapter() + { + @Override + public void onBegin(Request request) { - @Override - public void onBegin(Request request) - { - // Remove the host header, this will make the request invalid - request.header(HttpHeader.HOST, null); - } + // Remove the host header, this will make the request invalid + request.header(HttpHeader.HOST, null); + } - @Override - public void onHeaders(Request request) + @Override + public void onHeaders(Request request) + { + try { - try - { - TimeUnit.MILLISECONDS.sleep(delay); - } - catch (InterruptedException e) - { - e.printStackTrace(); - } + TimeUnit.MILLISECONDS.sleep(delay); } - - @Override - public void onSuccess(Request request) + catch (InterruptedException e) { - successLatch.countDown(); + e.printStackTrace(); } - }) - .send(new Response.Listener.Adapter() + } + + @Override + public void onSuccess(Request request) { - @Override - public void onSuccess(Response response) - { - assertEquals(400, response.getStatus()); - // 400 response also come with a Connection: close, - // so the connection is closed and removed - successLatch.countDown(); - } + successLatch.countDown(); + } + }) + .send(new Response.Listener.Adapter() + { + @Override + public void onSuccess(Response response) + { + assertEquals(400, response.getStatus()); + // 400 response also come with a Connection: close, + // so the connection is closed and removed + successLatch.countDown(); + } - @Override - public void onComplete(Result result) - { - assertFalse(result.isFailed()); - successLatch.countDown(); - } - }); + @Override + public void onComplete(Result result) + { + assertFalse(result.isFailed()); + successLatch.countDown(); + } + }); assertTrue(successLatch.await(delay * 30, TimeUnit.MILLISECONDS)); @@ -306,21 +303,20 @@ public void test_ConnectionFailure_RemovesConnection(Scenario scenario) throws E String host = "localhost"; int port = connector.getLocalPort(); - HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); + Request request = client.newRequest(host, port).scheme(scenario.getScheme()); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); - final Collection idleConnections = connectionPool.getIdleConnections(); + Collection idleConnections = connectionPool.getIdleConnections(); assertEquals(0, idleConnections.size()); - final Collection activeConnections = connectionPool.getActiveConnections(); + Collection activeConnections = connectionPool.getActiveConnections(); assertEquals(0, activeConnections.size()); server.stop(); - final CountDownLatch failureLatch = new CountDownLatch(2); - client.newRequest(host, port) - .scheme(scenario.getScheme()) - .onRequestFailure((request, failure) -> failureLatch.countDown()) + CountDownLatch failureLatch = new CountDownLatch(2); + request.onRequestFailure((r, x) -> failureLatch.countDown()) .send(result -> { assertTrue(result.isFailed()); @@ -340,7 +336,7 @@ public void test_ResponseWithConnectionCloseHeader_RemovesConnection(Scenario sc start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) { response.setHeader("Connection", "close"); baseRequest.setHandled(true); @@ -349,29 +345,28 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, String host = "localhost"; int port = connector.getLocalPort(); - HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); + Request request = client.newRequest(host, port).scheme(scenario.getScheme()); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); - final Collection idleConnections = connectionPool.getIdleConnections(); + Collection idleConnections = connectionPool.getIdleConnections(); assertEquals(0, idleConnections.size()); - final Collection activeConnections = connectionPool.getActiveConnections(); + Collection activeConnections = connectionPool.getActiveConnections(); assertEquals(0, activeConnections.size()); - final CountDownLatch latch = new CountDownLatch(1); - client.newRequest(host, port) - .scheme(scenario.getScheme()) - .send(new Response.Listener.Adapter() + CountDownLatch latch = new CountDownLatch(1); + request.send(new Response.Listener.Adapter() + { + @Override + public void onComplete(Result result) { - @Override - public void onComplete(Result result) - { - assertFalse(result.isFailed()); - assertEquals(0, idleConnections.size()); - assertEquals(0, activeConnections.size()); - latch.countDown(); - } - }); + assertFalse(result.isFailed()); + assertEquals(0, idleConnections.size()); + assertEquals(0, activeConnections.size()); + latch.countDown(); + } + }); assertTrue(latch.await(30, TimeUnit.SECONDS)); @@ -388,7 +383,7 @@ public void test_BigRequestContent_ResponseWithConnectionCloseHeader_RemovesConn start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) { response.setHeader("Connection", "close"); baseRequest.setHandled(true); @@ -398,23 +393,22 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, String host = "localhost"; int port = connector.getLocalPort(); - HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); + Request request = client.newRequest(host, port).scheme(scenario.getScheme()); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); - final Collection idleConnections = connectionPool.getIdleConnections(); + Collection idleConnections = connectionPool.getIdleConnections(); assertEquals(0, idleConnections.size()); - final Collection activeConnections = connectionPool.getActiveConnections(); + Collection activeConnections = connectionPool.getActiveConnections(); assertEquals(0, activeConnections.size()); Log.getLogger(HttpConnection.class).info("Expecting java.lang.IllegalStateException: HttpParser{s=CLOSED,..."); - final CountDownLatch latch = new CountDownLatch(1); + CountDownLatch latch = new CountDownLatch(1); ByteBuffer buffer = ByteBuffer.allocate(16 * 1024 * 1024); Arrays.fill(buffer.array(), (byte)'x'); - client.newRequest(host, port) - .scheme(scenario.getScheme()) - .content(new ByteBufferContentProvider(buffer)) + request.content(new ByteBufferContentProvider(buffer)) .send(new Response.Listener.Adapter() { @Override @@ -446,19 +440,17 @@ public void test_IdleConnection_IsClosed_OnRemoteClose(Scenario scenario) throws String host = "localhost"; int port = connector.getLocalPort(); - HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); + Request request = client.newRequest(host, port).scheme(scenario.getScheme()); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); - final Collection idleConnections = connectionPool.getIdleConnections(); + Collection idleConnections = connectionPool.getIdleConnections(); assertEquals(0, idleConnections.size()); - final Collection activeConnections = connectionPool.getActiveConnections(); + Collection activeConnections = connectionPool.getActiveConnections(); assertEquals(0, activeConnections.size()); - ContentResponse response = client.newRequest(host, port) - .scheme(scenario.getScheme()) - .timeout(30, TimeUnit.SECONDS) - .send(); + ContentResponse response = request.timeout(30, TimeUnit.SECONDS).send(); assertEquals(200, response.getStatus()); @@ -479,18 +471,18 @@ public void testConnectionForHTTP10ResponseIsRemoved(Scenario scenario) throws E String host = "localhost"; int port = connector.getLocalPort(); - HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); + Request request = client.newRequest(host, port).scheme(scenario.getScheme()); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); - final Collection idleConnections = connectionPool.getIdleConnections(); + Collection idleConnections = connectionPool.getIdleConnections(); assertEquals(0, idleConnections.size()); - final Collection activeConnections = connectionPool.getActiveConnections(); + Collection activeConnections = connectionPool.getActiveConnections(); assertEquals(0, activeConnections.size()); client.setStrictEventOrdering(false); - ContentResponse response = client.newRequest(host, port) - .scheme(scenario.getScheme()) + ContentResponse response = request .onResponseBegin(response1 -> { // Simulate a HTTP 1.0 response has been received. diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpRequestAbortTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpRequestAbortTest.java index 199e4cb003e8..68da47ae9fae 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpRequestAbortTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpRequestAbortTest.java @@ -55,18 +55,17 @@ public void testAbortBeforeQueued(Scenario scenario) throws Exception Exception failure = new Exception("oops"); + Request request = client.newRequest("localhost", connector.getLocalPort()).scheme(scenario.getScheme()); ExecutionException x = assertThrows(ExecutionException.class, () -> { - Request request = client.newRequest("localhost", connector.getLocalPort()) - .scheme(scenario.getScheme()) - .timeout(5, TimeUnit.SECONDS); + request.timeout(5, TimeUnit.SECONDS); request.abort(failure); request.send(); }); assertSame(failure, x.getCause()); // Make sure the pool is in a sane state. - HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(1, connectionPool.getConnectionCount()); assertEquals(0, connectionPool.getActiveConnections().size()); @@ -79,32 +78,29 @@ public void testAbortOnQueued(Scenario scenario) throws Exception { start(scenario, new EmptyServerHandler()); - final Throwable cause = new Exception(); - final AtomicBoolean aborted = new AtomicBoolean(); - final CountDownLatch latch = new CountDownLatch(1); - final AtomicBoolean begin = new AtomicBoolean(); + Throwable cause = new Exception(); + AtomicBoolean aborted = new AtomicBoolean(); + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean begin = new AtomicBoolean(); + Request request = client.newRequest("localhost", connector.getLocalPort()).scheme(scenario.getScheme()); ExecutionException x = assertThrows(ExecutionException.class, () -> { - client.newRequest("localhost", connector.getLocalPort()) - .scheme(scenario.getScheme()) - .listener(new Request.Listener.Adapter() + request.listener(new Request.Listener.Adapter() + { + @Override + public void onQueued(Request request) { - @Override - public void onQueued(Request request) - { - aborted.set(request.abort(cause)); - latch.countDown(); - } + aborted.set(request.abort(cause)); + latch.countDown(); + } - @Override - public void onBegin(Request request) - { - begin.set(true); - } - }) - .timeout(5, TimeUnit.SECONDS) - .send(); + @Override + public void onBegin(Request request) + { + begin.set(true); + } + }).timeout(5, TimeUnit.SECONDS).send(); }); assertTrue(latch.await(5, TimeUnit.SECONDS)); @@ -112,7 +108,7 @@ public void onBegin(Request request) assertSame(cause, x.getCause()); assertFalse(begin.get()); - HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, connectionPool.getConnectionCount()); assertEquals(0, connectionPool.getActiveConnections().size()); @@ -130,34 +126,31 @@ public void testAbortOnBegin(Scenario scenario) throws Exception final CountDownLatch latch = new CountDownLatch(1); final CountDownLatch committed = new CountDownLatch(1); + Request request = client.newRequest("localhost", connector.getLocalPort()).scheme(scenario.getScheme()); ExecutionException x = assertThrows(ExecutionException.class, () -> { - client.newRequest("localhost", connector.getLocalPort()) - .scheme(scenario.getScheme()) - .listener(new Request.Listener.Adapter() + request.listener(new Request.Listener.Adapter() + { + @Override + public void onBegin(Request request) { - @Override - public void onBegin(Request request) - { - aborted.set(request.abort(cause)); - latch.countDown(); - } + aborted.set(request.abort(cause)); + latch.countDown(); + } - @Override - public void onCommit(Request request) - { - committed.countDown(); - } - }) - .timeout(5, TimeUnit.SECONDS) - .send(); + @Override + public void onCommit(Request request) + { + committed.countDown(); + } + }).timeout(5, TimeUnit.SECONDS).send(); }); assertTrue(latch.await(5, TimeUnit.SECONDS)); if (aborted.get()) assertSame(cause, x.getCause()); assertFalse(committed.await(1, TimeUnit.SECONDS)); - HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, connectionPool.getConnectionCount()); assertEquals(0, connectionPool.getActiveConnections().size()); @@ -175,34 +168,31 @@ public void testAbortOnHeaders(Scenario scenario) throws Exception final CountDownLatch latch = new CountDownLatch(1); final CountDownLatch committed = new CountDownLatch(1); + Request request = client.newRequest("localhost", connector.getLocalPort()).scheme(scenario.getScheme()); ExecutionException x = assertThrows(ExecutionException.class, () -> { - client.newRequest("localhost", connector.getLocalPort()) - .scheme(scenario.getScheme()) - .listener(new Request.Listener.Adapter() + request.listener(new Request.Listener.Adapter() + { + @Override + public void onHeaders(Request request) { - @Override - public void onHeaders(Request request) - { - aborted.set(request.abort(cause)); - latch.countDown(); - } + aborted.set(request.abort(cause)); + latch.countDown(); + } - @Override - public void onCommit(Request request) - { - committed.countDown(); - } - }) - .timeout(5, TimeUnit.SECONDS) - .send(); + @Override + public void onCommit(Request request) + { + committed.countDown(); + } + }).timeout(5, TimeUnit.SECONDS).send(); }); assertTrue(latch.await(5, TimeUnit.SECONDS)); if (aborted.get()) assertSame(cause, x.getCause()); assertFalse(committed.await(1, TimeUnit.SECONDS)); - HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, connectionPool.getConnectionCount()); assertEquals(0, connectionPool.getActiveConnections().size()); @@ -219,26 +209,24 @@ public void testAbortOnCommit(Scenario scenario) throws Exception // A) the request is failed before the response arrived // B) the request is failed after the response arrived - final Throwable cause = new Exception(); - final AtomicBoolean aborted = new AtomicBoolean(); - final CountDownLatch latch = new CountDownLatch(1); + Throwable cause = new Exception(); + AtomicBoolean aborted = new AtomicBoolean(); + CountDownLatch latch = new CountDownLatch(1); + + Request request = client.newRequest("localhost", connector.getLocalPort()).scheme(scenario.getScheme()); ExecutionException x = assertThrows(ExecutionException.class, () -> { - client.newRequest("localhost", connector.getLocalPort()) - .scheme(scenario.getScheme()) - .onRequestCommit(request -> - { - aborted.set(request.abort(cause)); - latch.countDown(); - }) - .timeout(5, TimeUnit.SECONDS) - .send(); + request.onRequestCommit(r -> + { + aborted.set(r.abort(cause)); + latch.countDown(); + }).timeout(5, TimeUnit.SECONDS).send(); }); assertTrue(latch.await(5, TimeUnit.SECONDS)); if (aborted.get()) assertSame(cause, x.getCause()); - HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, connectionPool.getConnectionCount()); assertEquals(0, connectionPool.getActiveConnections().size()); @@ -249,11 +237,11 @@ public void testAbortOnCommit(Scenario scenario) throws Exception @ArgumentsSource(ScenarioProvider.class) public void testAbortOnCommitWithContent(Scenario scenario) throws Exception { - final AtomicReference failure = new AtomicReference<>(); + AtomicReference failure = new AtomicReference<>(); start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { try { @@ -269,35 +257,31 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, } }); - final Throwable cause = new Exception(); - final AtomicBoolean aborted = new AtomicBoolean(); - final CountDownLatch latch = new CountDownLatch(1); + Throwable cause = new Exception(); + AtomicBoolean aborted = new AtomicBoolean(); + CountDownLatch latch = new CountDownLatch(1); + Request request = client.newRequest("localhost", connector.getLocalPort()).scheme(scenario.getScheme()); ExecutionException x = assertThrows(ExecutionException.class, () -> { - client.newRequest("localhost", connector.getLocalPort()) - .scheme(scenario.getScheme()) - .onRequestCommit(request -> - { - aborted.set(request.abort(cause)); - latch.countDown(); - }) - .content(new ByteBufferContentProvider(ByteBuffer.wrap(new byte[]{0}), ByteBuffer.wrap(new byte[]{1})) + request.onRequestCommit(r -> + { + aborted.set(r.abort(cause)); + latch.countDown(); + }).content(new ByteBufferContentProvider(ByteBuffer.wrap(new byte[]{0}), ByteBuffer.wrap(new byte[]{1})) + { + @Override + public long getLength() { - @Override - public long getLength() - { - return -1; - } - }) - .timeout(5, TimeUnit.SECONDS) - .send(); + return -1; + } + }).timeout(5, TimeUnit.SECONDS).send(); }); assertTrue(latch.await(5, TimeUnit.SECONDS)); if (aborted.get()) assertSame(cause, x.getCause()); - HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, connectionPool.getConnectionCount()); assertEquals(0, connectionPool.getActiveConnections().size()); @@ -314,7 +298,7 @@ public void testAbortOnContent(Scenario scenario) throws Exception start(scenario, new EmptyServerHandler() { @Override - protected void service(String target, org.eclipse.jetty.server.Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + protected void service(String target, org.eclipse.jetty.server.Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { try { @@ -328,28 +312,25 @@ protected void service(String target, org.eclipse.jetty.server.Request jettyRequ } }); - final Throwable cause = new Exception(); - final AtomicBoolean aborted = new AtomicBoolean(); - final CountDownLatch latch = new CountDownLatch(1); + Throwable cause = new Exception(); + AtomicBoolean aborted = new AtomicBoolean(); + CountDownLatch latch = new CountDownLatch(1); + + Request request = client.newRequest("localhost", connector.getLocalPort()).scheme(scenario.getScheme()); ExecutionException x = assertThrows(ExecutionException.class, () -> { - client.newRequest("localhost", connector.getLocalPort()) - .scheme(scenario.getScheme()) - .onRequestContent((request, content) -> - { - aborted.set(request.abort(cause)); - latch.countDown(); - }) - .content(new ByteBufferContentProvider(ByteBuffer.wrap(new byte[]{0}), ByteBuffer.wrap(new byte[]{1})) + request.onRequestContent((r, c) -> + { + aborted.set(r.abort(cause)); + latch.countDown(); + }).content(new ByteBufferContentProvider(ByteBuffer.wrap(new byte[]{0}), ByteBuffer.wrap(new byte[]{1})) + { + @Override + public long getLength() { - @Override - public long getLength() - { - return -1; - } - }) - .timeout(5, TimeUnit.SECONDS) - .send(); + return -1; + } + }).timeout(5, TimeUnit.SECONDS).send(); }); assertTrue(latch.await(5, TimeUnit.SECONDS)); if (aborted.get()) @@ -357,7 +338,7 @@ public long getLength() assertTrue(serverLatch.await(5, TimeUnit.SECONDS)); - HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, connectionPool.getConnectionCount()); assertEquals(0, connectionPool.getActiveConnections().size()); @@ -369,11 +350,11 @@ public long getLength() @ArgumentsSource(ScenarioProvider.class) public void testInterrupt(Scenario scenario) throws Exception { - final long delay = 1000; + long delay = 1000; start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws ServletException { try { @@ -389,10 +370,10 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, }); Request request = client.newRequest("localhost", connector.getLocalPort()) - .timeout(3 * delay, TimeUnit.MILLISECONDS) - .scheme(scenario.getScheme()); + .timeout(3 * delay, TimeUnit.MILLISECONDS) + .scheme(scenario.getScheme()); - final Thread thread = Thread.currentThread(); + Thread thread = Thread.currentThread(); new Thread(() -> { try @@ -406,7 +387,7 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, } }).start(); - assertThrows(InterruptedException.class, () -> request.send()); + assertThrows(InterruptedException.class, request::send); } @ParameterizedTest @@ -417,7 +398,7 @@ public void testAbortLongPoll(Scenario scenario) throws Exception start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws ServletException { try { @@ -432,13 +413,13 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, } }); - final Request request = client.newRequest("localhost", connector.getLocalPort()) - .timeout(3 * delay, TimeUnit.MILLISECONDS) - .scheme(scenario.getScheme()); + Request request = client.newRequest("localhost", connector.getLocalPort()) + .timeout(3 * delay, TimeUnit.MILLISECONDS) + .scheme(scenario.getScheme()); - final Throwable cause = new Exception(); - final AtomicBoolean aborted = new AtomicBoolean(); - final CountDownLatch latch = new CountDownLatch(1); + Throwable cause = new Exception(); + AtomicBoolean aborted = new AtomicBoolean(); + CountDownLatch latch = new CountDownLatch(1); new Thread(() -> { try @@ -464,7 +445,7 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, assertSame(cause, x.getCause()); } - HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, connectionPool.getConnectionCount()); assertEquals(0, connectionPool.getActiveConnections().size()); @@ -479,7 +460,7 @@ public void testAbortLongPollAsync(Scenario scenario) throws Exception start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws ServletException { try { @@ -497,8 +478,8 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, final Throwable cause = new Exception(); final CountDownLatch latch = new CountDownLatch(1); Request request = client.newRequest("localhost", connector.getLocalPort()) - .scheme(scenario.getScheme()) - .timeout(3 * delay, TimeUnit.MILLISECONDS); + .scheme(scenario.getScheme()) + .timeout(3 * delay, TimeUnit.MILLISECONDS); request.send(result -> { assertTrue(result.isFailed()); @@ -520,7 +501,7 @@ public void testAbortConversation(Scenario scenario) throws Exception start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { baseRequest.setHandled(true); if (!"/done".equals(request.getRequestURI())) @@ -555,10 +536,10 @@ public void onComplete(Result result) ExecutionException e = assertThrows(ExecutionException.class, () -> { client.newRequest("localhost", connector.getLocalPort()) - .scheme(scenario.getScheme()) - .path("/redirect") - .timeout(5, TimeUnit.SECONDS) - .send(); + .scheme(scenario.getScheme()) + .path("/redirect") + .timeout(5, TimeUnit.SECONDS) + .send(); }); assertTrue(latch.await(5, TimeUnit.SECONDS)); if (aborted.get()) diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ServerConnectionCloseTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ServerConnectionCloseTest.java index 7a4b646fc4e5..9544f84d9664 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/ServerConnectionCloseTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ServerConnectionCloseTest.java @@ -147,7 +147,7 @@ private void testServerSendsConnectionClose(boolean shutdownOutput, boolean chun Thread.sleep(1000); // Connection should have been removed from pool. - HttpDestination destination = (HttpDestination)client.getDestination("http", "localhost", port); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, connectionPool.getConnectionCount()); assertEquals(0, connectionPool.getIdleConnectionCount()); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/TLSServerConnectionCloseTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/TLSServerConnectionCloseTest.java index 18cfa42cf075..c2e68a55a110 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/TLSServerConnectionCloseTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/TLSServerConnectionCloseTest.java @@ -172,7 +172,7 @@ private void testServerSendsConnectionClose(final CloseMode closeMode, boolean c Thread.sleep(1000); // Connection should have been removed from pool. - HttpDestination destination = (HttpDestination)client.getDestination("http", "localhost", port); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, connectionPool.getConnectionCount()); assertEquals(0, connectionPool.getIdleConnectionCount()); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/api/Usage.java b/jetty-client/src/test/java/org/eclipse/jetty/client/api/Usage.java index 30030887a327..2d7d2134e92f 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/api/Usage.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/api/Usage.java @@ -153,13 +153,13 @@ public void testRequestWithExplicitConnectionControl() throws Exception HttpClient client = new HttpClient(); client.start(); + Request request = client.newRequest("localhost", 8080); + // Create an explicit connection, and use try-with-resources to manage it FuturePromise futureConnection = new FuturePromise<>(); - client.getDestination("http", "localhost", 8080).newConnection(futureConnection); + client.resolveDestination(request).newConnection(futureConnection); try (Connection connection = futureConnection.get(5, TimeUnit.SECONDS)) { - Request request = client.newRequest("localhost", 8080); - // Asynchronous send but using FutureResponseListener FutureResponseListener listener = new FutureResponseListener(request); connection.send(request, listener); @@ -293,15 +293,8 @@ public void testRequestOutputStream() throws Exception try (OutputStream output = content.getOutputStream()) { client.newRequest("localhost", 8080) - .content(content) - .send(new Response.CompleteListener() - { - @Override - public void onComplete(Result result) - { - assertEquals(200, result.getResponse().getStatus()); - } - }); + .content(content) + .send(result -> assertEquals(200, result.getResponse().getStatus())); output.write(new byte[1024]); output.write(new byte[512]); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTPTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTPTest.java index e146ea90056a..f2c9a3a7bdaf 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTPTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTPTest.java @@ -260,28 +260,27 @@ public void testDestinationIsRemoved(Scenario scenario) throws Exception String host = "localhost"; int port = connector.getLocalPort(); - Destination destinationBefore = client.getDestination(scenario.getScheme(), host, port); - - ContentResponse response = client.newRequest(host, port) - .scheme(scenario.getScheme()) - .header(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString()) - .send(); + Request request = client.newRequest(host, port) + .scheme(scenario.getScheme()) + .header(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString()); + Destination destinationBefore = client.resolveDestination(request); + ContentResponse response = request.send(); assertEquals(200, response.getStatus()); - Destination destinationAfter = client.getDestination(scenario.getScheme(), host, port); + Destination destinationAfter = client.resolveDestination(request); assertSame(destinationBefore, destinationAfter); client.setRemoveIdleDestinations(true); - response = client.newRequest(host, port) + request = client.newRequest(host, port) .scheme(scenario.getScheme()) - .header(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString()) - .send(); + .header(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString()); + response = request.send(); assertEquals(200, response.getStatus()); - destinationAfter = client.getDestination(scenario.getScheme(), host, port); + destinationAfter = client.resolveDestination(request); assertNotSame(destinationBefore, destinationAfter); } diff --git a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpClientTransportOverFCGI.java b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpClientTransportOverFCGI.java index 986deda19362..c6186dc3f463 100644 --- a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpClientTransportOverFCGI.java +++ b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpClientTransportOverFCGI.java @@ -19,6 +19,7 @@ package org.eclipse.jetty.fcgi.client.http; import java.io.IOException; +import java.util.List; import java.util.Map; import org.eclipse.jetty.client.AbstractConnectorHttpClientTransport; @@ -26,6 +27,8 @@ import org.eclipse.jetty.client.DuplexHttpDestination; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpDestination; +import org.eclipse.jetty.client.HttpRequest; +import org.eclipse.jetty.client.Origin; import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.fcgi.FCGI; @@ -71,6 +74,12 @@ public String getScriptRoot() return scriptRoot; } + @Override + public HttpDestination.Key newDestinationKey(HttpRequest request, Origin origin) + { + return new HttpDestination.Key(origin, new HttpDestination.Protocol(List.of("fastcgi/1.1"), false)); + } + @Override public HttpDestination newHttpDestination(HttpDestination.Key key) { diff --git a/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/HttpTransportOverFCGI.java b/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/HttpTransportOverFCGI.java index 09d9b2dbb518..1e12e7a64068 100644 --- a/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/HttpTransportOverFCGI.java +++ b/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/HttpTransportOverFCGI.java @@ -25,6 +25,7 @@ import org.eclipse.jetty.fcgi.generator.ServerGenerator; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeaderValue; +import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.server.HttpTransport; @@ -56,10 +57,13 @@ public boolean isOptimizedForDirectBuffers() } @Override - public void send(MetaData.Response info, boolean head, ByteBuffer content, boolean lastContent, Callback callback) + public void send(MetaData.Request request, MetaData.Response response, ByteBuffer content, boolean lastContent, Callback callback) { - if (info != null) - commit(info, head, content, lastContent, callback); + boolean head = HttpMethod.HEAD.is(request.getMethod()); + if (response != null) + { + commit(response, head, content, lastContent, callback); + } else { if (head) diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java index 826e0cc3d45b..7c78a7fab3ea 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java @@ -133,6 +133,7 @@ public enum HttpHeader C_AUTHORITY(":authority"), C_PATH(":path"), C_STATUS(":status"), + C_PROTOCOL(":protocol"), UNKNOWN("::UNKNOWN::"); diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java index 2161ea88c221..e200a0effd37 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java @@ -161,7 +161,8 @@ public HttpURI(String scheme, String host, int port, String pathQuery) _host = host; _port = port; - parse(State.PATH, pathQuery); + if (pathQuery != null) + parse(State.PATH, pathQuery); } public void parse(String uri) diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/MetaData.java b/jetty-http/src/main/java/org/eclipse/jetty/http/MetaData.java index cc9cac1d6ed8..ec188fc23364 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/MetaData.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/MetaData.java @@ -155,25 +155,19 @@ public Request(String method, HttpURI uri, HttpVersion version, HttpFields field public Request(String method, HttpScheme scheme, HostPortHttpField hostPort, String uri, HttpVersion version, HttpFields fields) { - this(method, new HttpURI(scheme == null ? null : scheme.asString(), - hostPort == null ? null : hostPort.getHost(), - hostPort == null ? -1 : hostPort.getPort(), - uri), version, fields); + this(method, scheme, hostPort, uri, version, fields, Long.MIN_VALUE); } public Request(String method, HttpScheme scheme, HostPortHttpField hostPort, String uri, HttpVersion version, HttpFields fields, long contentLength) { - this(method, new HttpURI(scheme == null ? null : scheme.asString(), - hostPort == null ? null : hostPort.getHost(), - hostPort == null ? -1 : hostPort.getPort(), - uri), version, fields, contentLength); + this(method, scheme == null ? null : scheme.asString(), hostPort, uri, version, fields, contentLength); } public Request(String method, String scheme, HostPortHttpField hostPort, String uri, HttpVersion version, HttpFields fields, long contentLength) { this(method, new HttpURI(scheme, - hostPort == null ? null : hostPort.getHost(), - hostPort == null ? -1 : hostPort.getPort(), + hostPort == null ? null : hostPort.getHost(), + hostPort == null ? -1 : hostPort.getPort(), uri), version, fields, contentLength); } @@ -221,6 +215,14 @@ public HttpURI getURI() return _uri; } + /** + * @param uri the HTTP URI to set + */ + public void setURI(HttpURI uri) + { + _uri = uri; + } + /** * @return the HTTP URI in string form */ @@ -229,20 +231,41 @@ public String getURIString() return _uri == null ? null : _uri.toString(); } - /** - * @param uri the HTTP URI to set - */ - public void setURI(HttpURI uri) + public String getProtocol() { - _uri = uri; + return null; } @Override public String toString() { HttpFields fields = getFields(); - return String.format("%s{u=%s,%s,h=%d,cl=%d}", - getMethod(), getURI(), getHttpVersion(), fields == null ? -1 : fields.size(), getContentLength()); + return String.format("%s{u=%s,%s,h=%d,cl=%d,p=%s}", + getMethod(), getURI(), getHttpVersion(), fields == null ? -1 : fields.size(), getContentLength(), getProtocol()); + } + } + + public static class ConnectRequest extends Request + { + private String _protocol; + + public ConnectRequest(HttpScheme scheme, HostPortHttpField authority, String path, HttpFields fields, String protocol) + { + super(HttpMethod.CONNECT.asString(), scheme, authority, path, HttpVersion.HTTP_2, fields, Long.MIN_VALUE); + _protocol = protocol; + } + + @Override + public String getProtocol() + { + return _protocol; + } + + @Override + public void recycle() + { + super.recycle(); + _protocol = null; } } diff --git a/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2Client.java b/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2Client.java index a058215190a3..1a09afc6fed5 100644 --- a/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2Client.java +++ b/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2Client.java @@ -115,6 +115,7 @@ public class HTTP2Client extends ContainerLifeCycle private int maxConcurrentPushedStreams = 32; private int maxSettingsKeys = SettingsFrame.DEFAULT_MAX_KEYS; private FlowControlStrategy.Factory flowControlStrategyFactory = () -> new BufferingFlowControlStrategy(0.5F); + private long streamIdleTimeout; public HTTP2Client() { @@ -194,6 +195,17 @@ public void setIdleTimeout(long idleTimeout) connector.setIdleTimeout(Duration.ofMillis(idleTimeout)); } + @ManagedAttribute("The stream idle timeout in milliseconds") + public long getStreamIdleTimeout() + { + return streamIdleTimeout; + } + + public void setStreamIdleTimeout(long streamIdleTimeout) + { + this.streamIdleTimeout = streamIdleTimeout; + } + @ManagedAttribute("The connect timeout in milliseconds") public long getConnectTimeout() { @@ -323,7 +335,7 @@ public void connect(SslContextFactory sslContextFactory, InetSocketAddress addre public void connect(SocketAddress address, ClientConnectionFactory factory, Session.Listener listener, Promise promise, Map context) { context = contextFrom(factory, listener, promise, context); - context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, new Promise.Wrapper<>(promise)); + context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, promise); connector.connect(address, context); } @@ -336,7 +348,7 @@ public void accept(SslContextFactory sslContextFactory, SocketChannel channel, S public void accept(SocketChannel channel, ClientConnectionFactory factory, Session.Listener listener, Promise promise) { Map context = contextFrom(factory, listener, promise, null); - context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, new Promise.Wrapper<>(promise)); + context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, promise); connector.accept(channel, context); } diff --git a/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientConnectionFactory.java b/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientConnectionFactory.java index 007507a8ee8c..bede2c571253 100644 --- a/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientConnectionFactory.java +++ b/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientConnectionFactory.java @@ -63,6 +63,9 @@ public Connection newConnection(EndPoint endPoint, Map context) final FlowControlStrategy flowControl = client.getFlowControlStrategyFactory().newFlowControlStrategy(); final HTTP2ClientSession session = new HTTP2ClientSession(scheduler, endPoint, generator, listener, flowControl); session.setMaxRemoteStreams(client.getMaxConcurrentPushedStreams()); + long streamIdleTimeout = client.getStreamIdleTimeout(); + if (streamIdleTimeout > 0) + session.setStreamIdleTimeout(streamIdleTimeout); final Parser parser = new Parser(byteBufferPool, session, 4096, 8192); parser.setMaxFrameLength(client.getMaxFrameLength()); diff --git a/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientSession.java b/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientSession.java index 660ae6c205f4..aca17ce69007 100644 --- a/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientSession.java +++ b/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientSession.java @@ -145,7 +145,7 @@ public void onPushPromise(PushPromiseFrame frame) } else { - IStream pushStream = createRemoteStream(pushStreamId); + IStream pushStream = createRemoteStream(pushStreamId, frame.getMetaData()); if (pushStream != null) { pushStream.process(frame, Callback.NOOP); diff --git a/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/ConnectTunnelTest.java b/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/ConnectTunnelTest.java new file mode 100644 index 000000000000..d7a9f21be3ab --- /dev/null +++ b/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/ConnectTunnelTest.java @@ -0,0 +1,151 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http2.client; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.http.HostPortHttpField; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpScheme; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http2.api.Session; +import org.eclipse.jetty.http2.api.Stream; +import org.eclipse.jetty.http2.api.server.ServerSessionListener; +import org.eclipse.jetty.http2.frames.DataFrame; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.FuturePromise; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ConnectTunnelTest extends AbstractTest +{ + @Test + public void testCONNECT() throws Exception + { + start(new ServerSessionListener.Adapter() + { + @Override + public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) + { + // Verifies that the CONNECT request is well formed. + MetaData.Request request = (MetaData.Request)frame.getMetaData(); + assertEquals(HttpMethod.CONNECT.asString(), request.getMethod()); + HttpURI uri = request.getURI(); + assertNull(uri.getScheme()); + assertNull(uri.getPath()); + assertNotNull(uri.getAuthority()); + return new Stream.Listener.Adapter() + { + @Override + public void onData(Stream stream, DataFrame frame, Callback callback) + { + stream.data(frame, callback); + } + }; + } + }); + + Session client = newClient(new Session.Listener.Adapter()); + + CountDownLatch latch = new CountDownLatch(1); + byte[] bytes = "HELLO".getBytes(StandardCharsets.UTF_8); + String host = "localhost"; + int port = connector.getLocalPort(); + String authority = host + ":" + port; + MetaData.Request request = new MetaData.Request(HttpMethod.CONNECT.asString(), null, new HostPortHttpField(authority), null, HttpVersion.HTTP_2, new HttpFields()); + FuturePromise streamPromise = new FuturePromise<>(); + client.newStream(new HeadersFrame(request, null, false), streamPromise, new Stream.Listener.Adapter() + { + @Override + public void onData(Stream stream, DataFrame frame, Callback callback) + { + if (frame.isEndStream()) + latch.countDown(); + } + }); + Stream stream = streamPromise.get(5, TimeUnit.SECONDS); + ByteBuffer data = ByteBuffer.wrap(bytes); + stream.data(new DataFrame(stream.getId(), data, true), Callback.NOOP); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testCONNECTWithProtocol() throws Exception + { + start(new ServerSessionListener.Adapter() + { + @Override + public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) + { + // Verifies that the CONNECT request is well formed. + MetaData.Request request = (MetaData.Request)frame.getMetaData(); + assertEquals(HttpMethod.CONNECT.asString(), request.getMethod()); + HttpURI uri = request.getURI(); + assertNotNull(uri.getScheme()); + assertNotNull(uri.getPath()); + assertNotNull(uri.getAuthority()); + assertNotNull(request.getProtocol()); + return new Stream.Listener.Adapter() + { + @Override + public void onData(Stream stream, DataFrame frame, Callback callback) + { + stream.data(frame, callback); + } + }; + } + }); + + Session client = newClient(new Session.Listener.Adapter()); + + CountDownLatch latch = new CountDownLatch(1); + byte[] bytes = "HELLO".getBytes(StandardCharsets.UTF_8); + String host = "localhost"; + int port = connector.getLocalPort(); + String authority = host + ":" + port; + MetaData.Request request = new MetaData.ConnectRequest(HttpScheme.HTTP, new HostPortHttpField(authority), "/", new HttpFields(), "websocket"); + FuturePromise streamPromise = new FuturePromise<>(); + client.newStream(new HeadersFrame(request, null, false), streamPromise, new Stream.Listener.Adapter() + { + @Override + public void onData(Stream stream, DataFrame frame, Callback callback) + { + if (frame.isEndStream()) + latch.countDown(); + } + }); + Stream stream = streamPromise.get(5, TimeUnit.SECONDS); + ByteBuffer data = ByteBuffer.wrap(bytes); + stream.data(new DataFrame(stream.getId(), data, true), Callback.NOOP); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } +} diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Channel.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Channel.java new file mode 100644 index 000000000000..c1cb5edc96b9 --- /dev/null +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Channel.java @@ -0,0 +1,65 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http2; + +import java.util.function.Consumer; + +import org.eclipse.jetty.http2.frames.DataFrame; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.util.Callback; + +/** + *

A HTTP/2 specific handler of events for normal and tunneled exchanges.

+ */ +public interface HTTP2Channel +{ + /** + *

A client specific handler for events that happen after + * a {@code HEADERS} response frame is received.

+ *

{@code DATA} frames may be handled as response content + * or as opaque tunnelled data.

+ */ + public interface Client + { + public void onData(DataFrame frame, Callback callback); + + public boolean onTimeout(Throwable failure); + + public void onFailure(Throwable failure, Callback callback); + } + + /** + *

A server specific handler for events that happen after + * a {@code HEADERS} request frame is received.

+ *

{@code DATA} frames may be handled as request content + * or as opaque tunnelled data.

+ */ + public interface Server + { + public Runnable onData(DataFrame frame, Callback callback); + + public Runnable onTrailer(HeadersFrame frame); + + public boolean onTimeout(Throwable failure, Consumer consumer); + + public Runnable onFailure(Throwable failure, Callback callback); + + public boolean isIdle(); + } +} diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Session.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Session.java index 1ffafe920653..88adcdffc607 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Session.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Session.java @@ -19,6 +19,7 @@ package org.eclipse.jetty.http2; import java.io.IOException; +import java.net.InetSocketAddress; import java.nio.channels.ClosedChannelException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -33,6 +34,7 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http2.api.Session; import org.eclipse.jetty.http2.api.Stream; import org.eclipse.jetty.http2.frames.DataFrame; @@ -43,6 +45,7 @@ import org.eclipse.jetty.http2.frames.GoAwayFrame; import org.eclipse.jetty.http2.frames.HeadersFrame; import org.eclipse.jetty.http2.frames.PingFrame; +import org.eclipse.jetty.http2.frames.PrefaceFrame; import org.eclipse.jetty.http2.frames.PriorityFrame; import org.eclipse.jetty.http2.frames.PushPromiseFrame; import org.eclipse.jetty.http2.frames.ResetFrame; @@ -561,6 +564,7 @@ public void newStream(HeadersFrame frame, Promise promise, Stream.Listen { // Synchronization is necessary to atomically create // the stream id and enqueue the frame to be sent. + IStream stream; boolean queued; synchronized (this) { @@ -573,12 +577,13 @@ public void newStream(HeadersFrame frame, Promise promise, Stream.Listen priority.getWeight(), priority.isExclusive()); frame = new HeadersFrame(streamId, frame.getMetaData(), priority, frame.isEndStream()); } - IStream stream = createLocalStream(streamId); + stream = createLocalStream(streamId, (MetaData.Request)frame.getMetaData()); stream.setListener(listener); ControlEntry entry = new ControlEntry(frame, stream, new StreamPromiseCallback(promise, stream)); queued = flusher.append(entry); } + stream.process(new PrefaceFrame(), Callback.NOOP); // Iterate outside the synchronized block. if (queued) flusher.iterate(); @@ -589,9 +594,9 @@ public void newStream(HeadersFrame frame, Promise promise, Stream.Listen } } - protected IStream newStream(int streamId, boolean local) + protected IStream newStream(int streamId, MetaData.Request request, boolean local) { - return new HTTP2Stream(scheduler, this, streamId, local); + return new HTTP2Stream(scheduler, this, streamId, request, local); } @Override @@ -622,7 +627,7 @@ public void push(IStream stream, Promise promise, PushPromiseFrame frame int streamId = localStreamIds.getAndAdd(2); frame = new PushPromiseFrame(frame.getStreamId(), streamId, frame.getMetaData()); - IStream pushStream = createLocalStream(streamId); + IStream pushStream = createLocalStream(streamId, frame.getMetaData()); pushStream.setListener(listener); ControlEntry entry = new ControlEntry(frame, pushStream, new StreamPromiseCallback(promise, pushStream)); @@ -778,7 +783,7 @@ private void frame(HTTP2Flusher.Entry entry, boolean flush) } } - protected IStream createLocalStream(int streamId) + protected IStream createLocalStream(int streamId, MetaData.Request request) { while (true) { @@ -791,7 +796,7 @@ protected IStream createLocalStream(int streamId) break; } - IStream stream = newStream(streamId, true); + IStream stream = newStream(streamId, request, true); if (streams.putIfAbsent(streamId, stream) == null) { stream.setIdleTimeout(getStreamIdleTimeout()); @@ -807,7 +812,7 @@ protected IStream createLocalStream(int streamId) } } - protected IStream createRemoteStream(int streamId) + protected IStream createRemoteStream(int streamId, MetaData.Request request) { // SPEC: exceeding max concurrent streams is treated as stream error. while (true) @@ -825,7 +830,7 @@ protected IStream createRemoteStream(int streamId) break; } - IStream stream = newStream(streamId, false); + IStream stream = newStream(streamId, request, false); // SPEC: duplicate stream is treated as connection error. if (streams.putIfAbsent(streamId, stream) == null) @@ -884,6 +889,18 @@ public IStream getStream(int streamId) return streams.get(streamId); } + @Override + public InetSocketAddress getLocalAddress() + { + return endPoint.getLocalAddress(); + } + + @Override + public InetSocketAddress getRemoteAddress() + { + return endPoint.getRemoteAddress(); + } + @ManagedAttribute(value = "The flow control send window", readonly = true) public int getSendWindow() { @@ -1556,6 +1573,8 @@ public void succeeded() @Override public void failed(Throwable x) { + if (LOG.isDebugEnabled()) + LOG.debug(x); complete(); } @@ -1576,6 +1595,8 @@ public void succeeded() @Override public void failed(Throwable x) { + if (LOG.isDebugEnabled()) + LOG.debug(x); complete(); } @@ -1612,6 +1633,8 @@ public void succeeded() @Override public void failed(Throwable x) { + if (LOG.isDebugEnabled()) + LOG.debug(x); complete(); } @@ -1632,6 +1655,8 @@ public void succeeded() @Override public void failed(Throwable x) { + if (LOG.isDebugEnabled()) + LOG.debug(x); complete(); } diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Stream.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Stream.java index 4da6aaa8d1f6..c7030d731fe9 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Stream.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Stream.java @@ -30,6 +30,7 @@ import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http2.api.Stream; import org.eclipse.jetty.http2.frames.DataFrame; @@ -60,17 +61,19 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa private final long timeStamp = System.nanoTime(); private final ISession session; private final int streamId; + private final MetaData.Request request; private final boolean local; private boolean localReset; private Listener listener; private boolean remoteReset; private long dataLength; - public HTTP2Stream(Scheduler scheduler, ISession session, int streamId, boolean local) + public HTTP2Stream(Scheduler scheduler, ISession session, int streamId, MetaData.Request request, boolean local) { super(scheduler); this.session = session; this.streamId = streamId; + this.request = request; this.local = local; this.dataLength = Long.MIN_VALUE; } @@ -237,6 +240,11 @@ public void process(Frame frame, Callback callback) notIdle(); switch (frame.getType()) { + case PREFACE: + { + onNewStream(callback); + break; + } case HEADERS: { onHeaders((HeadersFrame)frame, callback); @@ -274,6 +282,12 @@ public void process(Frame frame, Callback callback) } } + private void onNewStream(Callback callback) + { + notifyNewStream(this); + callback.succeeded(); + } + private void onHeaders(HeadersFrame frame, Callback callback) { MetaData metaData = frame.getMetaData(); @@ -281,7 +295,7 @@ private void onHeaders(HeadersFrame frame, Callback callback) { HttpFields fields = metaData.getFields(); long length = -1; - if (fields != null) + if (fields != null && !HttpMethod.CONNECT.is(request.getMethod())) length = fields.getLongField(HttpHeader.CONTENT_LENGTH.asString()); dataLength = length >= 0 ? length : Long.MIN_VALUE; } @@ -543,6 +557,22 @@ private Callback endWrite() return writing.getAndSet(null); } + private void notifyNewStream(Stream stream) + { + Listener listener = this.listener; + if (listener != null) + { + try + { + listener.onNewStream(stream); + } + catch (Throwable x) + { + LOG.info("Failure while notifying listener " + listener, x); + } + } + } + private void notifyData(Stream stream, DataFrame frame, Callback callback) { Listener listener = this.listener; diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2StreamEndPoint.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2StreamEndPoint.java new file mode 100644 index 000000000000..7b90ad1926e5 --- /dev/null +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2StreamEndPoint.java @@ -0,0 +1,661 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http2; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.channels.ReadPendingException; +import java.nio.channels.WritePendingException; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.jetty.http2.frames.DataFrame; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.EofException; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.thread.Invocable; + +public abstract class HTTP2StreamEndPoint implements EndPoint +{ + private static final Logger LOG = Log.getLogger(HTTP2StreamEndPoint.class); + + private final Deque dataQueue = new ArrayDeque<>(); + private final AtomicReference writeState = new AtomicReference<>(WriteState.IDLE); + private final AtomicReference readCallback = new AtomicReference<>(); + private final long created = System.currentTimeMillis(); + private final AtomicBoolean eof = new AtomicBoolean(); + private final AtomicBoolean closed = new AtomicBoolean(); + private final IStream stream; + private Connection connection; + + public HTTP2StreamEndPoint(IStream stream) + { + this.stream = stream; + } + + @Override + public InetSocketAddress getLocalAddress() + { + return stream.getSession().getLocalAddress(); + } + + @Override + public InetSocketAddress getRemoteAddress() + { + return stream.getSession().getRemoteAddress(); + } + + @Override + public boolean isOpen() + { + return !closed.get(); + } + + @Override + public long getCreatedTimeStamp() + { + return created; + } + + @Override + public void shutdownOutput() + { + while (true) + { + WriteState current = writeState.get(); + switch (current.state) + { + case IDLE: + case OSHUTTING: + if (!writeState.compareAndSet(current, WriteState.OSHUT)) + break; + stream.data(new DataFrame(stream.getId(), BufferUtil.EMPTY_BUFFER, true), Callback.from(this::oshutSuccess, this::oshutFailure)); + return; + case PENDING: + if (!writeState.compareAndSet(current, WriteState.OSHUTTING)) + break; + return; + case OSHUT: + case FAILED: + return; + } + } + } + + private void oshutSuccess() + { + WriteState current = writeState.get(); + switch (current.state) + { + case IDLE: + case PENDING: + case OSHUTTING: + throw new IllegalStateException(); + case OSHUT: + case FAILED: + break; + } + } + + private void oshutFailure(Throwable failure) + { + while (true) + { + WriteState current = writeState.get(); + switch (current.state) + { + case IDLE: + case PENDING: + case OSHUTTING: + throw new IllegalStateException(); + case OSHUT: + if (!writeState.compareAndSet(current, new WriteState(WriteState.State.FAILED, failure))) + break; + return; + case FAILED: + return; + } + } + } + + @Override + public boolean isOutputShutdown() + { + WriteState.State state = writeState.get().state; + return state == WriteState.State.OSHUTTING || state == WriteState.State.OSHUT; + } + + @Override + public boolean isInputShutdown() + { + return eof.get(); + } + + @Override + public void close(Throwable cause) + { + if (closed.compareAndSet(false, true)) + { + if (LOG.isDebugEnabled()) + LOG.debug("closing {}, cause: {}", this, cause); + shutdownOutput(); + stream.close(); + onClose(cause); + } + } + + @Override + public int fill(ByteBuffer sink) throws IOException + { + Entry entry; + synchronized (this) + { + entry = dataQueue.poll(); + } + + if (LOG.isDebugEnabled()) + LOG.debug("filled {} on {}", entry, this); + + if (entry == null) + return 0; + if (entry.isEOF()) + { + entry.succeed(); + return shutdownInput(); + } + IOException failure = entry.ioFailure(); + if (failure != null) + { + entry.fail(failure); + throw failure; + } + + int sinkPosition = BufferUtil.flipToFill(sink); + ByteBuffer source = entry.buffer; + int sourceLength = source.remaining(); + int length = Math.min(sourceLength, sink.remaining()); + int sourceLimit = source.limit(); + source.limit(source.position() + length); + sink.put(source); + source.limit(sourceLimit); + BufferUtil.flipToFlush(sink, sinkPosition); + + if (source.hasRemaining()) + { + synchronized (this) + { + dataQueue.offerFirst(entry); + } + } + else + { + entry.succeed(); + } + return length; + } + + private int shutdownInput() + { + eof.set(true); + return -1; + } + + @Override + public boolean flush(ByteBuffer... buffers) throws IOException + { + if (LOG.isDebugEnabled()) + LOG.debug("flushing {} on {}", BufferUtil.toDetailString(buffers), this); + if (buffers == null || buffers.length == 0) + { + return true; + } + else + { + while (true) + { + WriteState current = writeState.get(); + switch (current.state) + { + case IDLE: + if (!writeState.compareAndSet(current, WriteState.PENDING)) + break; + // We must copy the buffers because, differently from + // write(), the semantic of flush() is that it does not + // own them, but stream.data() needs to own them. + ByteBuffer buffer = coalesce(buffers, true); + Callback.Completable callback = new Callback.Completable(Invocable.InvocationType.NON_BLOCKING); + stream.data(new DataFrame(stream.getId(), buffer, false), callback); + callback.whenComplete((nothing, failure) -> + { + if (failure == null) + flushSuccess(); + else + flushFailure(failure); + }); + return callback.isDone(); + case PENDING: + return false; + case OSHUTTING: + case OSHUT: + throw new EofException("Output shutdown"); + case FAILED: + Throwable failure = current.failure; + if (failure instanceof IOException) + throw (IOException)failure; + throw new IOException(failure); + } + } + } + } + + private void flushSuccess() + { + while (true) + { + WriteState current = writeState.get(); + switch (current.state) + { + case IDLE: + case OSHUT: + throw new IllegalStateException(); + case PENDING: + if (!writeState.compareAndSet(current, WriteState.IDLE)) + break; + return; + case OSHUTTING: + shutdownOutput(); + return; + case FAILED: + return; + } + } + } + + private void flushFailure(Throwable failure) + { + while (true) + { + WriteState current = writeState.get(); + switch (current.state) + { + case IDLE: + case OSHUT: + throw new IllegalStateException(); + case PENDING: + if (!writeState.compareAndSet(current, new WriteState(WriteState.State.FAILED, failure))) + break; + return; + case OSHUTTING: + shutdownOutput(); + return; + case FAILED: + return; + } + } + } + + @Override + public Object getTransport() + { + return stream; + } + + @Override + public long getIdleTimeout() + { + return stream.getIdleTimeout(); + } + + @Override + public void setIdleTimeout(long idleTimeout) + { + stream.setIdleTimeout(idleTimeout); + } + + @Override + public void fillInterested(Callback callback) throws ReadPendingException + { + if (!tryFillInterested(callback)) + throw new ReadPendingException(); + } + + @Override + public boolean tryFillInterested(Callback callback) + { + boolean result = readCallback.compareAndSet(null, callback); + if (result) + process(); + return result; + } + + @Override + public boolean isFillInterested() + { + return readCallback.get() != null; + } + + @Override + public void write(Callback callback, ByteBuffer... buffers) throws WritePendingException + { + if (LOG.isDebugEnabled()) + LOG.debug("writing {} on {}", BufferUtil.toDetailString(buffers), this); + if (buffers == null || buffers.length == 0 || remaining(buffers) == 0) + { + callback.succeeded(); + } + else + { + while (true) + { + WriteState current = writeState.get(); + switch (current.state) + { + case IDLE: + if (!writeState.compareAndSet(current, WriteState.PENDING)) + break; + // TODO: we really need a Stream primitive to write multiple frames. + ByteBuffer result = coalesce(buffers, false); + stream.data(new DataFrame(stream.getId(), result, false), Callback.from(() -> writeSuccess(callback), x -> writeFailure(x, callback))); + return; + case PENDING: + callback.failed(new WritePendingException()); + return; + case OSHUTTING: + case OSHUT: + callback.failed(new EofException("Output shutdown")); + return; + case FAILED: + callback.failed(current.failure); + return; + } + } + } + } + + private void writeSuccess(Callback callback) + { + while (true) + { + WriteState current = writeState.get(); + switch (current.state) + { + case IDLE: + case OSHUT: + callback.failed(new IllegalStateException()); + return; + case PENDING: + if (!writeState.compareAndSet(current, WriteState.IDLE)) + break; + callback.succeeded(); + return; + case OSHUTTING: + callback.succeeded(); + shutdownOutput(); + return; + case FAILED: + callback.failed(current.failure); + return; + } + } + } + + private void writeFailure(Throwable failure, Callback callback) + { + while (true) + { + WriteState current = writeState.get(); + switch (current.state) + { + case IDLE: + case OSHUT: + callback.failed(new IllegalStateException(failure)); + return; + case PENDING: + case OSHUTTING: + if (!writeState.compareAndSet(current, new WriteState(WriteState.State.FAILED, failure))) + break; + callback.failed(failure); + return; + case FAILED: + return; + } + } + } + + private long remaining(ByteBuffer... buffers) + { + long total = 0; + for (ByteBuffer buffer : buffers) + total += buffer.remaining(); + return total; + } + + private ByteBuffer coalesce(ByteBuffer[] buffers, boolean forceCopy) + { + if (buffers.length == 1 && !forceCopy) + return buffers[0]; + long capacity = remaining(buffers); + if (capacity > Integer.MAX_VALUE) + throw new BufferOverflowException(); + ByteBuffer result = BufferUtil.allocateDirect((int)capacity); + for (ByteBuffer buffer : buffers) + BufferUtil.append(result, buffer); + return result; + } + + @Override + public Connection getConnection() + { + return connection; + } + + @Override + public void setConnection(Connection connection) + { + this.connection = connection; + } + + @Override + public void onOpen() + { + if (LOG.isDebugEnabled()) + LOG.debug("onOpen {}", this); + } + + @Override + public void onClose(Throwable cause) + { + if (LOG.isDebugEnabled()) + LOG.debug("onClose {}", this); + } + + @Override + public boolean isOptimizedForDirectBuffers() + { + return true; + } + + @Override + public void upgrade(Connection newConnection) + { + Connection oldConnection = getConnection(); + + ByteBuffer buffer = null; + if (oldConnection instanceof Connection.UpgradeFrom) + buffer = ((Connection.UpgradeFrom)oldConnection).onUpgradeFrom(); + + if (oldConnection != null) + oldConnection.onClose(null); + + if (LOG.isDebugEnabled()) + LOG.debug("upgrading from {} to {} with data {} on {}", oldConnection, newConnection, BufferUtil.toDetailString(buffer), this); + + setConnection(newConnection); + if (newConnection instanceof Connection.UpgradeTo && buffer != null) + ((Connection.UpgradeTo)newConnection).onUpgradeTo(buffer); + + newConnection.onOpen(); + } + + protected void offerData(DataFrame frame, Callback callback) + { + ByteBuffer buffer = frame.getData(); + if (LOG.isDebugEnabled()) + LOG.debug("offering {} on {}", frame, this); + if (frame.isEndStream()) + { + if (buffer.hasRemaining()) + offer(buffer, Callback.from(() -> {}, callback::failed), null); + offer(BufferUtil.EMPTY_BUFFER, callback, Entry.EOF); + } + else + { + if (buffer.hasRemaining()) + offer(buffer, callback, null); + else + callback.succeeded(); + } + process(); + } + + protected void offerFailure(Throwable failure) + { + offer(BufferUtil.EMPTY_BUFFER, Callback.NOOP, failure); + process(); + } + + private void offer(ByteBuffer buffer, Callback callback, Throwable failure) + { + synchronized (this) + { + dataQueue.offer(new Entry(buffer, callback, failure)); + } + } + + protected void process() + { + boolean empty; + synchronized (this) + { + empty = dataQueue.isEmpty(); + } + if (!empty) + { + Callback callback = readCallback.getAndSet(null); + if (callback != null) + callback.succeeded(); + } + } + + @Override + public String toString() + { + // Do not call Stream.toString() because it stringifies the attachment, + // which could be this instance, therefore causing a StackOverflowError. + return String.format("%s@%x[%s@%x#%d][w=%s]", getClass().getSimpleName(), hashCode(), + stream.getClass().getSimpleName(), stream.hashCode(), stream.getId(), + writeState); + } + + private static class Entry + { + private static final Throwable EOF = new Throwable(); + + private final ByteBuffer buffer; + private final Callback callback; + private final Throwable failure; + + private Entry(ByteBuffer buffer, Callback callback, Throwable failure) + { + this.buffer = buffer; + this.callback = callback; + this.failure = failure; + } + + private boolean isEOF() + { + return failure == EOF; + } + + private IOException ioFailure() + { + if (failure == null || isEOF()) + return null; + return failure instanceof IOException ? (IOException)failure : new IOException(failure); + } + + private void succeed() + { + callback.succeeded(); + } + + private void fail(Throwable failure) + { + callback.failed(failure); + } + + @Override + public String toString() + { + return String.format("%s@%x[b=%s,eof=%b,f=%s]", getClass().getSimpleName(), hashCode(), + BufferUtil.toDetailString(buffer), isEOF(), isEOF() ? null : failure); + } + } + + private static class WriteState + { + public static final WriteState IDLE = new WriteState(State.IDLE); + public static final WriteState PENDING = new WriteState(State.PENDING); + public static final WriteState OSHUTTING = new WriteState(State.OSHUTTING); + public static final WriteState OSHUT = new WriteState(State.OSHUT); + + private final State state; + private final Throwable failure; + + private WriteState(State state) + { + this(state, null); + } + + private WriteState(State state, Throwable failure) + { + this.state = state; + this.failure = failure; + } + + @Override + public String toString() + { + return state.toString(); + } + + private enum State + { + IDLE, PENDING, OSHUTTING, OSHUT, FAILED + } + } +} diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Session.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Session.java index 39888cd86134..91a492ea1b37 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Session.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Session.java @@ -18,6 +18,7 @@ package org.eclipse.jetty.http2.api; +import java.net.InetSocketAddress; import java.util.Collection; import java.util.Map; @@ -125,6 +126,18 @@ public interface Session */ public Stream getStream(int streamId); + /** + * @return the local network address this session is bound to, + * or {@code null} if this session is not bound to a network address + */ + public InetSocketAddress getLocalAddress(); + + /** + * @return the remote network address this session is connected to, + * or {@code null} if this session is not connected to a network address + */ + public InetSocketAddress getRemoteAddress(); + /** *

A {@link Listener} is the passive counterpart of a {@link Session} and * receives events happening on a HTTP/2 connection.

diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Stream.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Stream.java index 6f02b8288250..913cd2940298 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Stream.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Stream.java @@ -137,6 +137,16 @@ public interface Stream */ public interface Listener { + /** + *

Callback method invoked when a stream is created locally by + * {@link Session#newStream(HeadersFrame, Promise, Listener)}.

+ * + * @param stream the newly created stream + */ + public default void onNewStream(Stream stream) + { + } + /** *

Callback method invoked when a HEADERS frame representing the HTTP response has been received.

* diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/PushPromiseFrame.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/PushPromiseFrame.java index b6a0d9faf4aa..e61a1828d799 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/PushPromiseFrame.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/PushPromiseFrame.java @@ -24,9 +24,9 @@ public class PushPromiseFrame extends Frame { private final int streamId; private final int promisedStreamId; - private final MetaData metaData; + private final MetaData.Request metaData; - public PushPromiseFrame(int streamId, int promisedStreamId, MetaData metaData) + public PushPromiseFrame(int streamId, int promisedStreamId, MetaData.Request metaData) { super(FrameType.PUSH_PROMISE); this.streamId = streamId; @@ -44,7 +44,7 @@ public int getPromisedStreamId() return promisedStreamId; } - public MetaData getMetaData() + public MetaData.Request getMetaData() { return metaData; } diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/SettingsFrame.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/SettingsFrame.java index c4f78e1371af..4a4e2d4247fc 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/SettingsFrame.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/SettingsFrame.java @@ -54,6 +54,6 @@ public boolean isReply() @Override public String toString() { - return String.format("%s,reply=%b:%s", super.toString(), reply, settings); + return String.format("%s,reply=%b,params=%s", super.toString(), reply, settings); } } diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PushPromiseBodyParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PushPromiseBodyParser.java index 9e4341acb005..e50512475686 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PushPromiseBodyParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PushPromiseBodyParser.java @@ -124,7 +124,7 @@ public boolean parse(ByteBuffer buffer) } case HEADERS: { - MetaData metaData = headerBlockParser.parse(buffer, length); + MetaData.Request metaData = (MetaData.Request)headerBlockParser.parse(buffer, length); if (metaData == HeaderBlockParser.SESSION_FAILURE) return false; if (metaData != null) @@ -157,7 +157,7 @@ public boolean parse(ByteBuffer buffer) return false; } - private void onPushPromise(int streamId, MetaData metaData) + private void onPushPromise(int streamId, MetaData.Request metaData) { PushPromiseFrame frame = new PushPromiseFrame(getStreamId(), streamId, metaData); notifyPushPromise(frame); diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java index eaa17a53ee41..bff99f5ba1a1 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java @@ -26,6 +26,7 @@ import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpVersion; @@ -165,10 +166,17 @@ public void encode(ByteBuffer buffer, MetaData metadata) // TODO optimise these to avoid HttpField creation String scheme = request.getURI().getScheme(); - encode(buffer, new HttpField(HttpHeader.C_SCHEME, scheme == null ? HttpScheme.HTTP.asString() : scheme)); - encode(buffer, new HttpField(HttpHeader.C_METHOD, request.getMethod())); - encode(buffer, new HttpField(HttpHeader.C_AUTHORITY, request.getURI().getAuthority())); - encode(buffer, new HttpField(HttpHeader.C_PATH, request.getURI().getPathQuery())); + encode(buffer,new HttpField(HttpHeader.C_METHOD,request.getMethod())); + encode(buffer,new HttpField(HttpHeader.C_AUTHORITY,request.getURI().getAuthority())); + boolean isConnect = HttpMethod.CONNECT.is(request.getMethod()); + String protocol = request.getProtocol(); + if (!isConnect || protocol != null) + { + encode(buffer,new HttpField(HttpHeader.C_SCHEME,scheme == null ? HttpScheme.HTTP.asString() : scheme)); + encode(buffer,new HttpField(HttpHeader.C_PATH,request.getURI().getPathQuery())); + if (protocol != null) + encode(buffer,new HttpField(HttpHeader.C_PROTOCOL,protocol)); + } } else if (metadata.isResponse()) { diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java index 8afb5d5344a0..cad98d49c599 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java @@ -22,6 +22,7 @@ import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.MetaData; @@ -36,6 +37,7 @@ public class MetaDataBuilder private HttpScheme _scheme; private HostPortHttpField _authority; private String _path; + private String _protocol; private long _contentLength = Long.MIN_VALUE; private HttpFields _fields = new HttpFields(); private HpackException.StreamException _streamException; @@ -140,6 +142,23 @@ else if (value != null) _request = true; break; + case C_PATH: + if (checkPseudoHeader(header, _path)) + { + if (value != null && value.length() > 0) + _path = value; + else + streamException("No Path"); + } + _request = true; + break; + + case C_PROTOCOL: + if (checkPseudoHeader(header, _protocol)) + _protocol = value; + _request = true; + break; + case HOST: // :authority fields must come first. If we have one, ignore the host header as far as authority goes. if (_authority == null) @@ -152,37 +171,26 @@ else if (value != null) _fields.add(field); break; - case C_PATH: - if (checkPseudoHeader(header, _path)) - { - if (value != null && value.length() > 0) - _path = value; - else - streamException("No Path"); - } - _request = true; - break; - case CONTENT_LENGTH: _contentLength = field.getLongValue(); _fields.add(field); break; - + case TE: if ("trailers".equalsIgnoreCase(value)) _fields.add(field); else streamException("Unsupported TE value '%s'", value); break; - + case CONNECTION: if ("TE".equalsIgnoreCase(value)) _fields.add(field); else streamException("Connection specific field '%s'", header); - break; + break; - default: + default: if (name.charAt(0) == ':') streamException("Unknown pseudo header '%s'", name); else @@ -228,7 +236,7 @@ public MetaData build() throws HpackException.StreamException _streamException.addSuppressed(new Throwable()); throw _streamException; } - + if (_request && _response) throw new HpackException.StreamException("Request and Response headers"); @@ -239,11 +247,18 @@ public MetaData build() throws HpackException.StreamException { if (_method == null) throw new HpackException.StreamException("No Method"); - if (_scheme == null) - throw new HpackException.StreamException("No Scheme"); - if (_path == null) - throw new HpackException.StreamException("No Path"); - return new MetaData.Request(_method, _scheme, _authority, _path, HttpVersion.HTTP_2, fields, _contentLength); + boolean isConnect = HttpMethod.CONNECT.is(_method); + if (!isConnect || _protocol != null) + { + if (_scheme == null) + throw new HpackException.StreamException("No Scheme"); + if (_path == null) + throw new HpackException.StreamException("No Path"); + } + if (isConnect) + return new MetaData.ConnectRequest(_scheme, _authority, _path, fields, _protocol); + else + return new MetaData.Request(_method, _scheme, _authority, _path, HttpVersion.HTTP_2, fields, _contentLength); } if (_response) { @@ -251,7 +266,7 @@ public MetaData build() throws HpackException.StreamException throw new HpackException.StreamException("No Status"); return new MetaData.Response(HttpVersion.HTTP_2, _status, fields, _contentLength); } - + return new MetaData(HttpVersion.HTTP_2, fields, _contentLength); } finally @@ -264,6 +279,7 @@ public MetaData build() throws HpackException.StreamException _scheme = null; _authority = null; _path = null; + _protocol = null; _size = 0; _contentLength = Long.MIN_VALUE; } diff --git a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/ClientHTTP2StreamEndPoint.java b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/ClientHTTP2StreamEndPoint.java new file mode 100644 index 000000000000..7acc4eb86ea8 --- /dev/null +++ b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/ClientHTTP2StreamEndPoint.java @@ -0,0 +1,61 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http2.client.http; + +import org.eclipse.jetty.http2.HTTP2Channel; +import org.eclipse.jetty.http2.HTTP2StreamEndPoint; +import org.eclipse.jetty.http2.IStream; +import org.eclipse.jetty.http2.frames.DataFrame; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +public class ClientHTTP2StreamEndPoint extends HTTP2StreamEndPoint implements HTTP2Channel.Client +{ + private static final Logger LOG = Log.getLogger(ClientHTTP2StreamEndPoint.class); + + public ClientHTTP2StreamEndPoint(IStream stream) + { + super(stream); + } + + @Override + public void onData(DataFrame frame, Callback callback) + { + offerData(frame, callback); + } + + @Override + public boolean onTimeout(Throwable failure) + { + if (LOG.isDebugEnabled()) + LOG.debug("idle timeout on {}: {}", this, failure); + Connection connection = getConnection(); + if (connection != null) + return connection.onIdleExpired(); + return true; + } + + @Override + public void onFailure(Throwable failure, Callback callback) + { + callback.failed(failure); + } +} diff --git a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpChannelOverHTTP2.java b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpChannelOverHTTP2.java index 3373e5a14011..5731619b7ef2 100644 --- a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpChannelOverHTTP2.java +++ b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpChannelOverHTTP2.java @@ -18,6 +18,8 @@ package org.eclipse.jetty.http2.client.http; +import java.io.IOException; + import org.eclipse.jetty.client.HttpChannel; import org.eclipse.jetty.client.HttpDestination; import org.eclipse.jetty.client.HttpExchange; @@ -25,14 +27,19 @@ import org.eclipse.jetty.client.HttpSender; import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.http2.ErrorCode; +import org.eclipse.jetty.http2.HTTP2Channel; import org.eclipse.jetty.http2.IStream; import org.eclipse.jetty.http2.api.Session; import org.eclipse.jetty.http2.api.Stream; +import org.eclipse.jetty.http2.frames.DataFrame; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.http2.frames.PushPromiseFrame; import org.eclipse.jetty.http2.frames.ResetFrame; import org.eclipse.jetty.util.Callback; public class HttpChannelOverHTTP2 extends HttpChannel { + private final Stream.Listener listener = new Listener(); private final HttpConnectionOverHTTP2 connection; private final Session session; private final HttpSenderOverHTTP2 sender; @@ -60,7 +67,7 @@ public Session getSession() public Stream.Listener getStreamListener() { - return receiver; + return listener; } @Override @@ -83,6 +90,8 @@ public Stream getStream() public void setStream(Stream stream) { this.stream = stream; + if (stream != null) + ((IStream)stream).setAttachment(receiver); } public boolean isFailed() @@ -156,4 +165,60 @@ public InvocationType getInvocationType() return InvocationType.NON_BLOCKING; } } + + private class Listener implements Stream.Listener + { + @Override + public void onNewStream(Stream stream) + { + setStream(stream); + } + + @Override + public void onHeaders(Stream stream, HeadersFrame frame) + { + receiver.onHeaders(stream, frame); + } + + @Override + public Stream.Listener onPush(Stream stream, PushPromiseFrame frame) + { + return receiver.onPush(stream, frame); + } + + @Override + public void onData(Stream stream, DataFrame frame, Callback callback) + { + HTTP2Channel.Client channel = (HTTP2Channel.Client)((IStream)stream).getAttachment(); + channel.onData(frame, callback); + } + + @Override + public void onReset(Stream stream, ResetFrame frame) + { + // TODO: needs to call HTTP2Channel? + receiver.onReset(stream, frame); + } + + @Override + public boolean onIdleTimeout(Stream stream, Throwable x) + { + HTTP2Channel.Client channel = (HTTP2Channel.Client)((IStream)stream).getAttachment(); + return channel.onTimeout(x); + } + + @Override + public void onFailure(Stream stream, int error, String reason, Callback callback) + { + HTTP2Channel.Client channel = (HTTP2Channel.Client)((IStream)stream).getAttachment(); + channel.onFailure(new IOException(String.format("Failure %s/%s", ErrorCode.toString(error, null), reason)), callback); + } + + @Override + public void onClosed(Stream stream) + { + // TODO: needs to call HTTP2Channel? + receiver.onClosed(stream); + } + } } diff --git a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2.java b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2.java index 7efe42ccdc63..fb5ec54b0411 100644 --- a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2.java +++ b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2.java @@ -20,14 +20,17 @@ import java.io.IOException; import java.net.InetSocketAddress; +import java.util.List; import java.util.Map; import org.eclipse.jetty.alpn.client.ALPNClientConnectionFactory; import org.eclipse.jetty.client.AbstractHttpClientTransport; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpDestination; +import org.eclipse.jetty.client.HttpRequest; import org.eclipse.jetty.client.MultiplexConnectionPool; import org.eclipse.jetty.client.MultiplexHttpDestination; +import org.eclipse.jetty.client.Origin; import org.eclipse.jetty.client.ProxyConfiguration; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http2.api.Session; @@ -104,6 +107,13 @@ protected void doStop() throws Exception removeBean(client); } + @Override + public HttpDestination.Key newDestinationKey(HttpRequest request, Origin origin) + { + String protocol = HttpScheme.HTTPS.is(origin.getScheme()) ? "h2" : "h2c"; + return new HttpDestination.Key(origin, new HttpDestination.Protocol(List.of(protocol), false)); + } + @Override public HttpDestination newHttpDestination(HttpDestination.Key key) { diff --git a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpReceiverOverHTTP2.java b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpReceiverOverHTTP2.java index 0d91d5ea4e33..afcc8ed7d63f 100644 --- a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpReceiverOverHTTP2.java +++ b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpReceiverOverHTTP2.java @@ -35,21 +35,24 @@ import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http2.ErrorCode; +import org.eclipse.jetty.http2.HTTP2Channel; import org.eclipse.jetty.http2.IStream; import org.eclipse.jetty.http2.api.Stream; import org.eclipse.jetty.http2.frames.DataFrame; import org.eclipse.jetty.http2.frames.HeadersFrame; import org.eclipse.jetty.http2.frames.PushPromiseFrame; import org.eclipse.jetty.http2.frames.ResetFrame; +import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.IteratingCallback; import org.eclipse.jetty.util.Retainable; -public class HttpReceiverOverHTTP2 extends HttpReceiver implements Stream.Listener +public class HttpReceiverOverHTTP2 extends HttpReceiver implements HTTP2Channel.Client { private final ContentNotifier contentNotifier = new ContentNotifier(); @@ -71,8 +74,7 @@ protected void reset() contentNotifier.reset(); } - @Override - public void onHeaders(Stream stream, HeadersFrame frame) + void onHeaders(Stream stream, HeadersFrame frame) { HttpExchange exchange = getHttpExchange(); if (exchange == null) @@ -94,6 +96,19 @@ public void onHeaders(Stream stream, HeadersFrame frame) return; } + HttpRequest httpRequest = exchange.getRequest(); + if (HttpMethod.CONNECT.is(httpRequest.getMethod()) && httpResponse.getStatus() == HttpStatus.OK_200) + { + ClientHTTP2StreamEndPoint endPoint = new ClientHTTP2StreamEndPoint((IStream)stream); + long idleTimeout = httpRequest.getIdleTimeout(); + if (idleTimeout > 0) + endPoint.setIdleTimeout(idleTimeout); + if (LOG.isDebugEnabled()) + LOG.debug("Successful HTTP2 tunnel on {} via {}", stream, endPoint); + ((IStream)stream).setAttachment(endPoint); + httpRequest.getConversation().setAttribute(EndPoint.class.getName(), endPoint); + } + if (responseHeaders(exchange)) { int status = response.getStatus(); @@ -111,15 +126,14 @@ public void onHeaders(Stream stream, HeadersFrame frame) } } - @Override - public Stream.Listener onPush(Stream stream, PushPromiseFrame frame) + Stream.Listener onPush(Stream stream, PushPromiseFrame frame) { HttpExchange exchange = getHttpExchange(); if (exchange == null) return null; HttpRequest request = exchange.getRequest(); - MetaData.Request metaData = (MetaData.Request)frame.getMetaData(); + MetaData.Request metaData = frame.getMetaData(); HttpRequest pushRequest = (HttpRequest)getHttpDestination().getHttpClient().newRequest(metaData.getURIString()); // TODO: copy PUSH_PROMISE headers into pushRequest. @@ -146,7 +160,7 @@ public Stream.Listener onPush(Stream stream, PushPromiseFrame frame) } @Override - public void onData(Stream stream, DataFrame frame, Callback callback) + public void onData(DataFrame frame, Callback callback) { HttpExchange exchange = getHttpExchange(); if (exchange == null) @@ -159,8 +173,7 @@ public void onData(Stream stream, DataFrame frame, Callback callback) } } - @Override - public void onReset(Stream stream, ResetFrame frame) + void onReset(Stream stream, ResetFrame frame) { HttpExchange exchange = getHttpExchange(); if (exchange == null) @@ -170,23 +183,22 @@ public void onReset(Stream stream, ResetFrame frame) } @Override - public boolean onIdleTimeout(Stream stream, Throwable x) + public boolean onTimeout(Throwable failure) { HttpExchange exchange = getHttpExchange(); if (exchange == null) return false; - return !exchange.abort(x); + return !exchange.abort(failure); } @Override - public void onFailure(Stream stream, int error, String reason, Callback callback) + public void onFailure(Throwable failure, Callback callback) { - responseFailure(new IOException(String.format("%s/%s", ErrorCode.toString(error, null), reason))); + responseFailure(failure); callback.succeeded(); } - @Override - public void onClosed(Stream stream) + void onClosed(Stream stream) { getHttpChannel().onStreamClosed((IStream)stream); } diff --git a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpSenderOverHTTP2.java b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpSenderOverHTTP2.java index e1078ad0976c..ff2d71aca668 100644 --- a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpSenderOverHTTP2.java +++ b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpSenderOverHTTP2.java @@ -19,17 +19,19 @@ package org.eclipse.jetty.http2.client.http; import java.net.URI; +import java.util.function.Consumer; import java.util.function.Supplier; import org.eclipse.jetty.client.HttpContent; import org.eclipse.jetty.client.HttpExchange; import org.eclipse.jetty.client.HttpRequest; import org.eclipse.jetty.client.HttpSender; +import org.eclipse.jetty.http.HostPortHttpField; import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.MetaData; -import org.eclipse.jetty.http2.IStream; import org.eclipse.jetty.http2.api.Stream; import org.eclipse.jetty.http2.frames.DataFrame; import org.eclipse.jetty.http2.frames.HeadersFrame; @@ -53,23 +55,35 @@ protected HttpChannelOverHTTP2 getHttpChannel() protected void sendHeaders(HttpExchange exchange, final HttpContent content, final Callback callback) { HttpRequest request = exchange.getRequest(); - String path = relativize(request.getPath()); - HttpURI uri = HttpURI.createHttpURI(request.getScheme(), request.getHost(), request.getPort(), path, null, request.getQuery(), null); - MetaData.Request metaData = new MetaData.Request(request.getMethod(), uri, HttpVersion.HTTP_2, request.getHeaders()); + boolean isTunnel = HttpMethod.CONNECT.is(request.getMethod()); + MetaData.Request metaData; + if (isTunnel) + { + metaData = new MetaData.Request(request.getMethod(), null, new HostPortHttpField(request.getPath()), null, HttpVersion.HTTP_2, request.getHeaders()); + } + else + { + String path = relativize(request.getPath()); + HttpURI uri = HttpURI.createHttpURI(request.getScheme(), request.getHost(), request.getPort(), path, null, request.getQuery(), null); + metaData = new MetaData.Request(request.getMethod(), uri, HttpVersion.HTTP_2, request.getHeaders()); + } Supplier trailerSupplier = request.getTrailers(); metaData.setTrailerSupplier(trailerSupplier); HeadersFrame headersFrame; Promise promise; - if (content.hasContent()) + if (isTunnel) { headersFrame = new HeadersFrame(metaData, null, false); - promise = new HeadersPromise(request, callback) + promise = new HeadersPromise(request, callback, stream -> callback.succeeded()); + } + else + { + if (content.hasContent()) { - @Override - public void succeeded(Stream stream) + headersFrame = new HeadersFrame(metaData, null, false); + promise = new HeadersPromise(request, callback, stream -> { - super.succeeded(stream); if (expects100Continue(request)) { // Don't send the content yet. @@ -84,26 +98,21 @@ public void succeeded(Stream stream) else callback.succeeded(); } - } - }; - } - else - { - HttpFields trailers = trailerSupplier == null ? null : trailerSupplier.get(); - boolean endStream = trailers == null || trailers.size() == 0; - headersFrame = new HeadersFrame(metaData, null, endStream); - promise = new HeadersPromise(request, callback) + }); + } + else { - @Override - public void succeeded(Stream stream) + HttpFields trailers = trailerSupplier == null ? null : trailerSupplier.get(); + boolean endStream = trailers == null || trailers.size() <= 0; + headersFrame = new HeadersFrame(metaData, null, endStream); + promise = new HeadersPromise(request, callback, stream -> { - super.succeeded(stream); if (endStream) callback.succeeded(); else sendTrailers(stream, trailers, callback); - } - }; + }); + } } // TODO optimize the send of HEADERS and DATA frames. HttpChannelOverHTTP2 channel = getHttpChannel(); @@ -168,26 +177,26 @@ private void sendTrailers(Stream stream, HttpFields trailers, Callback callback) stream.headers(trailersFrame, callback); } - private class HeadersPromise implements Promise + private static class HeadersPromise implements Promise { private final HttpRequest request; private final Callback callback; + private final Consumer succeed; - private HeadersPromise(HttpRequest request, Callback callback) + private HeadersPromise(HttpRequest request, Callback callback, Consumer succeed) { this.request = request; this.callback = callback; + this.succeed = succeed; } @Override public void succeeded(Stream stream) { - HttpChannelOverHTTP2 channel = getHttpChannel(); - channel.setStream(stream); - ((IStream)stream).setAttachment(channel); long idleTimeout = request.getIdleTimeout(); if (idleTimeout >= 0) stream.setIdleTimeout(idleTimeout); + succeed.accept(stream); } @Override diff --git a/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2Test.java b/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2Test.java index 32ccb0701cc8..a067f510fcd5 100644 --- a/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2Test.java +++ b/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2Test.java @@ -43,6 +43,7 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpDestination; import org.eclipse.jetty.client.HttpProxy; +import org.eclipse.jetty.client.Origin; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpMethod; @@ -341,7 +342,7 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques }); int proxyPort = connector.getLocalPort(); - client.getProxyConfiguration().getProxies().add(new HttpProxy("localhost", proxyPort)); + client.getProxyConfiguration().getProxies().add(new HttpProxy(new Origin.Address("localhost", proxyPort), false, new HttpDestination.Protocol(List.of("h2c"), false))); int serverPort = proxyPort + 1; // Any port will do, just not the same as the proxy. ContentResponse response = client.newRequest("localhost", serverPort) diff --git a/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/MaxConcurrentStreamsTest.java b/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/MaxConcurrentStreamsTest.java index 9dd125b626a7..df7f6e39535b 100644 --- a/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/MaxConcurrentStreamsTest.java +++ b/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/MaxConcurrentStreamsTest.java @@ -159,7 +159,6 @@ protected void service(String target, Request jettyRequest, HttpServletRequest r } }); - String scheme = "http"; String host = "localhost"; int port = connector.getLocalPort(); @@ -212,15 +211,15 @@ public void onSettings(Session session, SettingsFrame frame) // This request will be queued and establish the connection, // which will trigger the send of the second request. - ContentResponse response1 = client.newRequest(host, port) - .path("/1") - .timeout(5, TimeUnit.SECONDS) - .send(); + var request = client.newRequest(host, port) + .path("/1") + .timeout(5, TimeUnit.SECONDS); + ContentResponse response = request.send(); - assertEquals(HttpStatus.OK_200, response1.getStatus()); + assertEquals(HttpStatus.OK_200, response.getStatus()); assertTrue(latch.await(5, TimeUnit.SECONDS), failures.toString()); assertEquals(2, connections.get()); - HttpDestination destination = (HttpDestination)client.getDestination(scheme, host, port); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); AbstractConnectionPool connectionPool = (AbstractConnectionPool)destination.getConnectionPool(); assertEquals(2, connectionPool.getConnectionCount()); } @@ -253,16 +252,15 @@ protected void service(String target, Request jettyRequest, HttpServletRequest r // Send the request in excess. CountDownLatch latch = new CountDownLatch(1); String path = "/excess"; - client.newRequest("localhost", connector.getLocalPort()) - .path(path) - .send(result -> - { - if (result.getResponse().getStatus() == HttpStatus.OK_200) - latch.countDown(); - }); + var request = client.newRequest("localhost", connector.getLocalPort()).path(path); + request.send(result -> + { + if (result.getResponse().getStatus() == HttpStatus.OK_200) + latch.countDown(); + }); // The last exchange should remain in the queue. - HttpDestination destination = (HttpDestination)client.getDestination("http", "localhost", connector.getLocalPort()); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); assertEquals(1, destination.getHttpExchanges().size()); assertEquals(path, destination.getHttpExchanges().peek().getRequest().getPath()); diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/AbstractHTTP2ServerConnectionFactory.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/AbstractHTTP2ServerConnectionFactory.java index a7492e814f80..dda71dd05e8f 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/AbstractHTTP2ServerConnectionFactory.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/AbstractHTTP2ServerConnectionFactory.java @@ -210,9 +210,8 @@ public Connection newConnection(Connector connector, EndPoint endPoint) // the typical case is that the connection will be busier and the // stream idle timeout will expire earlier than the connection's. long streamIdleTimeout = getStreamIdleTimeout(); - if (streamIdleTimeout <= 0) - streamIdleTimeout = endPoint.getIdleTimeout(); - session.setStreamIdleTimeout(streamIdleTimeout); + if (streamIdleTimeout > 0) + session.setStreamIdleTimeout(streamIdleTimeout); session.setInitialSessionRecvWindow(getInitialSessionRecvWindow()); session.setWriteThreshold(getHttpConfiguration().getOutputBufferSize()); diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java index 331696417d29..2dd10e670e66 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java @@ -35,9 +35,11 @@ import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http.MetaData.Request; import org.eclipse.jetty.http2.ErrorCode; +import org.eclipse.jetty.http2.HTTP2Channel; import org.eclipse.jetty.http2.HTTP2Connection; import org.eclipse.jetty.http2.ISession; import org.eclipse.jetty.http2.IStream; @@ -86,7 +88,7 @@ public static boolean isSupportedProtocol(String protocol) return false; } } - + private final Queue channels = new ArrayDeque<>(); private final List upgradeFrames = new ArrayList<>(); private final AtomicLong totalRequests = new AtomicLong(); @@ -176,10 +178,10 @@ public void onData(IStream stream, DataFrame frame, Callback callback) { if (LOG.isDebugEnabled()) LOG.debug("Processing {} on {}", frame, stream); - HttpChannelOverHTTP2 channel = (HttpChannelOverHTTP2)stream.getAttachment(); + HTTP2Channel.Server channel = (HTTP2Channel.Server)stream.getAttachment(); if (channel != null) { - Runnable task = channel.onRequestContent(frame, callback); + Runnable task = channel.onData(frame, callback); if (task != null) offerTask(task, false); } @@ -193,10 +195,10 @@ public void onTrailers(IStream stream, HeadersFrame frame) { if (LOG.isDebugEnabled()) LOG.debug("Processing trailers {} on {}", frame, stream); - HttpChannelOverHTTP2 channel = (HttpChannelOverHTTP2)stream.getAttachment(); + HTTP2Channel.Server channel = (HTTP2Channel.Server)stream.getAttachment(); if (channel != null) { - Runnable task = channel.onRequestTrailers(frame); + Runnable task = channel.onTrailer(frame); if (task != null) offerTask(task, false); } @@ -204,8 +206,8 @@ public void onTrailers(IStream stream, HeadersFrame frame) public boolean onStreamTimeout(IStream stream, Throwable failure) { - HttpChannelOverHTTP2 channel = (HttpChannelOverHTTP2)stream.getAttachment(); - boolean result = channel != null && channel.onStreamTimeout(failure, task -> offerTask(task, true)); + HTTP2Channel.Server channel = (HTTP2Channel.Server)stream.getAttachment(); + boolean result = channel != null && channel.onTimeout(failure, task -> offerTask(task, true)); if (LOG.isDebugEnabled()) LOG.debug("{} idle timeout on {}: {}", result ? "Processed" : "Ignored", stream, failure); return result; @@ -215,7 +217,7 @@ public void onStreamFailure(IStream stream, Throwable failure, Callback callback { if (LOG.isDebugEnabled()) LOG.debug("Processing failure on {}: {}", stream, failure); - HttpChannelOverHTTP2 channel = (HttpChannelOverHTTP2)stream.getAttachment(); + HTTP2Channel.Server channel = (HTTP2Channel.Server)stream.getAttachment(); if (channel != null) { Runnable task = channel.onFailure(failure, callback); @@ -233,11 +235,11 @@ public boolean onSessionTimeout(Throwable failure) ISession session = getSession(); // Compute whether all requests are idle. boolean result = session.getStreams().stream() - .map(stream -> (IStream)stream) - .map(stream -> (HttpChannelOverHTTP2)stream.getAttachment()) - .filter(Objects::nonNull) - .map(HttpChannelOverHTTP2::isRequestIdle) - .reduce(true, Boolean::logicalAnd); + .map(stream -> (IStream)stream) + .map(stream -> (HTTP2Channel.Server)stream.getAttachment()) + .filter(Objects::nonNull) + .map(HTTP2Channel.Server::isIdle) + .reduce(true, Boolean::logicalAnd); if (LOG.isDebugEnabled()) LOG.debug("{} idle timeout on {}: {}", result ? "Processed" : "Ignored", session, failure); return result; @@ -373,15 +375,27 @@ public Runnable onRequest(HeadersFrame frame) return super.onRequest(frame); } + @Override + protected void checkAndPrepareUpgrade() + { + if (isTunnel()) + getHttpTransport().prepareUpgrade(); + } + @Override public void onCompleted() { - totalResponses.incrementAndGet(); super.onCompleted(); - if (!getStream().isReset()) + totalResponses.incrementAndGet(); + if (!getStream().isReset() && !isTunnel()) recycle(); } + private boolean isTunnel() + { + return HttpMethod.CONNECT.is(getRequest().getMethod()) && getResponse().getStatus() == HttpStatus.OK_200; + } + @Override public void recycle() { diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerSession.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerSession.java index 9e657a9b4643..b5ee20f14639 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerSession.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerSession.java @@ -104,7 +104,7 @@ public void onHeaders(HeadersFrame frame) } else { - stream = createRemoteStream(streamId); + stream = createRemoteStream(streamId, (MetaData.Request)metaData); if (stream != null) { onStreamOpened(stream); diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpChannelOverHTTP2.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpChannelOverHTTP2.java index 617025a5465b..da64dfd58aa3 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpChannelOverHTTP2.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpChannelOverHTTP2.java @@ -29,9 +29,11 @@ import org.eclipse.jetty.http.HttpGenerator; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeaderValue; +import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http.PreEncodedHttpField; +import org.eclipse.jetty.http2.HTTP2Channel; import org.eclipse.jetty.http2.IStream; import org.eclipse.jetty.http2.api.Stream; import org.eclipse.jetty.http2.frames.DataFrame; @@ -47,7 +49,7 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, WriteFlusher.Listener +public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, WriteFlusher.Listener, HTTP2Channel.Server { private static final Logger LOG = Log.getLogger(HttpChannelOverHTTP2.class); private static final HttpField SERVER_VERSION = new PreEncodedHttpField(HttpHeader.SERVER, HttpConfiguration.SERVER_VERSION); @@ -127,16 +129,16 @@ public Runnable onRequest(HeadersFrame frame) } _delayedUntilContent = getHttpConfiguration().isDelayDispatchUntilContent() && - !endStream && !_expect100Continue; + !endStream && !_expect100Continue && !HttpMethod.CONNECT.is(request.getMethod()); if (LOG.isDebugEnabled()) { Stream stream = getStream(); LOG.debug("HTTP2 Request #{}/{}, delayed={}:{}{} {} {}{}{}", - stream.getId(), Integer.toHexString(stream.getSession().hashCode()), - _delayedUntilContent, System.lineSeparator(), - request.getMethod(), request.getURI(), request.getHttpVersion(), - System.lineSeparator(), fields); + stream.getId(), Integer.toHexString(stream.getSession().hashCode()), + _delayedUntilContent, System.lineSeparator(), + request.getMethod(), request.getURI(), request.getHttpVersion(), + System.lineSeparator(), fields); } return _delayedUntilContent ? null : this; @@ -166,9 +168,9 @@ public Runnable onPushRequest(MetaData.Request request) { Stream stream = getStream(); LOG.debug("HTTP2 PUSH Request #{}/{}:{}{} {} {}{}{}", - stream.getId(), Integer.toHexString(stream.getSession().hashCode()), System.lineSeparator(), - request.getMethod(), request.getURI(), request.getHttpVersion(), - System.lineSeparator(), request.getFields()); + stream.getId(), Integer.toHexString(stream.getSession().hashCode()), System.lineSeparator(), + request.getMethod(), request.getURI(), request.getHttpVersion(), + System.lineSeparator(), request.getFields()); } return this; @@ -208,11 +210,17 @@ protected void commit(MetaData.Response info) { Stream stream = getStream(); LOG.debug("HTTP2 Commit Response #{}/{}:{}{} {} {}{}{}", - stream.getId(), Integer.toHexString(stream.getSession().hashCode()), System.lineSeparator(), info.getHttpVersion(), info.getStatus(), info.getReason(), - System.lineSeparator(), info.getFields()); + stream.getId(), Integer.toHexString(stream.getSession().hashCode()), System.lineSeparator(), info.getHttpVersion(), info.getStatus(), info.getReason(), + System.lineSeparator(), info.getFields()); } } + @Override + public Runnable onData(DataFrame frame, Callback callback) + { + return onRequestContent(frame, callback); + } + public Runnable onRequestContent(DataFrame frame, final Callback callback) { Stream stream = getStream(); @@ -260,11 +268,11 @@ public InvocationType getInvocationType() if (LOG.isDebugEnabled()) { LOG.debug("HTTP2 Request #{}/{}: {} bytes of {} content, handle: {}", - stream.getId(), - Integer.toHexString(stream.getSession().hashCode()), - length, - endStream ? "last" : "some", - handle); + stream.getId(), + Integer.toHexString(stream.getSession().hashCode()), + length, + endStream ? "last" : "some", + handle); } boolean wasDelayed = _delayedUntilContent; @@ -272,7 +280,8 @@ public InvocationType getInvocationType() return handle || wasDelayed ? this : null; } - public Runnable onRequestTrailers(HeadersFrame frame) + @Override + public Runnable onTrailer(HeadersFrame frame) { HttpFields trailers = frame.getMetaData().getFields(); if (trailers.size() > 0) @@ -282,8 +291,8 @@ public Runnable onRequestTrailers(HeadersFrame frame) { Stream stream = getStream(); LOG.debug("HTTP2 Request #{}/{}, trailers:{}{}", - stream.getId(), Integer.toHexString(stream.getSession().hashCode()), - System.lineSeparator(), trailers); + stream.getId(), Integer.toHexString(stream.getSession().hashCode()), + System.lineSeparator(), trailers); } boolean handle = onRequestComplete(); @@ -293,17 +302,19 @@ public Runnable onRequestTrailers(HeadersFrame frame) return handle || wasDelayed ? this : null; } - public boolean isRequestIdle() + @Override + public boolean isIdle() { return getState().isIdle(); } - public boolean onStreamTimeout(Throwable failure, Consumer consumer) + @Override + public boolean onTimeout(Throwable failure, Consumer consumer) { final boolean delayed = _delayedUntilContent; _delayedUntilContent = false; - boolean result = isRequestIdle(); + boolean result = isIdle(); if (result) consumeInput(); @@ -317,6 +328,7 @@ public boolean onStreamTimeout(Throwable failure, Consumer consumer) return result; } + @Override public Runnable onFailure(Throwable failure, Callback callback) { getHttpTransport().onStreamFailure(failure); @@ -368,6 +380,18 @@ public void continue100(int available) throws IOException } } + @Override + public boolean isTunnellingSupported() + { + return true; + } + + @Override + public EndPoint getTunnellingEndPoint() + { + return new ServerHTTP2StreamEndPoint(getStream()); + } + @Override public void close() { diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java index 11cd845cce0c..1f53feba0582 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java @@ -23,6 +23,7 @@ import java.util.function.Supplier; import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.MetaData; @@ -33,8 +34,11 @@ import org.eclipse.jetty.http2.frames.HeadersFrame; import org.eclipse.jetty.http2.frames.PushPromiseFrame; import org.eclipse.jetty.http2.frames.ResetFrame; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.HttpTransport; +import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Promise; @@ -50,7 +54,7 @@ public class HttpTransportOverHTTP2 implements HttpTransport private final Connector connector; private final HTTP2ServerConnection connection; private IStream stream; - private MetaData metaData; + private MetaData.Response metaData; public HttpTransportOverHTTP2(Connector connector, HTTP2ServerConnection connection) { @@ -85,13 +89,14 @@ public void recycle() } @Override - public void send(MetaData.Response info, boolean isHeadRequest, ByteBuffer content, boolean lastContent, Callback callback) + public void send(MetaData.Request request, MetaData.Response response, ByteBuffer content, boolean lastContent, Callback callback) { + boolean isHeadRequest = HttpMethod.HEAD.is(request.getMethod()); boolean hasContent = BufferUtil.hasContent(content) && !isHeadRequest; - if (info != null) + if (response != null) { - metaData = info; - int status = info.getStatus(); + metaData = response; + int status = response.getStatus(); boolean interimResponse = status == HttpStatus.CONTINUE_100 || status == HttpStatus.PROCESSING_102; if (interimResponse) { @@ -103,7 +108,7 @@ public void send(MetaData.Response info, boolean isHeadRequest, ByteBuffer conte else { if (transportCallback.start(callback, false)) - sendHeadersFrame(info, false, transportCallback); + sendHeadersFrame(response, false, transportCallback); } } else @@ -139,28 +144,36 @@ public void succeeded() } }; if (transportCallback.start(commitCallback, true)) - sendHeadersFrame(info, false, transportCallback); + sendHeadersFrame(response, false, transportCallback); } else { if (lastContent) { - HttpFields trailers = retrieveTrailers(); - if (trailers != null) + if (isTunnel(request, response)) { - if (transportCallback.start(new SendTrailers(callback, trailers), true)) - sendHeadersFrame(info, false, transportCallback); + if (transportCallback.start(callback, true)) + sendHeadersFrame(response, false, transportCallback); } else { - if (transportCallback.start(callback, true)) - sendHeadersFrame(info, true, transportCallback); + HttpFields trailers = retrieveTrailers(); + if (trailers != null) + { + if (transportCallback.start(new SendTrailers(callback, trailers), true)) + sendHeadersFrame(response, false, transportCallback); + } + else + { + if (transportCallback.start(callback, true)) + sendHeadersFrame(response, true, transportCallback); + } } } else { if (transportCallback.start(callback, true)) - sendHeadersFrame(info, false, transportCallback); + sendHeadersFrame(response, false, transportCallback); } } } @@ -172,7 +185,7 @@ public void succeeded() } else { - if (hasContent || lastContent) + if (hasContent || (lastContent && !isTunnel(request, response))) { if (lastContent) { @@ -220,6 +233,11 @@ private HttpFields retrieveTrailers() return trailers.size() == 0 ? null : trailers; } + private boolean isTunnel(MetaData.Request request, MetaData.Response response) + { + return HttpMethod.CONNECT.is(request.getMethod()) && response.getStatus() == HttpStatus.OK_200; + } + @Override public boolean isPushSupported() { @@ -239,7 +257,7 @@ public void push(final MetaData.Request request) if (LOG.isDebugEnabled()) LOG.debug("HTTP/2 Push {}", request); - stream.push(new PushPromiseFrame(stream.getId(), 0, request), new Promise() + stream.push(new PushPromiseFrame(stream.getId(), 0, request), new Promise<>() { @Override public void succeeded(Stream pushStream) @@ -304,23 +322,50 @@ public boolean onStreamTimeout(Throwable failure) return transportCallback.onIdleTimeout(failure); } + void prepareUpgrade() + { + HttpChannelOverHTTP2 channel = (HttpChannelOverHTTP2)stream.getAttachment(); + Request request = channel.getRequest(); + Connection connection = (Connection)request.getAttribute(UPGRADE_CONNECTION_ATTRIBUTE); + EndPoint endPoint = connection.getEndPoint(); + endPoint.upgrade(connection); + stream.setAttachment(endPoint); + if (request.getHttpInput().hasContent()) + channel.sendErrorOrAbort("Unexpected content in CONNECT request"); + } + @Override public void onCompleted() { - // If the stream is not closed, it is still reading the request content. - // Send a reset to the other end so that it stops sending data. - if (!stream.isClosed()) + Object attachment = stream.getAttachment(); + if (attachment instanceof HttpChannelOverHTTP2) { - if (LOG.isDebugEnabled()) - LOG.debug("HTTP2 Response #{}: unconsumed request content, resetting stream", stream.getId()); - stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code), Callback.NOOP); - } + HttpChannelOverHTTP2 channel = (HttpChannelOverHTTP2)attachment; + if (channel.getResponse().getStatus() == HttpStatus.SWITCHING_PROTOCOLS_101) + { + Connection connection = (Connection)channel.getRequest().getAttribute(UPGRADE_CONNECTION_ATTRIBUTE); + EndPoint endPoint = connection.getEndPoint(); + // TODO: check that endPoint implements HTTP2Channel. + if (LOG.isDebugEnabled()) + LOG.debug("Tunnelling DATA frames through {}", endPoint); + endPoint.upgrade(connection); + stream.setAttachment(endPoint); + return; + } - // Consume the existing queued data frames to - // avoid stalling the session flow control. - HttpChannelOverHTTP2 channel = (HttpChannelOverHTTP2)stream.getAttachment(); - if (channel != null) + // If the stream is not closed, it is still reading the request content. + // Send a reset to the other end so that it stops sending data. + if (!stream.isClosed()) + { + if (LOG.isDebugEnabled()) + LOG.debug("HTTP2 Response #{}: unconsumed request content, resetting stream", stream.getId()); + stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code), Callback.NOOP); + } + + // Consume the existing queued data frames to + // avoid stalling the session flow control. channel.consumeInput(); + } } @Override diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/ServerHTTP2StreamEndPoint.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/ServerHTTP2StreamEndPoint.java new file mode 100644 index 000000000000..b54d9a4e87b4 --- /dev/null +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/ServerHTTP2StreamEndPoint.java @@ -0,0 +1,86 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http2.server; + +import java.util.function.Consumer; + +import org.eclipse.jetty.http2.HTTP2Channel; +import org.eclipse.jetty.http2.HTTP2StreamEndPoint; +import org.eclipse.jetty.http2.IStream; +import org.eclipse.jetty.http2.frames.DataFrame; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +public class ServerHTTP2StreamEndPoint extends HTTP2StreamEndPoint implements HTTP2Channel.Server +{ + private static final Logger LOG = Log.getLogger(ServerHTTP2StreamEndPoint.class); + + public ServerHTTP2StreamEndPoint(IStream stream) + { + super(stream); + } + + @Override + public Runnable onData(DataFrame frame, Callback callback) + { + offerData(frame, callback); + return null; + } + + @Override + public Runnable onTrailer(HeadersFrame frame) + { + // We are tunnelling, so there are no trailers. + return null; + } + + @Override + public boolean onTimeout(Throwable failure, Consumer consumer) + { + if (LOG.isDebugEnabled()) + LOG.debug("idle timeout on {}: {}", this, failure); + offerFailure(failure); + boolean result = true; + Connection connection = getConnection(); + if (connection != null) + result = connection.onIdleExpired(); + consumer.accept(() -> close(failure)); + return result; + } + + @Override + public Runnable onFailure(Throwable failure, Callback callback) + { + if (LOG.isDebugEnabled()) + LOG.debug("failure on {}: {}", this, failure); + offerFailure(failure); + close(failure); + return callback::succeeded; + } + + @Override + public boolean isIdle() + { + // We are tunnelling, so we are never idle. + return false; + } +} diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnectionFactory.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnectionFactory.java index d6bcf20369b0..56f71586c63b 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnectionFactory.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnectionFactory.java @@ -81,7 +81,13 @@ public ClientConnectionFactory getClientConnectionFactory() */ public boolean matches(List candidates) { - return protocols.stream().anyMatch(candidates::contains); + return protocols.stream().anyMatch(p -> candidates.stream().anyMatch(c -> c.equalsIgnoreCase(p))); + } + + @Override + public String toString() + { + return String.format("%s@%x%s", getClass().getSimpleName(), hashCode(), protocols); } } } diff --git a/jetty-proxy/pom.xml b/jetty-proxy/pom.xml index 7e37da585688..d8964411ccd0 100644 --- a/jetty-proxy/pom.xml +++ b/jetty-proxy/pom.xml @@ -48,6 +48,11 @@ jetty-client ${project.version} + + org.eclipse.jetty + jetty-alpn-client + ${project.version} + org.eclipse.jetty diff --git a/jetty-proxy/src/main/java/module-info.java b/jetty-proxy/src/main/java/module-info.java index 4b4549c5cc6c..d76a5841a078 100644 --- a/jetty-proxy/src/main/java/module-info.java +++ b/jetty-proxy/src/main/java/module-info.java @@ -21,6 +21,7 @@ exports org.eclipse.jetty.proxy; requires jetty.servlet.api; + requires org.eclipse.jetty.alpn.client; requires org.eclipse.jetty.client; requires org.eclipse.jetty.http; requires org.eclipse.jetty.io; diff --git a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java index d8ad11148a02..bf7dd3cec3ae 100644 --- a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java +++ b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java @@ -42,13 +42,14 @@ import org.eclipse.jetty.client.ProtocolHandlers; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; -import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; +import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeaderValue; +import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.util.HttpCookieStore; -import org.eclipse.jetty.util.ProcessorUtils; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -350,11 +351,23 @@ protected HttpClient createHttpClient() throws ServletException */ protected HttpClient newHttpClient() { - int selectors = Math.max(1, ProcessorUtils.availableProcessors() / 2); + int selectors = 1; String value = getServletConfig().getInitParameter("selectors"); if (value != null) selectors = Integer.parseInt(value); - return new HttpClient(new HttpClientTransportOverHTTP(selectors)); + ClientConnector clientConnector = newClientConnector(); + clientConnector.setSelectors(selectors); + return newHttpClient(clientConnector); + } + + protected HttpClient newHttpClient(ClientConnector clientConnector) + { + return new HttpClient(new HttpClientTransportDynamic(clientConnector)); + } + + protected ClientConnector newClientConnector() + { + return new ClientConnector(); } protected HttpClient getHttpClient() @@ -411,8 +424,14 @@ protected String rewriteTarget(HttpServletRequest clientRequest) { if (!validateDestination(clientRequest.getServerName(), clientRequest.getServerPort())) return null; - + // If the proxy is secure, we will likely get a proxied URI + // with the "https" scheme, but the upstream server needs + // to be called with the "http" scheme (the ConnectHandler + // is used to call upstream servers with the "https" scheme). StringBuffer target = clientRequest.getRequestURL(); + // Change "https" to "http". + if (HttpScheme.HTTPS.is(target.substring(0, 5))) + target.replace(4, 5, ""); String query = clientRequest.getQueryString(); if (query != null) target.append("?").append(query); diff --git a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ConnectHandler.java b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ConnectHandler.java index a74b5823b32b..7af0f914df48 100644 --- a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ConnectHandler.java +++ b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ConnectHandler.java @@ -38,6 +38,8 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeaderValue; 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.Connection; import org.eclipse.jetty.io.EndPoint; @@ -46,7 +48,7 @@ import org.eclipse.jetty.io.SelectorManager; import org.eclipse.jetty.io.SocketChannelEndPoint; import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.HttpConnection; +import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.HttpTransport; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.handler.HandlerWrapper; @@ -192,19 +194,24 @@ protected SelectorManager newSelectorManager() } @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - if (HttpMethod.CONNECT.is(request.getMethod())) + String tunnelProtocol = jettyRequest.getMetaData().getProtocol(); + if (HttpMethod.CONNECT.is(request.getMethod()) && tunnelProtocol == null) { - String serverAddress = request.getRequestURI(); + String serverAddress = target; + if (HttpVersion.HTTP_2.is(request.getProtocol())) + { + HttpURI httpURI = jettyRequest.getHttpURI(); + serverAddress = httpURI.getHost() + ":" + httpURI.getPort(); + } if (LOG.isDebugEnabled()) LOG.debug("CONNECT request for {}", serverAddress); - - handleConnect(baseRequest, request, response, serverAddress); + handleConnect(jettyRequest, request, response, serverAddress); } else { - super.handle(target, baseRequest, request, response); + super.handle(target, jettyRequest, request, response); } } @@ -244,12 +251,11 @@ protected void handleConnect(Request baseRequest, HttpServletRequest request, Ht return; } - HttpTransport transport = baseRequest.getHttpChannel().getHttpTransport(); - // TODO Handle CONNECT over HTTP2! - if (!(transport instanceof HttpConnection)) + HttpChannel httpChannel = baseRequest.getHttpChannel(); + if (!httpChannel.isTunnellingSupported()) { if (LOG.isDebugEnabled()) - LOG.debug("CONNECT not supported for {}", transport); + LOG.debug("CONNECT not supported for {}", httpChannel); sendConnectResponse(request, response, HttpServletResponse.SC_FORBIDDEN); return; } @@ -260,12 +266,12 @@ protected void handleConnect(Request baseRequest, HttpServletRequest request, Ht if (LOG.isDebugEnabled()) LOG.debug("Connecting to {}:{}", host, port); - connectToServer(request, host, port, new Promise() + connectToServer(request, host, port, new Promise<>() { @Override public void succeeded(SocketChannel channel) { - ConnectContext connectContext = new ConnectContext(request, response, asyncContext, (HttpConnection)transport); + ConnectContext connectContext = new ConnectContext(request, response, asyncContext, httpChannel.getTunnellingEndPoint()); if (channel.isConnected()) selector.accept(channel, connectContext); else @@ -335,8 +341,7 @@ protected void onConnectSuccess(ConnectContext connectContext, UpstreamConnectio HttpServletRequest request = connectContext.getRequest(); prepareContext(request, context); - HttpConnection httpConnection = connectContext.getHttpConnection(); - EndPoint downstreamEndPoint = httpConnection.getEndPoint(); + EndPoint downstreamEndPoint = connectContext.getEndPoint(); DownstreamConnection downstreamConnection = newDownstreamConnection(downstreamEndPoint, context); downstreamConnection.setInputBufferSize(getBufferSize()); @@ -370,11 +375,10 @@ private void sendConnectResponse(HttpServletRequest request, HttpServletResponse response.setContentLength(0); if (statusCode != HttpServletResponse.SC_OK) response.setHeader(HttpHeader.CONNECTION.asString(), HttpHeaderValue.CLOSE.asString()); - response.getOutputStream().close(); if (LOG.isDebugEnabled()) LOG.debug("CONNECT response sent {} {}", request.getProtocol(), response.getStatus()); } - catch (IOException x) + catch (Throwable x) { if (LOG.isDebugEnabled()) LOG.debug("Could not send CONNECT response", x); @@ -411,10 +415,9 @@ protected void prepareContext(HttpServletRequest request, ConcurrentMap getContext() @@ -564,9 +567,9 @@ public AsyncContext getAsyncContext() return asyncContext; } - public HttpConnection getHttpConnection() + public EndPoint getEndPoint() { - return httpConnection; + return endPoint; } } @@ -603,7 +606,7 @@ protected void write(EndPoint endPoint, ByteBuffer buffer, Callback callback) public class DownstreamConnection extends ProxyConnection implements Connection.UpgradeTo { - private ByteBuffer buffer; + private ByteBuffer buffer = BufferUtil.EMPTY_BUFFER; public DownstreamConnection(EndPoint endPoint, Executor executor, ByteBufferPool bufferPool, ConcurrentMap context) { @@ -613,7 +616,8 @@ public DownstreamConnection(EndPoint endPoint, Executor executor, ByteBufferPool @Override public void onUpgradeTo(ByteBuffer buffer) { - this.buffer = buffer == null ? BufferUtil.EMPTY_BUFFER : buffer; + if (buffer != null) + this.buffer = buffer; } @Override diff --git a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyConnection.java b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyConnection.java index 84bd52b1ba42..ce4f811e4696 100644 --- a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyConnection.java +++ b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyConnection.java @@ -37,7 +37,7 @@ public abstract class ProxyConnection extends AbstractConnection private final IteratingCallback pipe = new ProxyIteratingCallback(); private final ByteBufferPool bufferPool; private final ConcurrentMap context; - private Connection connection; + private ProxyConnection connection; protected ProxyConnection(EndPoint endp, Executor executor, ByteBufferPool bufferPool, ConcurrentMap context) { @@ -61,7 +61,7 @@ public Connection getConnection() return connection; } - public void setConnection(Connection connection) + public void setConnection(ProxyConnection connection) { this.connection = connection; } @@ -76,6 +76,11 @@ public void onFillable() protected abstract void write(EndPoint endPoint, ByteBuffer buffer, Callback callback); + protected void close(Throwable failure) + { + getEndPoint().close(failure); + } + @Override public String toConnectionString() { @@ -92,7 +97,7 @@ private class ProxyIteratingCallback extends IteratingCallback private int filled; @Override - protected Action process() throws Exception + protected Action process() { buffer = bufferPool.acquire(getInputBufferSize(), true); try @@ -123,7 +128,7 @@ else if (filled == 0) if (LOG.isDebugEnabled()) LOG.debug(ProxyConnection.this + " could not fill", x); bufferPool.release(buffer); - disconnect(); + disconnect(x); return Action.SUCCEEDED; } } @@ -147,14 +152,14 @@ protected void onCompleteFailure(Throwable x) { if (LOG.isDebugEnabled()) LOG.debug(ProxyConnection.this + " failed to write " + filled + " bytes", x); - disconnect(); + bufferPool.release(buffer); + disconnect(x); } - private void disconnect() + private void disconnect(Throwable x) { - bufferPool.release(buffer); - ProxyConnection.this.close(); - connection.close(); + ProxyConnection.this.close(x); + connection.close(x); } } } diff --git a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyServlet.java b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyServlet.java index 6f352e1a61fd..74594365b52c 100644 --- a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyServlet.java +++ b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyServlet.java @@ -55,9 +55,9 @@ public class ProxyServlet extends AbstractProxyServlet private static final String CONTINUE_ACTION_ATTRIBUTE = ProxyServlet.class.getName() + ".continueAction"; @Override - protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - final int requestId = getRequestId(request); + int requestId = getRequestId(request); String rewrittenTarget = rewriteTarget(request); @@ -76,7 +76,7 @@ protected void service(final HttpServletRequest request, final HttpServletRespon return; } - final Request proxyRequest = getHttpClient().newRequest(rewrittenTarget) + Request proxyRequest = getHttpClient().newRequest(rewrittenTarget) .method(request.getMethod()) .version(HttpVersion.fromString(request.getProtocol())); @@ -84,7 +84,7 @@ protected void service(final HttpServletRequest request, final HttpServletRespon addProxyHeaders(request, proxyRequest); - final AsyncContext asyncContext = request.startAsync(); + AsyncContext asyncContext = request.startAsync(); // We do not timeout the continuation, but the proxy request asyncContext.setTimeout(0); proxyRequest.timeout(getTimeout(), TimeUnit.MILLISECONDS); @@ -200,7 +200,7 @@ public void onHeaders(Response proxyResponse) } @Override - public void onContent(final Response proxyResponse, ByteBuffer content, final Callback callback) + public void onContent(Response proxyResponse, ByteBuffer content, Callback callback) { byte[] buffer; int offset; diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ForwardProxyTLSServerTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ForwardProxyTLSServerTest.java index ab57662feb58..6a00983a4b36 100644 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ForwardProxyTLSServerTest.java +++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ForwardProxyTLSServerTest.java @@ -300,7 +300,7 @@ public void testTwoConcurrentExchanges(SslContextFactory.Server scenario) throws .path("/echo?body=" + URLEncoder.encode(content1, "UTF-8")) .onRequestCommit(request -> { - Destination destination = httpClient.getDestination(HttpScheme.HTTPS.asString(), "localhost", serverConnector.getLocalPort()); + Destination destination = httpClient.resolveDestination(request); destination.newConnection(new Promise.Adapter<>() { @Override diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java index ecd99c7856f4..099ede80068e 100644 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java +++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java @@ -214,13 +214,12 @@ public void testProxyDown(Class proxyServletClass) throw // Shutdown the proxy proxy.stop(); - ExecutionException x = assertThrows(ExecutionException.class, - () -> - { - client.newRequest("localhost", serverConnector.getLocalPort()) - .timeout(5, TimeUnit.SECONDS) - .send(); - }); + ExecutionException x = assertThrows(ExecutionException.class, () -> + { + client.newRequest("localhost", serverConnector.getLocalPort()) + .timeout(5, TimeUnit.SECONDS) + .send(); + }); assertThat(x.getCause(), instanceOf(ConnectException.class)); } @@ -231,7 +230,7 @@ public void testProxyWithoutContent(Class proxyServletCl startServer(new HttpServlet() { @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { if (req.getHeader("Via") != null) resp.addHeader(PROXIED_HEADER, "true"); @@ -258,7 +257,7 @@ public void testProxyWithResponseContent(Class proxyServ startServer(new HttpServlet() { @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { if (req.getHeader("Via") != null) resp.addHeader(PROXIED_HEADER, "true"); @@ -292,7 +291,7 @@ public void testProxyWithRequestContentAndResponseContent(Class startServer(new HttpServlet() { @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { try { @@ -362,7 +361,7 @@ public void testProxyWithBigRequestContentConsumed(Class startServer(new HttpServlet() { @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { if (req.getHeader("Via") != null) resp.addHeader(PROXIED_HEADER, "true"); @@ -417,7 +416,7 @@ public void testProxyWithBigResponseContentWithSlowReader(Class proxyServletC startServer(new HttpServlet() { @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.getOutputStream().print(req.getQueryString()); } @@ -490,7 +489,7 @@ public void testProxyLongPoll(Class proxyServletClass) t startServer(new HttpServlet() { @Override - protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException + protected void doGet(final HttpServletRequest request, final HttpServletResponse response) { if (!request.isAsyncStarted()) { @@ -499,12 +498,12 @@ protected void doGet(final HttpServletRequest request, final HttpServletResponse asyncContext.addListener(new AsyncListener() { @Override - public void onComplete(AsyncEvent event) throws IOException + public void onComplete(AsyncEvent event) { } @Override - public void onTimeout(AsyncEvent event) throws IOException + public void onTimeout(AsyncEvent event) { if (request.getHeader("Via") != null) response.addHeader(PROXIED_HEADER, "true"); @@ -512,12 +511,12 @@ public void onTimeout(AsyncEvent event) throws IOException } @Override - public void onError(AsyncEvent event) throws IOException + public void onError(AsyncEvent event) { } @Override - public void onStartAsync(AsyncEvent event) throws IOException + public void onStartAsync(AsyncEvent event) { } }); @@ -541,7 +540,7 @@ public void testProxyXForwardedHostHeaderIsPresent(Class startServer(new HttpServlet() { @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { PrintWriter writer = resp.getWriter(); writer.write(req.getHeader("X-Forwarded-Host")); @@ -610,7 +609,7 @@ public void testClientExcludedHosts(Class proxyServletCl startServer(new HttpServlet() { @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { if (req.getHeader("Via") != null) resp.addHeader(PROXIED_HEADER, "true"); @@ -665,7 +664,7 @@ private void testTransparentProxyWithPrefix(Class proxyS startServer(new HttpServlet() { @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { if (req.getHeader("Via") != null) resp.addHeader(PROXIED_HEADER, "true"); @@ -722,7 +721,7 @@ private void testTransparentProxyWithQuery(Class proxySe startServer(new HttpServlet() { @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { if (req.getHeader("Via") != null) resp.addHeader(PROXIED_HEADER, "true"); @@ -767,7 +766,7 @@ public void testTransparentProxyWithQueryWithSpaces(Class prox startServer(new HttpServlet() { @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { if (req.getHeader("Via") != null) resp.addHeader(PROXIED_HEADER, "true"); @@ -840,7 +839,7 @@ public void testCachingProxy() throws Exception startServer(new HttpServlet() { @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { if (req.getHeader("Via") != null) resp.addHeader(PROXIED_HEADER, "true"); @@ -875,7 +874,7 @@ public void testRedirectsAreProxied(Class proxyServletCl startServer(new HttpServlet() { @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { if (req.getHeader("Via") != null) resp.addHeader(PROXIED_HEADER, "true"); @@ -902,7 +901,7 @@ public void testGZIPContentIsProxied(Class proxyServletC startServer(new HttpServlet() { @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { if (req.getHeader("Via") != null) resp.addHeader(PROXIED_HEADER, "true"); @@ -932,7 +931,7 @@ public void testWrongContentLength(Class proxyServletCla startServer(new HttpServlet() { @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { byte[] message = "tooshort".getBytes("ascii"); resp.setContentType("text/plain;charset=ascii"); @@ -964,7 +963,7 @@ public void testCookiesFromDifferentClientsAreNotMixed(Class proxyServletClass) throws Exception { int outputBufferSize = 1024; - final CountDownLatch chunk1Latch = new CountDownLatch(1); - final byte[] chunk1 = new byte[outputBufferSize]; + CountDownLatch chunk1Latch = new CountDownLatch(1); + byte[] chunk1 = new byte[outputBufferSize]; new Random().nextBytes(chunk1); - final int chunk2 = 'w'; + int chunk2 = 'w'; startServer(new HttpServlet() { @Override - protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException { ServletOutputStream output = response.getOutputStream(); output.write(chunk1); @@ -1142,29 +1142,29 @@ private boolean await(CountDownLatch latch, long ms) throws IOException InputStreamResponseListener listener = new InputStreamResponseListener(); int port = serverConnector.getLocalPort(); - client.newRequest("localhost", port).send(listener); + Request request = client.newRequest("localhost", port); + request.send(listener); Response response = listener.get(5, TimeUnit.SECONDS); assertEquals(200, response.getStatus()); InputStream input = listener.getInputStream(); - for (int i = 0; i < chunk1.length; ++i) + for (byte b : chunk1) { - assertEquals(chunk1[i] & 0xFF, input.read()); + assertEquals(b & 0xFF, input.read()); } TimeUnit.MILLISECONDS.sleep(2 * proxyTimeout); chunk1Latch.countDown(); - assertThrows(EOFException.class, - () -> - { - // Make sure the proxy does not receive chunk2. - input.read(); - }); + assertThrows(EOFException.class, () -> + { + // Make sure the proxy does not receive chunk2. + input.read(); + }); - HttpDestination destination = (HttpDestination)client.getDestination("http", "localhost", port); + HttpDestination destination = (HttpDestination)client.resolveDestination(request); ConnectionPool connectionPool = destination.getConnectionPool(); assertTrue(connectionPool.isEmpty()); } @@ -1181,7 +1181,7 @@ public void testResponseHeadersAreNotRemoved(Class proxy proxyContext.addFilter(new FilterHolder(new Filter() { @Override - public void init(FilterConfig filterConfig) throws ServletException + public void init(FilterConfig filterConfig) { } @@ -1221,7 +1221,7 @@ public void testHeadersListedByConnectionHeaderAreRemoved(Class names = Collections.list(request.getHeaderNames()); for (String name : names) @@ -1255,7 +1255,7 @@ public void testExpect100ContinueRespond100Continue(ClassChecks whether the processing of the request resulted in an upgrade, + * and if so performs upgrade preparation steps before the upgrade + * response is sent back to the client.

+ *

This avoids a race where the server is unprepared if the client sends + * data immediately after having received the upgrade response.

+ */ + protected void checkAndPrepareUpgrade() + { + } + public void onCompleted() { if (LOG.isDebugEnabled()) @@ -823,13 +848,13 @@ public void onBadMessage(BadMessageException failure) } } - protected boolean sendResponse(MetaData.Response info, ByteBuffer content, boolean complete, final Callback callback) + protected boolean sendResponse(MetaData.Response response, ByteBuffer content, boolean complete, final Callback callback) { boolean committing = _committed.compareAndSet(false, true); if (LOG.isDebugEnabled()) LOG.debug("sendResponse info={} content={} complete={} committing={} callback={}", - info, + response, BufferUtil.toDetailString(content), complete, committing, @@ -838,23 +863,23 @@ protected boolean sendResponse(MetaData.Response info, ByteBuffer content, boole if (committing) { // We need an info to commit - if (info == null) - info = _response.newResponseMetaData(); - commit(info); + if (response == null) + response = _response.newResponseMetaData(); + commit(response); - // wrap callback to process 100 responses - final int status = info.getStatus(); - final Callback committed = (status < 200 && status >= 100) ? new Send100Callback(callback) : new SendCallback(callback, content, true, complete); + // Wrap the callback to process 1xx responses. + Callback committed = HttpStatus.isInformational(response.getStatus()) + ? new Send100Callback(callback) : new SendCallback(callback, content, true, complete); notifyResponseBegin(_request); // committing write - _transport.send(info, _request.isHead(), content, complete, committed); + _transport.send(_request.getMetaData(), response, content, complete, committed); } - else if (info == null) + else if (response == null) { // This is a normal write - _transport.send(null, _request.isHead(), content, complete, new SendCallback(callback, content, false, complete)); + _transport.send(_request.getMetaData(), null, content, complete, new SendCallback(callback, content, false, complete)); } else { @@ -974,6 +999,16 @@ public void abort(Throwable failure) _transport.abort(failure); } + public boolean isTunnellingSupported() + { + return false; + } + + public EndPoint getTunnellingEndPoint() + { + throw new UnsupportedOperationException("Tunnelling not supported"); + } + private void notifyRequestBegin(Request request) { notifyEvent1(listener -> listener::onRequestBegin, request); @@ -1300,7 +1335,7 @@ public void failed(final Throwable x) if (x instanceof BadMessageException) { - _transport.send(HttpGenerator.RESPONSE_500_INFO, false, null, true, new Callback.Nested(this) + _transport.send(_request.getMetaData(), HttpGenerator.RESPONSE_500_INFO, null, true, new Callback.Nested(this) { @Override public void succeeded() diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelOverHttp.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelOverHttp.java index 24d3d94328d5..55243378512e 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelOverHttp.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelOverHttp.java @@ -229,7 +229,7 @@ public void earlyEOF() if (_metadata.getMethod() == null) _httpConnection.close(); else if (onEarlyEOF() || _delayedForContent) - { + { _delayedForContent = false; handle(); } @@ -283,6 +283,8 @@ public void badMessage(BadMessageException failure) @Override public boolean headerComplete() { + onRequest(_metadata); + if (_complianceViolations != null && !_complianceViolations.isEmpty()) { this.getRequest().setAttribute(HttpCompliance.VIOLATIONS_ATTR, _complianceViolations); @@ -363,9 +365,9 @@ public boolean headerComplete() _upgrade = PREAMBLE_UPGRADE_H2C; if (HttpMethod.PRI.is(_metadata.getMethod()) && - "*".equals(_metadata.getURI().toString()) && - _fields.size() == 0 && - upgrade()) + "*".equals(_metadata.getURI().getPath()) && + _fields.size() == 0 && + upgrade()) return true; badMessage(new BadMessageException(HttpStatus.UPGRADE_REQUIRED_426)); @@ -382,8 +384,6 @@ public boolean headerComplete() if (!persistent) _httpConnection.getGenerator().setPersistent(false); - onRequest(_metadata); - // Should we delay dispatch until we have some content? // We should not delay if there is no content expect or client is expecting 100 or the response is already committed or the request buffer already has something in it to parse _delayedForContent = (getHttpConfiguration().isDelayDispatchUntilContent() && @@ -472,8 +472,8 @@ private boolean upgrade() throws BadMessageException if (LOG.isDebugEnabled()) LOG.debug("Upgrade from {} to {}", getEndPoint().getConnection(), upgradeConnection); - getRequest().setAttribute(HttpConnection.UPGRADE_CONNECTION_ATTRIBUTE, upgradeConnection); - getResponse().setStatus(101); + getRequest().setAttribute(HttpTransport.UPGRADE_CONNECTION_ATTRIBUTE, upgradeConnection); + getResponse().setStatus(HttpStatus.SWITCHING_PROTOCOLS_101); getHttpTransport().onCompleted(); return true; } @@ -530,10 +530,22 @@ public void onComplianceViolation(ComplianceViolation.Mode mode, ComplianceViola _complianceViolations = new ArrayList<>(); } String record = String.format("%s (see %s) in mode %s for %s in %s", - violation.getDescription(), violation.getURL(), mode, details, getHttpTransport()); + violation.getDescription(), violation.getURL(), mode, details, getHttpTransport()); _complianceViolations.add(record); if (LOG.isDebugEnabled()) LOG.debug(record); } } + + @Override + public boolean isTunnellingSupported() + { + return true; + } + + @Override + public EndPoint getTunnellingEndPoint() + { + return getEndPoint(); + } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java index 3d717ca44970..8f5303c7737e 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java @@ -30,9 +30,9 @@ import org.eclipse.jetty.http.HttpGenerator; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeaderValue; +import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpParser; import org.eclipse.jetty.http.HttpParser.RequestHandler; -import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http.PreEncodedHttpField; import org.eclipse.jetty.io.AbstractConnection; @@ -54,7 +54,6 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http { private static final Logger LOG = Log.getLogger(HttpConnection.class); public static final HttpField CONNECTION_CLOSE = new PreEncodedHttpField(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString()); - public static final String UPGRADE_CONNECTION_ATTRIBUTE = "org.eclipse.jetty.server.HttpConnection.UPGRADE"; private static final ThreadLocal __currentConnection = new ThreadLocal<>(); private final HttpConfiguration _config; @@ -298,7 +297,7 @@ else if (filled < 0) LOG.debug("{} onFillable exit {} {}", this, _channel.getState(), BufferUtil.toDetailString(_requestBuffer)); } } - + /** * Fill and parse data looking for content * @@ -375,33 +374,38 @@ private boolean parseRequestBuffer() return handle; } - @Override - public void onCompleted() + private boolean upgrade() { - // Handle connection upgrades - if (_channel.getResponse().getStatus() == HttpStatus.SWITCHING_PROTOCOLS_101) + Connection connection = (Connection)_channel.getRequest().getAttribute(UPGRADE_CONNECTION_ATTRIBUTE); + if (connection == null) + return false; + + if (LOG.isDebugEnabled()) + LOG.debug("Upgrade from {} to {}", this, connection); + _channel.getState().upgrade(); + getEndPoint().upgrade(connection); + _channel.recycle(); + _parser.reset(); + _generator.reset(); + if (_contentBufferReferences.get() == 0) { - Connection connection = (Connection)_channel.getRequest().getAttribute(UPGRADE_CONNECTION_ATTRIBUTE); - if (connection != null) - { - if (LOG.isDebugEnabled()) - LOG.debug("Upgrade from {} to {}", this, connection); - _channel.getState().upgrade(); - getEndPoint().upgrade(connection); - _channel.recycle(); - _parser.reset(); - _generator.reset(); - if (_contentBufferReferences.get() == 0) - releaseRequestBuffer(); - else - { - LOG.warn("{} lingering content references?!?!", this); - _requestBuffer = null; // Not returned to pool! - _contentBufferReferences.set(0); - } - return; - } + releaseRequestBuffer(); + } + else + { + LOG.warn("{} lingering content references?!?!", this); + _requestBuffer = null; // Not returned to pool! + _contentBufferReferences.set(0); } + return true; + } + + @Override + public void onCompleted() + { + // Handle connection upgrades. + if (upgrade()) + return; // Finish consuming the request // If we are still expecting @@ -526,9 +530,9 @@ public void run() } @Override - public void send(MetaData.Response info, boolean head, ByteBuffer content, boolean lastContent, Callback callback) + public void send(MetaData.Request request, MetaData.Response response, ByteBuffer content, boolean lastContent, Callback callback) { - if (info == null) + if (response == null) { if (!lastContent && BufferUtil.isEmpty(content)) { @@ -544,7 +548,7 @@ public void send(MetaData.Response info, boolean head, ByteBuffer content, boole _generator.setPersistent(false); } - if (_sendCallback.reset(info, head, content, lastContent, callback)) + if (_sendCallback.reset(request, response, content, lastContent, callback)) { _sendCallback.iterate(); } @@ -616,11 +620,11 @@ public long getBytesOut() public String toConnectionString() { return String.format("%s@%x[p=%s,g=%s]=>%s", - getClass().getSimpleName(), - hashCode(), - _parser, - _generator, - _channel); + getClass().getSimpleName(), + hashCode(), + _parser, + _generator, + _channel); } private class Content extends HttpInput.Content @@ -706,21 +710,21 @@ public InvocationType getInvocationType() return _callback.getInvocationType(); } - private boolean reset(MetaData.Response info, boolean head, ByteBuffer content, boolean last, Callback callback) + private boolean reset(MetaData.Request request, MetaData.Response info, ByteBuffer content, boolean last, Callback callback) { if (reset()) { _info = info; - _head = head; + _head = HttpMethod.HEAD.is(request.getMethod()); _content = content; _lastContent = last; _callback = callback; _header = null; _shutdownOut = false; - + if (getConnector().isShutdown()) _generator.setPersistent(false); - + return true; } @@ -754,7 +758,7 @@ public Action process() throws Exception { case NEED_INFO: throw new EofException("request lifecycle violation"); - + case NEED_HEADER: { _header = _bufferPool.acquire(_config.getResponseHeaderSize(), _config.isUseDirectByteBuffers()); @@ -780,7 +784,7 @@ public Action process() throws Exception BufferUtil.clear(chunk); BufferUtil.clear(_content); } - + byte gatherWrite = 0; long bytes = 0; if (BufferUtil.hasContent(_header)) @@ -823,9 +827,9 @@ public Action process() throws Exception getEndPoint().write(this, _content); break; default: - succeeded(); + succeeded(); } - + return Action.SCHEDULED; } case SHUTDOWN_OUT: @@ -834,7 +838,7 @@ public Action process() throws Exception continue; } case DONE: - { + { // If shutdown after commit, we can still close here. if (getConnector().isShutdown()) _shutdownOut = true; diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpTransport.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpTransport.java index 2d6faeaa4476..ace983bd3117 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpTransport.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpTransport.java @@ -28,17 +28,19 @@ */ public interface HttpTransport { + String UPGRADE_CONNECTION_ATTRIBUTE = HttpTransport.class.getName() + ".UPGRADE"; + /** * Asynchronous call to send a response (or part) over the transport * - * @param info The header info to send, or null if just sending more data. - * The first call to send for a response must have a non null info. - * @param head True if the response if for a HEAD request (and the data should not be sent). + * @param request True if the response if for a HEAD request (and the data should not be sent). + * @param response The header info to send, or null if just sending more data. + * The first call to send for a response must have a non null info. * @param content A buffer of content to be sent. * @param lastContent True if the content is the last content for the current response. * @param callback The Callback instance that success or failure of the send is notified on */ - void send(MetaData.Response info, boolean head, ByteBuffer content, boolean lastContent, Callback callback); + void send(MetaData.Request request, MetaData.Response response, ByteBuffer content, boolean lastContent, Callback callback); /** * @return true if responses can be pushed over this transport @@ -55,7 +57,7 @@ public interface HttpTransport * some time after the last content is sent). */ void onCompleted(); - + /** * Aborts this transport. *

diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java index c45c312d861d..ee3193861e8a 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java @@ -139,13 +139,10 @@ public InetSocketAddress getLocalAddress() private Throwable _channelError; @Override - public void send(MetaData.Response info, boolean head, ByteBuffer content, boolean lastContent, Callback callback) + public void send(MetaData.Request request, MetaData.Response response, ByteBuffer content, boolean lastContent, Callback callback) { if (BufferUtil.hasContent(content)) - { BufferUtil.append(_content, content); - } - if (_channelError == null) callback.succeeded(); else diff --git a/jetty-unixsocket/jetty-unixsocket-client/src/main/java/org/eclipse/jetty/unixsocket/client/HttpClientTransportOverUnixSockets.java b/jetty-unixsocket/jetty-unixsocket-client/src/main/java/org/eclipse/jetty/unixsocket/client/HttpClientTransportOverUnixSockets.java index a203fc0132a7..fdd923e4a96f 100644 --- a/jetty-unixsocket/jetty-unixsocket-client/src/main/java/org/eclipse/jetty/unixsocket/client/HttpClientTransportOverUnixSockets.java +++ b/jetty-unixsocket/jetty-unixsocket-client/src/main/java/org/eclipse/jetty/unixsocket/client/HttpClientTransportOverUnixSockets.java @@ -38,6 +38,8 @@ import org.eclipse.jetty.client.DuplexHttpDestination; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpDestination; +import org.eclipse.jetty.client.HttpRequest; +import org.eclipse.jetty.client.Origin; import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.http.HttpConnectionOverHTTP; import org.eclipse.jetty.io.ClientConnector; @@ -71,6 +73,12 @@ private HttpClientTransportOverUnixSockets(ClientConnector connector) }); } + @Override + public HttpDestination.Key newDestinationKey(HttpRequest request, Origin origin) + { + return new HttpDestination.Key(origin, null); + } + @Override public HttpDestination newHttpDestination(HttpDestination.Key key) { diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/RFC6455Handshaker.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/RFC6455Handshaker.java index b5c7db8dbd7f..b7fd481cb946 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/RFC6455Handshaker.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/RFC6455Handshaker.java @@ -36,8 +36,8 @@ import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.HttpConfiguration; -import org.eclipse.jetty.server.HttpConnection; import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.HttpTransport; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.util.log.Log; @@ -233,7 +233,7 @@ public boolean upgradeRequest(WebSocketNegotiator negotiator, HttpServletRequest if (LOG.isDebugEnabled()) LOG.debug("upgrade connection={} session={}", connection, coreSession); - baseRequest.setAttribute(HttpConnection.UPGRADE_CONNECTION_ATTRIBUTE, connection); + baseRequest.setAttribute(HttpTransport.UPGRADE_CONNECTION_ATTRIBUTE, connection); return true; } diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AsyncIOServletTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AsyncIOServletTest.java index c868796d1ad1..626d2bd408b5 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AsyncIOServletTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AsyncIOServletTest.java @@ -39,7 +39,6 @@ import javax.servlet.AsyncContext; import javax.servlet.DispatcherType; import javax.servlet.ReadListener; -import javax.servlet.ServletException; import javax.servlet.ServletInputStream; import javax.servlet.ServletOutputStream; import javax.servlet.WriteListener; @@ -139,7 +138,7 @@ private void testAsyncReadThrows(Throwable throwable) throws Exception scenario.start(new HttpServlet() { @Override - protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException { scenario.assertScope(); AsyncContext asyncContext = request.startAsync(request, response); @@ -157,7 +156,7 @@ public void onDataAvailable() throws IOException } @Override - public void onAllDataRead() throws IOException + public void onAllDataRead() { scenario.assertScope(); } @@ -196,7 +195,7 @@ public void testAsyncReadIdleTimeout(Transport transport) throws Exception scenario.start(new HttpServlet() { @Override - protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException { scenario.assertScope(); AsyncContext asyncContext = request.startAsync(request, response); @@ -215,7 +214,7 @@ public void onDataAvailable() throws IOException } @Override - public void onAllDataRead() throws IOException + public void onAllDataRead() { scenario.assertScope(); } @@ -280,7 +279,7 @@ public void testOnErrorThrows(Transport transport) throws Exception scenario.start(new HttpServlet() { @Override - protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException { scenario.assertScope(); if (request.getDispatcherType() == DispatcherType.ERROR) @@ -293,14 +292,14 @@ protected void service(HttpServletRequest request, HttpServletResponse response) request.getInputStream().setReadListener(new ReadListener() { @Override - public void onDataAvailable() throws IOException + public void onDataAvailable() { scenario.assertScope(); throw new NullPointerException("explicitly_thrown_by_test_1"); } @Override - public void onAllDataRead() throws IOException + public void onAllDataRead() { scenario.assertScope(); } @@ -354,7 +353,7 @@ private void testAsyncWriteThrows(Throwable throwable) throws Exception scenario.start(new HttpServlet() { @Override - protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException { scenario.assertScope(); AsyncContext asyncContext = request.startAsync(request, response); @@ -411,7 +410,7 @@ public void testAsyncWriteClosed(Transport transport) throws Exception scenario.start(new HttpServlet() { @Override - protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException { scenario.assertScope(); response.flushBuffer(); @@ -471,7 +470,7 @@ public void testAsyncWriteLessThanContentLengthFlushed(Transport transport) thro scenario.start(new HttpServlet() { @Override - protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentLength(10); @@ -501,7 +500,7 @@ public void onWritePossible() throws IOException Thread.sleep(50); listener.onWritePossible(); } - catch (Exception e) + catch(Exception ignored) { } }).start(); @@ -542,22 +541,11 @@ public void onError(Throwable t) if (response.getStatus() == HttpStatus.OK_200) clientLatch.countDown(); }) - .onResponseContent(new Response.ContentListener() - { - @Override - public void onContent(Response response, ByteBuffer content) - { - // System.err.println("Content: "+BufferUtil.toDetailString(content)); - } - }) - .onResponseFailure(new Response.FailureListener() + .onResponseContent((response, content) -> { - @Override - public void onFailure(Response response, Throwable failure) - { - clientLatch.countDown(); - } + // System.err.println("Content: "+BufferUtil.toDetailString(content)); }) + .onResponseFailure((response, failure) -> clientLatch.countDown()) .send(result -> { failed.set(result.isFailed()); @@ -582,7 +570,7 @@ public void testIsReadyAtEOF(Transport transport) throws Exception scenario.start(new HttpServlet() { @Override - protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException { scenario.assertScope(); response.flushBuffer(); @@ -654,7 +642,7 @@ public void testOnAllDataRead(Transport transport) throws Exception scenario.start(new HttpServlet() { @Override - protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException { scenario.assertScope(); response.flushBuffer(); @@ -667,7 +655,7 @@ protected void service(HttpServletRequest request, HttpServletResponse response) in.setReadListener(new ReadListener() { @Override - public void onDataAvailable() throws IOException + public void onDataAvailable() { scenario.assertScope(); try @@ -753,7 +741,7 @@ public void testOtherThreadOnAllDataRead(Transport transport) throws Exception scenario.start(new HttpServlet() { @Override - protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException { scenario.assertScope(); response.flushBuffer(); @@ -769,7 +757,7 @@ protected void service(HttpServletRequest request, HttpServletResponse response) input.setReadListener(new ReadListener() { @Override - public void onDataAvailable() throws IOException + public void onDataAvailable() { scenario.assertScope(); async.start(() -> @@ -853,7 +841,7 @@ public void testCompleteBeforeOnAllDataRead(Transport transport) throws Exceptio scenario.start(new HttpServlet() { @Override - protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException { scenario.assertScope(); response.flushBuffer(); @@ -922,7 +910,7 @@ public void testEmptyAsyncRead(Transport transport) throws Exception scenario.start(new HttpServlet() { @Override - protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException { scenario.assertScope(); AsyncContext asyncContext = request.startAsync(request, response); @@ -931,14 +919,14 @@ protected void service(HttpServletRequest request, HttpServletResponse response) request.getInputStream().setReadListener(new ReadListener() { @Override - public void onDataAvailable() throws IOException + public void onDataAvailable() { scenario.assertScope(); oda.set(true); } @Override - public void onAllDataRead() throws IOException + public void onAllDataRead() { scenario.assertScope(); asyncContext.complete(); @@ -978,7 +966,7 @@ public void testWriteFromOnDataAvailable(Transport transport) throws Exception scenario.start(new HttpServlet() { @Override - protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException { AsyncContext asyncContext = request.startAsync(); request.getInputStream().setReadListener(new ReadListener() @@ -1005,7 +993,7 @@ public void onDataAvailable() throws IOException } @Override - public void onAllDataRead() throws IOException + public void onAllDataRead() { asyncContext.complete(); } @@ -1019,7 +1007,7 @@ public void onError(Throwable t) response.getOutputStream().setWriteListener(new WriteListener() { @Override - public void onWritePossible() throws IOException + public void onWritePossible() { writeLatch.countDown(); } @@ -1084,7 +1072,7 @@ public void testAsyncReadEarlyEOF(Transport transport) throws Exception scenario.start(new HttpServlet() { @Override - protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException { AsyncContext asyncContext = request.startAsync(); ServletInputStream input = request.getInputStream(); @@ -1120,7 +1108,7 @@ public void onError(Throwable x) CountDownLatch responseLatch = new CountDownLatch(1); DeferredContentProvider contentProvider = new DeferredContentProvider(); contentProvider.offer(ByteBuffer.wrap(content.getBytes(StandardCharsets.UTF_8))); - org.eclipse.jetty.client.api.Request request = scenario.client.newRequest(scenario.newURI()) + var request = scenario.client.newRequest(scenario.newURI()) .method(HttpMethod.POST) .path(scenario.servletPath) .content(contentProvider) @@ -1135,9 +1123,7 @@ public void onError(Throwable x) responseLatch.countDown(); }); - Destination destination = scenario.client.getDestination(scenario.getScheme(), - "localhost", - scenario.getNetworkConnectorLocalPortInt().get()); + Destination destination = scenario.client.resolveDestination(request); FuturePromise promise = new FuturePromise<>(); destination.newConnection(promise); org.eclipse.jetty.client.api.Connection connection = promise.get(5, TimeUnit.SECONDS); @@ -1194,7 +1180,7 @@ public void testAsyncIntercepted(Transport transport) throws Exception scenario.start(new HttpServlet() { @Override - protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException { System.err.println("Service " + request); @@ -1361,7 +1347,7 @@ public void testWriteListenerFromOtherThread(Transport transport) throws Excepti scenario.start(new HttpServlet() { @Override - protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException { AsyncContext asyncContext = request.startAsync(); asyncContext.setTimeout(0); @@ -1444,13 +1430,13 @@ public void onDataAvailable() throws IOException } @Override - public void onAllDataRead() throws IOException + public void onAllDataRead() { inputComplete.complete(null); } @Override - public void onWritePossible() throws IOException + public void onWritePossible() { // Dispatch OWP to another thread. executor.execute(() -> @@ -1535,7 +1521,7 @@ private void checkScope() } @Override - public void stopServer() throws Exception + public void stopServer() { checkScope(); scope.set(null); diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTimeoutTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTimeoutTest.java index a59dccd6fe3a..a91609337604 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTimeoutTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTimeoutTest.java @@ -189,14 +189,13 @@ public void testTimeoutOnListenerWithExplicitConnection(Transport transport) thr long timeout = 1000; scenario.start(new TimeoutHandler(2 * timeout)); - final CountDownLatch latch = new CountDownLatch(1); - Destination destination = scenario.client.getDestination(scenario.getScheme(), "localhost", scenario.getNetworkConnectorLocalPortInt().get()); + Request request = scenario.client.newRequest(scenario.newURI()).timeout(timeout, TimeUnit.MILLISECONDS); + CountDownLatch latch = new CountDownLatch(1); + Destination destination = scenario.client.resolveDestination(request); FuturePromise futureConnection = new FuturePromise<>(); destination.newConnection(futureConnection); try (Connection connection = futureConnection.get(5, TimeUnit.SECONDS)) { - Request request = scenario.client.newRequest(scenario.newURI()) - .timeout(timeout, TimeUnit.MILLISECONDS); connection.send(request, result -> { assertTrue(result.isFailed()); @@ -217,14 +216,13 @@ public void testTimeoutIsCancelledOnSuccessWithExplicitConnection(Transport tran long timeout = 1000; scenario.start(new TimeoutHandler(timeout)); - final CountDownLatch latch = new CountDownLatch(1); - Destination destination = scenario.client.getDestination(scenario.getScheme(), "localhost", scenario.getNetworkConnectorLocalPortInt().get()); + Request request = scenario.client.newRequest(scenario.newURI()).timeout(2 * timeout, TimeUnit.MILLISECONDS); + CountDownLatch latch = new CountDownLatch(1); + Destination destination = scenario.client.resolveDestination(request); FuturePromise futureConnection = new FuturePromise<>(); destination.newConnection(futureConnection); try (Connection connection = futureConnection.get(5, TimeUnit.SECONDS)) { - Request request = scenario.client.newRequest(scenario.newURI()) - .timeout(2 * timeout, TimeUnit.MILLISECONDS); connection.send(request, result -> { Response response = result.getResponse(); @@ -510,10 +508,9 @@ private void assumeConnectTimeout(String host, int port, int connectTimeout) // Abort the test if we can connect. fail("Error: Should not have been able to connect to " + host + ":" + port); } - catch (SocketTimeoutException x) + catch (SocketTimeoutException ignored) { // Expected timeout during connect, continue the test. - return; } catch (Throwable x) { diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/ProxyWithDynamicTransportTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/ProxyWithDynamicTransportTest.java new file mode 100644 index 000000000000..b47f8a02767e --- /dev/null +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/ProxyWithDynamicTransportTest.java @@ -0,0 +1,582 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http.client; + +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; +import org.eclipse.jetty.client.AbstractConnectionPool; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpDestination; +import org.eclipse.jetty.client.HttpProxy; +import org.eclipse.jetty.client.Origin; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Destination; +import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic; +import org.eclipse.jetty.client.http.HttpClientConnectionFactory; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpScheme; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http2.ErrorCode; +import org.eclipse.jetty.http2.HTTP2Cipher; +import org.eclipse.jetty.http2.HTTP2Connection; +import org.eclipse.jetty.http2.api.Session; +import org.eclipse.jetty.http2.api.Stream; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.client.http.ClientConnectionFactoryOverHTTP2; +import org.eclipse.jetty.http2.frames.DataFrame; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.http2.frames.ResetFrame; +import org.eclipse.jetty.http2.hpack.AuthorityHttpField; +import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.io.ClientConnectionFactory; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.proxy.AsyncProxyServlet; +import org.eclipse.jetty.proxy.ConnectHandler; +import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.FuturePromise; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ProxyWithDynamicTransportTest +{ + private static final Logger LOG = Log.getLogger(ProxyWithDynamicTransportTest.class); + + private Server server; + private ServerConnector serverConnector; + private ServerConnector serverTLSConnector; + private Server proxy; + private ServerConnector proxyConnector; + private ServerConnector proxyTLSConnector; + private HTTP2Client http2Client; + private HttpClient client; + + private void start(Handler handler) throws Exception + { + startServer(handler); + startProxy(new ConnectHandler()); + startClient(); + } + + private void startServer(Handler handler) throws Exception + { + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath("src/test/resources/keystore.jks"); + sslContextFactory.setKeyStorePassword("storepwd"); + sslContextFactory.setUseCipherSuitesOrder(true); + sslContextFactory.setCipherComparator(HTTP2Cipher.COMPARATOR); + + QueuedThreadPool serverThreads = new QueuedThreadPool(); + serverThreads.setName("server"); + server = new Server(serverThreads); + + HttpConfiguration httpConfig = new HttpConfiguration(); + HttpConnectionFactory h1c = new HttpConnectionFactory(httpConfig); + HTTP2CServerConnectionFactory h2c = new HTTP2CServerConnectionFactory(httpConfig); + serverConnector = new ServerConnector(server, 1, 1, h1c, h2c); + server.addConnector(serverConnector); + HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig); + httpsConfig.addCustomizer(new SecureRequestCustomizer()); + HttpConnectionFactory h1 = new HttpConnectionFactory(httpsConfig); + HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpsConfig); + ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); + alpn.setDefaultProtocol(h1.getProtocol()); + SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, alpn.getProtocol()); + serverTLSConnector = new ServerConnector(server, 1, 1, ssl, alpn, h2, h1, h2c); + server.addConnector(serverTLSConnector); + server.setHandler(handler); + server.start(); + LOG.info("Started server on :{} and :{}", serverConnector.getLocalPort(), serverTLSConnector.getLocalPort()); + } + + private void startProxy(ConnectHandler connectHandler) throws Exception + { + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath("src/test/resources/keystore.jks"); + sslContextFactory.setKeyStorePassword("storepwd"); + sslContextFactory.setUseCipherSuitesOrder(true); + sslContextFactory.setCipherComparator(HTTP2Cipher.COMPARATOR); + + QueuedThreadPool proxyThreads = new QueuedThreadPool(); + proxyThreads.setName("proxy"); + proxy = new Server(proxyThreads); + + HttpConfiguration httpConfig = new HttpConfiguration(); + ConnectionFactory h1c = new HttpConnectionFactory(httpConfig); + ConnectionFactory h2c = new HTTP2CServerConnectionFactory(httpConfig); + proxyConnector = new ServerConnector(proxy, 1, 1, h1c, h2c); + proxy.addConnector(proxyConnector); + HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig); + httpsConfig.addCustomizer(new SecureRequestCustomizer()); + HttpConnectionFactory h1 = new HttpConnectionFactory(httpsConfig); + HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpsConfig); + ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); + alpn.setDefaultProtocol(h1.getProtocol()); + SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, alpn.getProtocol()); + proxyTLSConnector = new ServerConnector(proxy, 1, 1, ssl, alpn, h2, h1, h2c); + proxy.addConnector(proxyTLSConnector); + + proxy.setHandler(connectHandler); + ServletContextHandler context = new ServletContextHandler(connectHandler, "/"); + ServletHolder holder = new ServletHolder(new AsyncProxyServlet() + { + @Override + protected HttpClient newHttpClient(ClientConnector clientConnector) + { + ClientConnectionFactory.Info h1 = HttpClientConnectionFactory.HTTP11; + HTTP2Client http2Client = new HTTP2Client(clientConnector); + ClientConnectionFactory.Info h2c = new ClientConnectionFactoryOverHTTP2.H2C(http2Client); + ClientConnectionFactory.Info h2 = new ClientConnectionFactoryOverHTTP2.H2(http2Client); + return new HttpClient(new HttpClientTransportDynamic(clientConnector, h1, h2c, h2)); + } + }); + context.addServlet(holder, "/*"); + proxy.start(); + LOG.info("Started proxy on :{} and :{}", proxyConnector.getLocalPort(), proxyTLSConnector.getLocalPort()); + } + + private void startClient() throws Exception + { + QueuedThreadPool clientThreads = new QueuedThreadPool(); + clientThreads.setName("client"); + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSelectors(1); + clientConnector.setExecutor(clientThreads); + clientConnector.setSslContextFactory(new SslContextFactory.Client(true)); + http2Client = new HTTP2Client(clientConnector); + ClientConnectionFactory.Info h1 = HttpClientConnectionFactory.HTTP11; + ClientConnectionFactory.Info h2c = new ClientConnectionFactoryOverHTTP2.H2C(http2Client); + ClientConnectionFactory.Info h2 = new ClientConnectionFactoryOverHTTP2.H2(http2Client); + client = new HttpClient(new HttpClientTransportDynamic(clientConnector, h1, h2c, h2)); + client.start(); + } + + @AfterEach + public void dispose() throws Exception + { + if (server != null) + server.stop(); + if (proxy != null) + proxy.stop(); + if (client != null) + client.stop(); + } + + private static java.util.stream.Stream testParams() + { + var h1 = List.of("http/1.1"); + var h2c = List.of("h2c"); + var h2 = List.of("h2"); + return java.util.stream.Stream.of( + // HTTP/1.1 Proxy with HTTP/1.1 Server. + Arguments.of(new HttpDestination.Protocol(h1, false), false, HttpVersion.HTTP_1_1, false), + Arguments.of(new HttpDestination.Protocol(h1, false), false, HttpVersion.HTTP_1_1, true), + Arguments.of(new HttpDestination.Protocol(h1, false), true, HttpVersion.HTTP_1_1, false), + Arguments.of(new HttpDestination.Protocol(h1, false), true, HttpVersion.HTTP_1_1, true), + // HTTP/1.1 Proxy with HTTP/2 Server. + Arguments.of(new HttpDestination.Protocol(h1, false), false, HttpVersion.HTTP_2, false), + Arguments.of(new HttpDestination.Protocol(h1, false), false, HttpVersion.HTTP_2, true), + Arguments.of(new HttpDestination.Protocol(h1, false), true, HttpVersion.HTTP_2, false), + Arguments.of(new HttpDestination.Protocol(h1, false), true, HttpVersion.HTTP_2, true), + // HTTP/2 Proxy with HTTP/1.1 Server. + Arguments.of(new HttpDestination.Protocol(h2c, false), false, HttpVersion.HTTP_1_1, false), + Arguments.of(new HttpDestination.Protocol(h2c, false), false, HttpVersion.HTTP_1_1, true), + Arguments.of(new HttpDestination.Protocol(h2, false), true, HttpVersion.HTTP_1_1, false), + Arguments.of(new HttpDestination.Protocol(h2, false), true, HttpVersion.HTTP_1_1, true), + Arguments.of(new HttpDestination.Protocol(h2, true), true, HttpVersion.HTTP_1_1, false), + Arguments.of(new HttpDestination.Protocol(h2, true), true, HttpVersion.HTTP_1_1, true), + // HTTP/2 Proxy with HTTP/2 Server. + Arguments.of(new HttpDestination.Protocol(h2c, false), false, HttpVersion.HTTP_2, false), + Arguments.of(new HttpDestination.Protocol(h2c, false), false, HttpVersion.HTTP_2, true), + Arguments.of(new HttpDestination.Protocol(h2, false), true, HttpVersion.HTTP_2, false), + Arguments.of(new HttpDestination.Protocol(h2, false), true, HttpVersion.HTTP_2, true), + Arguments.of(new HttpDestination.Protocol(h2, true), true, HttpVersion.HTTP_2, false), + Arguments.of(new HttpDestination.Protocol(h2, true), true, HttpVersion.HTTP_2, true) + ); + } + + @ParameterizedTest(name = "proxyProtocol={0}, proxySecure={1}, serverProtocol={2}, serverSecure={3}") + @MethodSource("testParams") + public void testProxy(HttpDestination.Protocol proxyProtocol, boolean proxySecure, HttpVersion serverProtocol, boolean serverSecure) throws Exception + { + int status = HttpStatus.NO_CONTENT_204; + start(new EmptyServerHandler() + { + @Override + protected void service(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) + { + response.setStatus(status); + } + }); + + int proxyPort = proxySecure ? proxyTLSConnector.getLocalPort() : proxyConnector.getLocalPort(); + Origin.Address proxyAddress = new Origin.Address("localhost", proxyPort); + HttpProxy proxy = new HttpProxy(proxyAddress, proxySecure, proxyProtocol); + client.getProxyConfiguration().getProxies().add(proxy); + + String scheme = serverSecure ? "https" : "http"; + int serverPort = serverSecure ? serverTLSConnector.getLocalPort() : serverConnector.getLocalPort(); + ContentResponse response1 = client.newRequest("localhost", serverPort) + .scheme(scheme) + .version(serverProtocol) + .timeout(5, TimeUnit.SECONDS) + .send(); + assertEquals(status, response1.getStatus()); + + // Make a second request to be sure it went through the same connection. + ContentResponse response2 = client.newRequest("localhost", serverPort) + .scheme(scheme) + .version(serverProtocol) + .timeout(5, TimeUnit.SECONDS) + .send(); + assertEquals(status, response2.getStatus()); + + List destinations = client.getDestinations().stream() + .filter(d -> d.getPort() == serverPort) + .collect(Collectors.toList()); + assertEquals(1, destinations.size()); + HttpDestination destination = (HttpDestination)destinations.get(0); + AbstractConnectionPool connectionPool = (AbstractConnectionPool)destination.getConnectionPool(); + assertEquals(1, connectionPool.getConnectionCount()); + } + + @Test + public void testHTTP2TunnelClosedByClient() throws Exception + { + start(new EmptyServerHandler()); + + int proxyPort = proxyConnector.getLocalPort(); + Origin.Address proxyAddress = new Origin.Address("localhost", proxyPort); + HttpProxy proxy = new HttpProxy(proxyAddress, false, new HttpDestination.Protocol(List.of("h2c"), false)); + client.getProxyConfiguration().getProxies().add(proxy); + + long idleTimeout = 1000; + http2Client.setStreamIdleTimeout(idleTimeout); + + String serverScheme = "http"; + int serverPort = serverConnector.getLocalPort(); + ContentResponse response = client.newRequest("localhost", serverPort) + .scheme(serverScheme) + .version(HttpVersion.HTTP_1_1) + .timeout(5, TimeUnit.SECONDS) + .send(); + assertEquals(HttpStatus.OK_200, response.getStatus()); + + // Client will close the HTTP2StreamEndPoint. + Thread.sleep(2 * idleTimeout); + + List destinations = client.getDestinations().stream() + .filter(d -> d.getPort() == serverPort) + .collect(Collectors.toList()); + assertEquals(1, destinations.size()); + HttpDestination destination = (HttpDestination)destinations.get(0); + AbstractConnectionPool connectionPool = (AbstractConnectionPool)destination.getConnectionPool(); + assertEquals(0, connectionPool.getConnectionCount()); + + List serverConnections = proxyConnector.getConnectedEndPoints().stream() + .map(EndPoint::getConnection) + .map(HTTP2Connection.class::cast) + .collect(Collectors.toList()); + assertEquals(1, serverConnections.size()); + assertTrue(serverConnections.get(0).getSession().getStreams().isEmpty()); + } + + @Test + public void testProxyDown() throws Exception + { + start(new EmptyServerHandler()); + + int proxyPort = proxyConnector.getLocalPort(); + Origin.Address proxyAddress = new Origin.Address("localhost", proxyPort); + HttpProxy httpProxy = new HttpProxy(proxyAddress, false, new HttpDestination.Protocol(List.of("h2c"), false)); + client.getProxyConfiguration().getProxies().add(httpProxy); + proxy.stop(); + + CountDownLatch latch = new CountDownLatch(1); + client.newRequest("localhost", serverConnector.getLocalPort()) + .version(HttpVersion.HTTP_1_1) + .timeout(5, TimeUnit.SECONDS) + .send(result -> + { + assertTrue(result.isFailed()); + assertThat(result.getFailure(), Matchers.instanceOf(ConnectException.class)); + latch.countDown(); + }); + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testHTTP2TunnelHardClosedByProxy() throws Exception + { + startServer(new EmptyServerHandler()); + CountDownLatch closeLatch = new CountDownLatch(1); + startProxy(new ConnectHandler() + { + @Override + protected void handleConnect(Request jettyRequest, HttpServletRequest request, HttpServletResponse response, String serverAddress) + { + jettyRequest.getHttpChannel().getEndPoint().close(); + closeLatch.countDown(); + } + }); + startClient(); + + int proxyPort = proxyConnector.getLocalPort(); + Origin.Address proxyAddress = new Origin.Address("localhost", proxyPort); + HttpProxy httpProxy = new HttpProxy(proxyAddress, false, new HttpDestination.Protocol(List.of("h2c"), false)); + client.getProxyConfiguration().getProxies().add(httpProxy); + + CountDownLatch latch = new CountDownLatch(1); + client.newRequest("localhost", serverConnector.getLocalPort()) + .version(HttpVersion.HTTP_1_1) + .timeout(5, TimeUnit.SECONDS) + .send(result -> + { + assertTrue(result.isFailed()); + assertThat(result.getFailure(), Matchers.instanceOf(ClosedChannelException.class)); + latch.countDown(); + }); + assertTrue(closeLatch.await(5, TimeUnit.SECONDS)); + assertTrue(latch.await(5, TimeUnit.SECONDS)); + + List destinations = client.getDestinations().stream() + .filter(d -> d.getPort() == proxyPort) + .collect(Collectors.toList()); + assertEquals(1, destinations.size()); + HttpDestination destination = (HttpDestination)destinations.get(0); + AbstractConnectionPool connectionPool = (AbstractConnectionPool)destination.getConnectionPool(); + assertEquals(0, connectionPool.getConnectionCount()); + } + + @Test + public void testHTTP2TunnelResetByClient() throws Exception + { + startServer(new EmptyServerHandler()); + CountDownLatch closeLatch = new CountDownLatch(2); + startProxy(new ConnectHandler() + { + @Override + protected DownstreamConnection newDownstreamConnection(EndPoint endPoint, ConcurrentMap context) + { + return new DownstreamConnection(endPoint, getExecutor(), getByteBufferPool(), context) + { + @Override + protected void close(Throwable failure) + { + super.close(failure); + closeLatch.countDown(); + } + }; + } + + @Override + protected UpstreamConnection newUpstreamConnection(EndPoint endPoint, ConnectContext connectContext) + { + return new UpstreamConnection(endPoint, getExecutor(), getByteBufferPool(), connectContext) + { + @Override + protected void close(Throwable failure) + { + super.close(failure); + closeLatch.countDown(); + } + }; + } + }); + startClient(); + + FuturePromise sessionPromise = new FuturePromise<>(); + http2Client.connect(new InetSocketAddress("localhost", proxyConnector.getLocalPort()), new Session.Listener.Adapter(), sessionPromise); + Session session = sessionPromise.get(5, TimeUnit.SECONDS); + String serverAddress = "localhost:" + serverConnector.getLocalPort(); + MetaData.ConnectRequest connect = new MetaData.ConnectRequest(HttpScheme.HTTP, new AuthorityHttpField(serverAddress), null, new HttpFields(), null); + HeadersFrame frame = new HeadersFrame(connect, null, false); + FuturePromise streamPromise = new FuturePromise<>(); + CountDownLatch tunnelLatch = new CountDownLatch(1); + CountDownLatch responseLatch = new CountDownLatch(1); + session.newStream(frame, streamPromise, new Stream.Listener.Adapter() + { + @Override + public void onHeaders(Stream stream, HeadersFrame frame) + { + MetaData.Response response = (MetaData.Response)frame.getMetaData(); + if (response.getStatus() == HttpStatus.OK_200) + tunnelLatch.countDown(); + } + + @Override + public void onData(Stream stream, DataFrame frame, Callback callback) + { + callback.succeeded(); + ByteBuffer data = frame.getData(); + String response = BufferUtil.toString(data, StandardCharsets.UTF_8); + if (response.startsWith("HTTP/1.1 200")) + responseLatch.countDown(); + } + }); + Stream stream = streamPromise.get(5, TimeUnit.SECONDS); + assertTrue(tunnelLatch.await(5, TimeUnit.SECONDS)); + + // Tunnel is established, send a HTTP/1.1 request. + String h1 = "GET / HTTP/1.1\r\n" + + "Host: " + serverAddress + "\r\n" + + "\r\n"; + stream.data(new DataFrame(stream.getId(), ByteBuffer.wrap(h1.getBytes(StandardCharsets.UTF_8)), false), Callback.NOOP); + assertTrue(responseLatch.await(5, TimeUnit.SECONDS)); + + // Now reset the stream, tunnel must be closed. + stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code), Callback.NOOP); + assertTrue(closeLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testHTTP2TunnelProxyStreamTimeout() throws Exception + { + startServer(new EmptyServerHandler()); + CountDownLatch closeLatch = new CountDownLatch(2); + startProxy(new ConnectHandler() + { + @Override + protected DownstreamConnection newDownstreamConnection(EndPoint endPoint, ConcurrentMap context) + { + return new DownstreamConnection(endPoint, getExecutor(), getByteBufferPool(), context) + { + @Override + protected void close(Throwable failure) + { + super.close(failure); + closeLatch.countDown(); + } + }; + } + + @Override + protected UpstreamConnection newUpstreamConnection(EndPoint endPoint, ConnectContext connectContext) + { + return new UpstreamConnection(endPoint, getExecutor(), getByteBufferPool(), connectContext) + { + @Override + protected void close(Throwable failure) + { + super.close(failure); + closeLatch.countDown(); + } + }; + } + }); + startClient(); + + long streamIdleTimeout = 1000; + ConnectionFactory h2c = proxyConnector.getConnectionFactory("h2c"); + ((HTTP2CServerConnectionFactory)h2c).setStreamIdleTimeout(streamIdleTimeout); + + FuturePromise sessionPromise = new FuturePromise<>(); + http2Client.connect(new InetSocketAddress("localhost", proxyConnector.getLocalPort()), new Session.Listener.Adapter(), sessionPromise); + Session session = sessionPromise.get(5, TimeUnit.SECONDS); + String serverAddress = "localhost:" + serverConnector.getLocalPort(); + MetaData.ConnectRequest connect = new MetaData.ConnectRequest(HttpScheme.HTTP, new AuthorityHttpField(serverAddress), null, new HttpFields(), null); + HeadersFrame frame = new HeadersFrame(connect, null, false); + FuturePromise streamPromise = new FuturePromise<>(); + CountDownLatch tunnelLatch = new CountDownLatch(1); + CountDownLatch responseLatch = new CountDownLatch(1); + CountDownLatch resetLatch = new CountDownLatch(1); + session.newStream(frame, streamPromise, new Stream.Listener.Adapter() + { + @Override + public void onHeaders(Stream stream, HeadersFrame frame) + { + MetaData.Response response = (MetaData.Response)frame.getMetaData(); + if (response.getStatus() == HttpStatus.OK_200) + tunnelLatch.countDown(); + } + + @Override + public void onData(Stream stream, DataFrame frame, Callback callback) + { + callback.succeeded(); + ByteBuffer data = frame.getData(); + String response = BufferUtil.toString(data, StandardCharsets.UTF_8); + if (response.startsWith("HTTP/1.1 200")) + responseLatch.countDown(); + } + + @Override + public void onReset(Stream stream, ResetFrame frame) + { + resetLatch.countDown(); + } + }); + Stream stream = streamPromise.get(5, TimeUnit.SECONDS); + assertTrue(tunnelLatch.await(5, TimeUnit.SECONDS)); + + // Tunnel is established, send a HTTP/1.1 request. + String h1 = "GET / HTTP/1.1\r\n" + + "Host: " + serverAddress + "\r\n" + + "\r\n"; + stream.data(new DataFrame(stream.getId(), ByteBuffer.wrap(h1.getBytes(StandardCharsets.UTF_8)), false), Callback.NOOP); + assertTrue(responseLatch.await(5, TimeUnit.SECONDS)); + + // Wait until the proxy stream idle times out. + Thread.sleep(2 * streamIdleTimeout); + + // Client should see a RST_STREAM. + assertTrue(resetLatch.await(5, TimeUnit.SECONDS)); + // Tunnel must be closed. + assertTrue(closeLatch.await(5, TimeUnit.SECONDS)); + } +} diff --git a/tests/test-http-client-transport/src/test/resources/jetty-logging.properties b/tests/test-http-client-transport/src/test/resources/jetty-logging.properties index 914cac87711b..9ba6265ef10f 100644 --- a/tests/test-http-client-transport/src/test/resources/jetty-logging.properties +++ b/tests/test-http-client-transport/src/test/resources/jetty-logging.properties @@ -1,6 +1,7 @@ org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog #org.eclipse.jetty.LEVEL=DEBUG #org.eclipse.jetty.client.LEVEL=DEBUG +#org.eclipse.jetty.proxy.LEVEL=DEBUG #org.eclipse.jetty.http2.LEVEL=DEBUG org.eclipse.jetty.http2.hpack.LEVEL=INFO #org.eclipse.jetty.http2.client.LEVEL=DEBUG diff --git a/tests/test-sessions/test-sessions-common/src/test/resources/jetty-logging.properties b/tests/test-sessions/test-sessions-common/src/test/resources/jetty-logging.properties new file mode 100644 index 000000000000..d96a696f82e6 --- /dev/null +++ b/tests/test-sessions/test-sessions-common/src/test/resources/jetty-logging.properties @@ -0,0 +1,2 @@ +org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog +#org.eclipse.jetty.LEVEL=DEBUG