From 64ca8c99a18a5839cac9cd3c6a4470b35e559aff Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Thu, 1 Apr 2021 18:08:33 +0200 Subject: [PATCH] Issue #6109 - Introduce carriers for HttpClient transports Introduced org.eclipse.jetty.io.Connectable. Retrofitted HttpClientTransport implementation to take a Connectable instead of ClientConnector. In future, a different Connectable implementation can be passed to HttpClientTransport implementation so that it may delegate to concrete ClientConnector implementation that use TCP, or Unix Domain sockets, or QUIC over UDP. Signed-off-by: Simone Bordet --- .../AbstractConnectorHttpClientTransport.java | 33 +-- .../org/eclipse/jetty/client/HttpClient.java | 4 +- .../dynamic/HttpClientTransportDynamic.java | 5 +- .../http/HttpClientTransportOverHTTP.java | 16 +- .../http/HttpClientTransportOverFCGI.java | 13 +- .../jetty/http2/client/HTTP2Client.java | 49 ++-- .../http/HttpClientTransportOverHTTP2.java | 13 +- .../org/eclipse/jetty/io/ClientConnector.java | 5 +- .../org/eclipse/jetty/io/Connectable.java | 40 +++ .../client/HttpClientConnectableTest.java | 242 ++++++++++++++++++ 10 files changed, 356 insertions(+), 64 deletions(-) create mode 100644 jetty-io/src/main/java/org/eclipse/jetty/io/Connectable.java create mode 100644 tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientConnectableTest.java 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 cdac10161a83..cae5632184ef 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 @@ -14,21 +14,22 @@ package org.eclipse.jetty.client; import java.net.InetSocketAddress; -import java.time.Duration; import java.util.Map; import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.Connectable; import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; +import org.eclipse.jetty.util.component.Container; @ManagedObject public abstract class AbstractConnectorHttpClientTransport extends AbstractHttpClientTransport { - private final ClientConnector connector; + private final Connectable connector; - protected AbstractConnectorHttpClientTransport(ClientConnector connector) + protected AbstractConnectorHttpClientTransport(Connectable connector) { this.connector = connector; addBean(connector); @@ -36,28 +37,20 @@ protected AbstractConnectorHttpClientTransport(ClientConnector connector) public ClientConnector getClientConnector() { - return connector; + ClientConnector result = null; + if (connector instanceof ClientConnector) + result = (ClientConnector)connector; + else if (connector instanceof Container) + result = ((Container)connector).getContainedBeans(ClientConnector.class).stream().findFirst().orElse(null); + if (result == null) + throw new IllegalArgumentException(ClientConnector.class.getName() + " not found in transport " + this); + return result; } @ManagedAttribute(value = "The number of selectors", readonly = true) public int getSelectors() { - return connector.getSelectors(); - } - - @Override - protected void doStart() throws Exception - { - HttpClient httpClient = getHttpClient(); - connector.setBindAddress(httpClient.getBindAddress()); - connector.setByteBufferPool(httpClient.getByteBufferPool()); - connector.setConnectBlocking(httpClient.isConnectBlocking()); - connector.setConnectTimeout(Duration.ofMillis(httpClient.getConnectTimeout())); - connector.setExecutor(httpClient.getExecutor()); - connector.setIdleTimeout(Duration.ofMillis(httpClient.getIdleTimeout())); - connector.setScheduler(httpClient.getScheduler()); - connector.setSslContextFactory(httpClient.getSslContextFactory()); - super.doStart(); + return getClientConnector().getSelectors(); } @Override 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 7e672fd42b55..5dfd23087b3d 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 @@ -159,7 +159,9 @@ public HttpClient(HttpClientTransport transport) { this.transport = Objects.requireNonNull(transport); addBean(transport); - this.connector = ((AbstractHttpClientTransport)transport).getBean(ClientConnector.class); + this.connector = ((AbstractHttpClientTransport)transport).getContainedBeans(ClientConnector.class).stream() + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(ClientConnector.class.getName() + " not found in transport " + transport)); addBean(handlers); addBean(decoderFactories); } 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 15b200cc04aa..708738882a05 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 @@ -38,6 +38,7 @@ import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.Connectable; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; @@ -95,7 +96,7 @@ public HttpClientTransportDynamic() * @param connector the ClientConnector used by this transport * @param factoryInfos the application protocols that this transport can speak */ - public HttpClientTransportDynamic(ClientConnector connector, ClientConnectionFactory.Info... factoryInfos) + public HttpClientTransportDynamic(Connectable connector, ClientConnectionFactory.Info... factoryInfos) { super(connector); addBean(connector); @@ -179,7 +180,7 @@ public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map new DuplexConnectionPool(destination, getHttpClient().getMaxConnectionsPerDestination(), destination)); } + private static Connectable newClientConnector(int selectors) + { + ClientConnector connector = new ClientConnector(); + connector.setSelectors(selectors); + return connector; + } + @Override public Origin newOrigin(HttpRequest request) { 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 a33fa967e10d..37438e88f4c7 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 @@ -29,6 +29,7 @@ import org.eclipse.jetty.fcgi.FCGI; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.Connectable; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.util.ProcessorUtils; import org.eclipse.jetty.util.Promise; @@ -47,11 +48,10 @@ public HttpClientTransportOverFCGI(String scriptRoot) public HttpClientTransportOverFCGI(int selectors, String scriptRoot) { - this(new ClientConnector(), scriptRoot); - getClientConnector().setSelectors(selectors); + this(newClientConnector(selectors), scriptRoot); } - public HttpClientTransportOverFCGI(ClientConnector connector, String scriptRoot) + public HttpClientTransportOverFCGI(Connectable connector, String scriptRoot) { super(connector); this.scriptRoot = scriptRoot; @@ -63,6 +63,13 @@ public HttpClientTransportOverFCGI(ClientConnector connector, String scriptRoot) }); } + private static ClientConnector newClientConnector(int selectors) + { + ClientConnector connector = new ClientConnector(); + connector.setSelectors(selectors); + return connector; + } + @ManagedAttribute(value = "The scripts root directory", readonly = true) public String getScriptRoot() { 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 2319d9634bad..6286046caab7 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 @@ -31,10 +31,12 @@ import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.Connectable; import org.eclipse.jetty.io.ssl.SslClientConnectionFactory; import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; +import org.eclipse.jetty.util.component.Container; import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.Scheduler; @@ -101,7 +103,7 @@ @ManagedObject public class HTTP2Client extends ContainerLifeCycle { - private final ClientConnector connector; + private final Connectable connector; private int inputBufferSize = 8192; private List protocols = List.of("h2"); private int initialSessionRecvWindow = 16 * 1024 * 1024; @@ -121,7 +123,7 @@ public HTTP2Client() this(new ClientConnector()); } - public HTTP2Client(ClientConnector connector) + public HTTP2Client(Connectable connector) { this.connector = connector; addBean(connector); @@ -129,37 +131,44 @@ public HTTP2Client(ClientConnector connector) public ClientConnector getClientConnector() { - return connector; + ClientConnector result = null; + if (connector instanceof ClientConnector) + result = (ClientConnector)connector; + else if (connector instanceof Container) + result = ((Container)connector).getContainedBeans(ClientConnector.class).stream().findAny().orElse(null); + if (result == null) + throw new IllegalArgumentException(ClientConnector.class.getName() + " not found in " + this); + return result; } public Executor getExecutor() { - return connector.getExecutor(); + return getClientConnector().getExecutor(); } public void setExecutor(Executor executor) { - connector.setExecutor(executor); + getClientConnector().setExecutor(executor); } public Scheduler getScheduler() { - return connector.getScheduler(); + return getClientConnector().getScheduler(); } public void setScheduler(Scheduler scheduler) { - connector.setScheduler(scheduler); + getClientConnector().setScheduler(scheduler); } public ByteBufferPool getByteBufferPool() { - return connector.getByteBufferPool(); + return getClientConnector().getByteBufferPool(); } public void setByteBufferPool(ByteBufferPool bufferPool) { - connector.setByteBufferPool(bufferPool); + getClientConnector().setByteBufferPool(bufferPool); } public FlowControlStrategy.Factory getFlowControlStrategyFactory() @@ -175,23 +184,23 @@ public void setFlowControlStrategyFactory(FlowControlStrategy.Factory flowContro @ManagedAttribute("The number of selectors") public int getSelectors() { - return connector.getSelectors(); + return getClientConnector().getSelectors(); } public void setSelectors(int selectors) { - connector.setSelectors(selectors); + getClientConnector().setSelectors(selectors); } @ManagedAttribute("The idle timeout in milliseconds") public long getIdleTimeout() { - return connector.getIdleTimeout().toMillis(); + return getClientConnector().getIdleTimeout().toMillis(); } public void setIdleTimeout(long idleTimeout) { - connector.setIdleTimeout(Duration.ofMillis(idleTimeout)); + getClientConnector().setIdleTimeout(Duration.ofMillis(idleTimeout)); } @ManagedAttribute("The stream idle timeout in milliseconds") @@ -208,33 +217,33 @@ public void setStreamIdleTimeout(long streamIdleTimeout) @ManagedAttribute("The connect timeout in milliseconds") public long getConnectTimeout() { - return connector.getConnectTimeout().toMillis(); + return getClientConnector().getConnectTimeout().toMillis(); } public void setConnectTimeout(long connectTimeout) { - connector.setConnectTimeout(Duration.ofMillis(connectTimeout)); + getClientConnector().setConnectTimeout(Duration.ofMillis(connectTimeout)); } @ManagedAttribute("Whether the connect() operation is blocking") public boolean isConnectBlocking() { - return connector.isConnectBlocking(); + return getClientConnector().isConnectBlocking(); } public void setConnectBlocking(boolean connectBlocking) { - connector.setConnectBlocking(connectBlocking); + getClientConnector().setConnectBlocking(connectBlocking); } public SocketAddress getBindAddress() { - return connector.getBindAddress(); + return getClientConnector().getBindAddress(); } public void setBindAddress(SocketAddress bindAddress) { - connector.setBindAddress(bindAddress); + getClientConnector().setBindAddress(bindAddress); } @ManagedAttribute("The size of the buffer used to read from the network") @@ -404,7 +413,7 @@ public void accept(SocketChannel channel, ClientConnectionFactory factory, Sessi { Map context = contextFrom(factory, listener, promise, null); context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, Promise.from(ioConnection -> {}, promise::failed)); - connector.accept(channel, context); + getClientConnector().accept(channel, context); } private Map contextFrom(ClientConnectionFactory factory, Session.Listener listener, Promise promise, Map context) 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 fc7a28ea7044..0710e2839ba1 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 @@ -17,6 +17,7 @@ import java.net.InetSocketAddress; import java.util.List; import java.util.Map; +import java.util.Objects; import org.eclipse.jetty.alpn.client.ALPNClientConnectionFactory; import org.eclipse.jetty.client.AbstractHttpClientTransport; @@ -47,8 +48,8 @@ public class HttpClientTransportOverHTTP2 extends AbstractHttpClientTransport public HttpClientTransportOverHTTP2(HTTP2Client client) { - this.client = client; - addBean(client.getClientConnector(), false); + this.client = Objects.requireNonNull(client); + addBean(client); setConnectionPoolFactory(destination -> { HttpClient httpClient = getHttpClient(); @@ -93,17 +94,9 @@ protected void doStart() throws Exception client.setUseInputDirectByteBuffers(httpClient.isUseInputDirectByteBuffers()); client.setUseOutputDirectByteBuffers(httpClient.isUseOutputDirectByteBuffers()); } - addBean(client); super.doStart(); } - @Override - protected void doStop() throws Exception - { - super.doStop(); - removeBean(client); - } - @Override public Origin newOrigin(HttpRequest request) { diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java index f2d5ebb4ce8b..63fdc8f160fc 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java @@ -35,12 +35,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class ClientConnector extends ContainerLifeCycle +public class ClientConnector extends ContainerLifeCycle implements Connectable { - public static final String CLIENT_CONNECTOR_CONTEXT_KEY = "org.eclipse.jetty.client.connector"; public static final String REMOTE_SOCKET_ADDRESS_CONTEXT_KEY = CLIENT_CONNECTOR_CONTEXT_KEY + ".remoteSocketAddress"; public static final String CLIENT_CONNECTION_FACTORY_CONTEXT_KEY = CLIENT_CONNECTOR_CONTEXT_KEY + ".clientConnectionFactory"; - public static final String CONNECTION_PROMISE_CONTEXT_KEY = CLIENT_CONNECTOR_CONTEXT_KEY + ".connectionPromise"; private static final Logger LOG = LoggerFactory.getLogger(ClientConnector.class); private Executor executor; @@ -211,6 +209,7 @@ protected SelectorManager newSelectorManager() return new ClientSelectorManager(getExecutor(), getScheduler(), getSelectors()); } + @Override public void connect(SocketAddress address, Map context) { SocketChannel channel = null; diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/Connectable.java b/jetty-io/src/main/java/org/eclipse/jetty/io/Connectable.java new file mode 100644 index 000000000000..0ec2d9ece60f --- /dev/null +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/Connectable.java @@ -0,0 +1,40 @@ +// +// ======================================================================== +// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.io; + +import java.net.SocketAddress; +import java.util.Map; + +/** + *

The abstraction that client components implement to + * provide a service that connects to remote hosts.

+ */ +public interface Connectable +{ + public static final String CLIENT_CONNECTOR_CONTEXT_KEY = "org.eclipse.jetty.client.connector"; + public static final String CONNECTION_PROMISE_CONTEXT_KEY = CLIENT_CONNECTOR_CONTEXT_KEY + ".connectionPromise"; + + /** + *

Connects to a remote hosts using the information provided + * by the given {@code address} and {@code context} map.

+ *

The connection may not be established to the given socket address.

+ *

Implementations must arrange to notify the {@code Promise}, + * present in the {@code context} map under the {@link #CONNECTION_PROMISE_CONTEXT_KEY} key, + * both in case of successful connection or in case of connection failure.

+ * + * @param address the socket address + * @param context the context map + */ + public void connect(SocketAddress address, Map context); +} diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientConnectableTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientConnectableTest.java new file mode 100644 index 000000000000..7d4e0016d45f --- /dev/null +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientConnectableTest.java @@ -0,0 +1,242 @@ +// +// ======================================================================== +// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.http.client; + +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.function.BiPredicate; + +import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpClientTransport; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; +import org.eclipse.jetty.fcgi.client.http.HttpClientTransportOverFCGI; +import org.eclipse.jetty.fcgi.server.ServerFCGIConnectionFactory; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2; +import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.Connectable; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +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.util.Promise; +import org.eclipse.jetty.util.component.ContainerLifeCycle; +import org.eclipse.jetty.util.component.LifeCycle; +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.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.opentest4j.TestAbortedException; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class HttpClientConnectableTest +{ + private Server server; + private ServerConnector connector; + private HttpClient client; + + private void startServer(Transport transport, Handler handler) throws Exception + { + QueuedThreadPool serverThreads = new QueuedThreadPool(); + serverThreads.setName("server"); + server = new Server(serverThreads); + + switch (transport) + { + case HTTP: + { + connector = new ServerConnector(server, new HttpConnectionFactory()); + break; + } + case HTTPS: + { + HttpConfiguration httpsConfig = new HttpConfiguration(); + httpsConfig.addCustomizer(new SecureRequestCustomizer()); + HttpConnectionFactory https = new HttpConnectionFactory(httpsConfig); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath("src/test/resources/keystore.p12"); + sslContextFactory.setKeyStorePassword("storepwd"); + SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, https.getProtocol()); + connector = new ServerConnector(server, ssl, https); + break; + } + case FCGI: + { + connector = new ServerConnector(server, new ServerFCGIConnectionFactory(new HttpConfiguration())); + break; + } + case H2C: + { + connector = new ServerConnector(server, new HTTP2CServerConnectionFactory(new HttpConfiguration())); + break; + } + case H2: + { + HttpConfiguration httpsConfig = new HttpConfiguration(); + httpsConfig.addCustomizer(new SecureRequestCustomizer()); + HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpsConfig); + ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath("src/test/resources/keystore.p12"); + sslContextFactory.setKeyStorePassword("storepwd"); + SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, alpn.getProtocol()); + connector = new ServerConnector(server, ssl, alpn, h2); + break; + } + default: + { + throw new TestAbortedException("Unsupported transport " + transport); + } + } + + server.addConnector(connector); + server.setHandler(handler); + server.start(); + } + + private void startClient(Transport transport, MatchingConnectable connectable) throws Exception + { + ClientConnector connector = new ClientConnector(); + connectable.addBean(connector); + + QueuedThreadPool clientThreads = new QueuedThreadPool(); + clientThreads.setName("client"); + connector.setExecutor(clientThreads); + + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + sslContextFactory.setTrustStorePath("src/test/resources/keystore.p12"); + sslContextFactory.setTrustStorePassword("storepwd"); + connector.setSslContextFactory(sslContextFactory); + + HttpClientTransport clientTransport; + switch (transport) + { + case HTTP: + case HTTPS: + { + clientTransport = new HttpClientTransportOverHTTP(connectable); + break; + } + case H2C: + case H2: + { + clientTransport = new HttpClientTransportOverHTTP2(new HTTP2Client(connectable)); + break; + } + case FCGI: + { + clientTransport = new HttpClientTransportOverFCGI(connectable, ""); + break; + } + default: + { + throw new TestAbortedException("Unsupported transport " + transport); + } + } + + client = new HttpClient(clientTransport); + client.start(); + } + + @AfterEach + public void dispose() + { + LifeCycle.stop(client); + LifeCycle.stop(server); + } + + @ParameterizedTest + @EnumSource(Transport.class) + public void testMatchingConnectable(Transport transport) throws Exception + { + startServer(transport, new EmptyServerHandler()); + + MatchingConnectable matcher = new MatchingConnectable(); + startClient(transport, matcher); + ClientConnector clientConnector = matcher.getBean(ClientConnector.class); + assertNotNull(clientConnector); + + String scheme = transport.isTlsBased() ? "https" : "http"; + String host = "localhost"; + int port = connector.getLocalPort(); + URI uri = URI.create(scheme + "://" + host + ":" + port + "/path"); + + // No matching rules added, request should fail. + ExecutionException exception = assertThrows(ExecutionException.class, () -> client.GET(uri)); + assertThat(exception.getCause(), Matchers.instanceOf(ConnectException.class)); + + // Add a matching rule, request should succeed. + matcher.rules.add((a, c) -> + { + clientConnector.connect(a, c); + return true; + }); + ContentResponse response = client.GET(uri); + assertEquals(HttpStatus.OK_200, response.getStatus()); + + matcher.rules.clear(); + + URI otherURI = URI.create(scheme + "://" + host + (port + 1) + "/path"); + // Add a rule that connects to another address. + matcher.rules.add((a, c) -> + { + if (a instanceof InetSocketAddress) + { + assertEquals(otherURI.getPort(), ((InetSocketAddress)a).getPort()); + clientConnector.connect(new InetSocketAddress(uri.getHost(), uri.getPort()), c); + return true; + } + return false; + }); + response = client.GET(uri); + assertEquals(HttpStatus.OK_200, response.getStatus()); + } + + private static class MatchingConnectable extends ContainerLifeCycle implements Connectable + { + private final List>> rules = new ArrayList<>(); + + @Override + public void connect(SocketAddress address, Map context) + { + if (rules.stream().noneMatch(rule -> rule.test(address, context))) + { + @SuppressWarnings("unchecked") + Promise promise = (Promise)context.get(Connectable.CONNECTION_PROMISE_CONTEXT_KEY); + promise.failed(new ConnectException()); + } + } + } +}