diff --git a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/Apache5HttpClient.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/Apache5HttpClient.java index d65c90db4be0..a8d9e506302b 100644 --- a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/Apache5HttpClient.java +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/Apache5HttpClient.java @@ -58,7 +58,7 @@ import org.apache.hc.client5.http.routing.RoutingSupport; import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier; import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; -import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; +import org.apache.hc.client5.http.ssl.TlsSocketStrategy; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.Header; @@ -467,12 +467,15 @@ public interface Builder extends SdkHttpClient.Builder - * When set to a non-null value, the use of a custom factory implies the configuration options TRUST_ALL_CERTIFICATES, - * TLS_TRUST_MANAGERS_PROVIDER, and TLS_KEY_MANAGERS_PROVIDER are ignored. + * Configure a custom TLS strategy for SSL/TLS connections. + * This is the preferred method over the ConnectionSocketFactory. + * + * @param tlsSocketStrategy The TLS strategy to use for upgrading connections to TLS. + * If null, default TLS configuration will be used. + * @return This builder for method chaining + */ - Builder socketFactory(SSLConnectionSocketFactory socketFactory); + Builder tlsSocketStrategy(TlsSocketStrategy tlsSocketStrategy); /** * Configuration that defines an HTTP route planner that computes the route an HTTP request should take. @@ -530,7 +533,7 @@ private static final class DefaultBuilder implements Builder { private HttpRoutePlanner httpRoutePlanner; private CredentialsProvider credentialsProvider; private DnsResolver dnsResolver; - private SSLConnectionSocketFactory socketFactory; + private TlsSocketStrategy tlsStrategy; private DefaultBuilder() { } @@ -652,15 +655,11 @@ public void setDnsResolver(DnsResolver dnsResolver) { } @Override - public Builder socketFactory(SSLConnectionSocketFactory socketFactory) { - this.socketFactory = socketFactory; + public Builder tlsSocketStrategy(TlsSocketStrategy tlsSocketStrategy) { + this.tlsStrategy = tlsSocketStrategy; return this; } - public void setSocketFactory(SSLConnectionSocketFactory socketFactory) { - socketFactory(socketFactory); - } - @Override public Builder httpRoutePlanner(HttpRoutePlanner httpRoutePlanner) { this.httpRoutePlanner = httpRoutePlanner; @@ -734,13 +733,13 @@ public SdkHttpClient buildWithDefaults(AttributeMap serviceDefaults) { private static class ApacheConnectionManagerFactory { public PoolingHttpClientConnectionManager create(Apache5HttpClient.DefaultBuilder configuration, - AttributeMap standardOptions) { - // TODO : Deprecated method needs to be removed with new replacements - SSLConnectionSocketFactory sslsf = getPreferredSocketFactory(configuration, standardOptions); + AttributeMap standardOptions) { + + TlsSocketStrategy tlsStrategy = getPreferredTlsStrategy(configuration, standardOptions); PoolingHttpClientConnectionManagerBuilder builder = PoolingHttpClientConnectionManagerBuilder.create() - .setSSLSocketFactory(sslsf) + .setTlsSocketStrategy(tlsStrategy) .setSchemePortResolver(DefaultSchemePortResolver.INSTANCE) .setDnsResolver(configuration.dnsResolver); builder.setMaxConnPerRoute(standardOptions.get(SdkHttpConfigurationOption.MAX_CONNECTIONS)); @@ -765,11 +764,13 @@ private static ConnectionConfig getConnectionConfig(AttributeMap standardOptions return connectionConfigBuilder.build(); } - private SSLConnectionSocketFactory getPreferredSocketFactory(Apache5HttpClient.DefaultBuilder configuration, - AttributeMap standardOptions) { - return Optional.ofNullable(configuration.socketFactory) - .orElseGet(() -> new SdkTlsSocketFactory(getSslContext(standardOptions), - getHostNameVerifier(standardOptions))); + private TlsSocketStrategy getPreferredTlsStrategy(Apache5HttpClient.DefaultBuilder configuration, + AttributeMap standardOptions) { + if (configuration.tlsStrategy != null) { + return configuration.tlsStrategy; + } + return new SdkTlsSocketFactory(getSslContext(standardOptions), + getHostNameVerifier(standardOptions)); } diff --git a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/ClientConnectionManagerFactory.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/ClientConnectionManagerFactory.java index d3e30aec3532..208c47712643 100644 --- a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/ClientConnectionManagerFactory.java +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/ClientConnectionManagerFactory.java @@ -49,7 +49,7 @@ public static HttpClientConnectionManager wrap(HttpClientConnectionManager orig) /** * Further wraps {@link LeaseRequest} to capture performance metrics. */ - private static class InstrumentedHttpClientConnectionManager extends DelegatingHttpClientConnectionManager { + private static final class InstrumentedHttpClientConnectionManager extends DelegatingHttpClientConnectionManager { private InstrumentedHttpClientConnectionManager(HttpClientConnectionManager delegate) { super(delegate); diff --git a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/ClientConnectionRequestFactory.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/ClientConnectionRequestFactory.java index b107b7c59df7..513a8da42d0b 100644 --- a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/ClientConnectionRequestFactory.java +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/ClientConnectionRequestFactory.java @@ -55,7 +55,7 @@ static LeaseRequest wrap(LeaseRequest orig) { /** * Measures the latency of {@link LeaseRequest#get(Timeout)}. */ - private static class InstrumentedConnectionRequest extends DelegatingConnectionRequest { + private static final class InstrumentedConnectionRequest extends DelegatingConnectionRequest { private InstrumentedConnectionRequest(LeaseRequest delegate) { super(delegate); diff --git a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/SdkTlsSocketFactory.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/SdkTlsSocketFactory.java index 8f2a0ef44406..62359fc9b851 100644 --- a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/SdkTlsSocketFactory.java +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/SdkTlsSocketFactory.java @@ -16,22 +16,19 @@ package software.amazon.awssdk.http.apache5.internal.conn; import java.io.IOException; -import java.net.InetSocketAddress; import java.net.Socket; import java.util.Arrays; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; -import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; -import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; import org.apache.hc.core5.http.protocol.HttpContext; -import org.apache.hc.core5.util.TimeValue; import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.http.apache5.internal.net.SdkSocket; +import software.amazon.awssdk.http.apache5.internal.net.SdkSslSocket; import software.amazon.awssdk.utils.Logger; @SdkInternalApi -public class SdkTlsSocketFactory extends SSLConnectionSocketFactory { +public class SdkTlsSocketFactory extends DefaultClientTlsStrategy { private static final Logger log = Logger.loggerFor(SdkTlsSocketFactory.class); @@ -39,27 +36,30 @@ public SdkTlsSocketFactory(SSLContext sslContext, HostnameVerifier hostnameVerif super(sslContext, hostnameVerifier); if (sslContext == null) { throw new IllegalArgumentException( - "sslContext must not be null. " + "Use SSLContext.getDefault() if you are unsure."); + "sslContext must not be null. Use SSLContext.getDefault() if you are unsure."); } } @Override - protected final void prepareSocket(SSLSocket socket) { + protected void initializeSocket(SSLSocket socket) { + super.initializeSocket(socket); log.debug(() -> String.format("socket.getSupportedProtocols(): %s, socket.getEnabledProtocols(): %s", Arrays.toString(socket.getSupportedProtocols()), Arrays.toString(socket.getEnabledProtocols()))); } @Override - public Socket connectSocket(TimeValue connectTimeout, - Socket socket, - HttpHost host, - InetSocketAddress remoteAddress, - InetSocketAddress localAddress, - HttpContext context) throws IOException { - log.trace(() -> String.format("Connecting to %s:%s", remoteAddress.getAddress(), remoteAddress.getPort())); + public SSLSocket upgrade(Socket socket, + String target, + int port, + Object attachment, + HttpContext context) throws IOException { + log.trace(() -> String.format("Upgrading socket to TLS for %s:%s", target, port)); - Socket connectSocket = super.connectSocket(connectTimeout, socket, host, remoteAddress, localAddress, context); - return new SdkSocket(connectSocket); + SSLSocket upgradedSocket = super.upgrade(socket, target, port, attachment, context); + + // Wrap the upgraded SSLSocket in SdkSSLSocket for logging + return new SdkSslSocket(upgradedSocket); } + } diff --git a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/net/DelegateSocket.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/net/DelegateSocket.java deleted file mode 100644 index cc203f059630..000000000000 --- a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/net/DelegateSocket.java +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.http.apache5.internal.net; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetAddress; -import java.net.Socket; -import java.net.SocketAddress; -import java.net.SocketException; -import java.nio.channels.SocketChannel; -import software.amazon.awssdk.annotations.SdkInternalApi; - -/** - * Socket delegate class. Subclasses could extend this class, so that - * they only need to override methods they are interested in enhancing. - */ -@SdkInternalApi -public class DelegateSocket extends Socket { - - protected final Socket sock; - - public DelegateSocket(Socket sock) { - this.sock = sock; - } - - @Override - public void connect(SocketAddress endpoint) throws IOException { - sock.connect(endpoint); - } - - @Override - public void connect(SocketAddress endpoint, int timeout) throws IOException { - sock.connect(endpoint, timeout); - } - - @Override - public void bind(SocketAddress bindpoint) throws IOException { - sock.bind(bindpoint); - } - - @Override - public InetAddress getInetAddress() { - return sock.getInetAddress(); - } - - @Override - public InetAddress getLocalAddress() { - return sock.getLocalAddress(); - } - - @Override - public int getPort() { - return sock.getPort(); - } - - @Override - public int getLocalPort() { - return sock.getLocalPort(); - } - - @Override - public SocketAddress getRemoteSocketAddress() { - return sock.getRemoteSocketAddress(); - } - - @Override - public SocketAddress getLocalSocketAddress() { - return sock.getLocalSocketAddress(); - } - - @Override - public SocketChannel getChannel() { - return sock.getChannel(); - } - - @Override - public InputStream getInputStream() throws IOException { - return sock.getInputStream(); - } - - @Override - public OutputStream getOutputStream() throws IOException { - return sock.getOutputStream(); - } - - @Override - public void setTcpNoDelay(boolean on) throws SocketException { - sock.setTcpNoDelay(on); - } - - @Override - public boolean getTcpNoDelay() throws SocketException { - return sock.getTcpNoDelay(); - } - - @Override - public void setSoLinger(boolean on, int linger) throws SocketException { - sock.setSoLinger(on, linger); - } - - @Override - public int getSoLinger() throws SocketException { - return sock.getSoLinger(); - } - - @Override - public void sendUrgentData(int data) throws IOException { - sock.sendUrgentData(data); - } - - @Override - public void setOOBInline(boolean on) throws SocketException { - sock.setOOBInline(on); - } - - @Override - public boolean getOOBInline() throws SocketException { - return sock.getOOBInline(); - } - - @Override - public void setSoTimeout(int timeout) throws SocketException { - sock.setSoTimeout(timeout); - } - - @Override - public int getSoTimeout() throws SocketException { - return sock.getSoTimeout(); - } - - @Override - public void setSendBufferSize(int size) throws SocketException { - sock.setSendBufferSize(size); - } - - @Override - public int getSendBufferSize() throws SocketException { - return sock.getSendBufferSize(); - } - - @Override - public void setReceiveBufferSize(int size) throws SocketException { - sock.setReceiveBufferSize(size); - } - - @Override - public int getReceiveBufferSize() throws SocketException { - return sock.getReceiveBufferSize(); - } - - @Override - public void setKeepAlive(boolean on) throws SocketException { - sock.setKeepAlive(on); - } - - @Override - public boolean getKeepAlive() throws SocketException { - return sock.getKeepAlive(); - } - - @Override - public void setTrafficClass(int tc) throws SocketException { - sock.setTrafficClass(tc); - } - - @Override - public int getTrafficClass() throws SocketException { - return sock.getTrafficClass(); - } - - @Override - public void setReuseAddress(boolean on) throws SocketException { - sock.setReuseAddress(on); - } - - @Override - public boolean getReuseAddress() throws SocketException { - return sock.getReuseAddress(); - } - - @Override - public void close() throws IOException { - sock.close(); - } - - @Override - public void shutdownInput() throws IOException { - sock.shutdownInput(); - } - - @Override - public void shutdownOutput() throws IOException { - sock.shutdownOutput(); - } - - @Override - public String toString() { - return sock.toString(); - } - - @Override - public boolean isConnected() { - return sock.isConnected(); - } - - @Override - public boolean isBound() { - return sock.isBound(); - } - - @Override - public boolean isClosed() { - return sock.isClosed(); - } - - @Override - public boolean isInputShutdown() { - return sock.isInputShutdown(); - } - - @Override - public boolean isOutputShutdown() { - return sock.isOutputShutdown(); - } - - @Override - public void setPerformancePreferences(int connectionTime, int latency, - int bandwidth) { - sock.setPerformancePreferences(connectionTime, latency, bandwidth); - } -} diff --git a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/net/SdkSocket.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/net/SdkSocket.java deleted file mode 100644 index 286ce1b87d65..000000000000 --- a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/net/SdkSocket.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.http.apache5.internal.net; - -import java.io.IOException; -import java.net.Socket; -import java.net.SocketAddress; -import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.utils.Logger; - -@SdkInternalApi -public class SdkSocket extends DelegateSocket { - private static final Logger log = Logger.loggerFor(SdkSocket.class); - - public SdkSocket(Socket sock) { - super(sock); - log.debug(() -> "created: " + endpoint()); - } - - /** - * Returns the endpoint in the format of "address:port" - */ - private String endpoint() { - return sock.getInetAddress() + ":" + sock.getPort(); - } - - @Override - public void connect(SocketAddress endpoint) throws IOException { - log.trace(() -> "connecting to: " + endpoint); - sock.connect(endpoint); - log.debug(() -> "connected to: " + endpoint); - } - - @Override - public void connect(SocketAddress endpoint, int timeout) throws IOException { - log.trace(() -> "connecting to: " + endpoint); - sock.connect(endpoint, timeout); - log.debug(() -> "connected to: " + endpoint); - } - - @Override - public void close() throws IOException { - log.debug(() -> "closing " + endpoint()); - sock.close(); - } - - @Override - public void shutdownInput() throws IOException { - log.debug(() -> "shutting down input of " + endpoint()); - sock.shutdownInput(); - } - - @Override - public void shutdownOutput() throws IOException { - log.debug(() -> "shutting down output of " + endpoint()); - sock.shutdownOutput(); - } -} diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/Apache5ClientTlsAuthTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/Apache5ClientTlsAuthTest.java index 45e4b44a6d59..b9c118215e5a 100644 --- a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/Apache5ClientTlsAuthTest.java +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/Apache5ClientTlsAuthTest.java @@ -27,6 +27,7 @@ import com.github.tomakehurst.wiremock.WireMockServer; import java.io.IOException; +import java.net.Socket; import java.net.SocketException; import java.net.URI; import java.security.KeyManagementException; @@ -34,8 +35,11 @@ import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; -import org.apache.hc.client5.http.socket.ConnectionSocketFactory; +import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; +import org.apache.hc.client5.http.ssl.TlsSocketStrategy; +import org.apache.hc.core5.http.protocol.HttpContext; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; @@ -113,7 +117,7 @@ public void methodTeardown() { } @Test - public void canMakeHttpsRequestWhenKeyProviderConfigured() throws IOException { + public void prepareRequest_whenKeyProviderConfigured_successfullyMakesHttpsRequest() throws IOException { client = Apache5HttpClient.builder() .tlsKeyManagersProvider(keyManagersProvider) .build(); @@ -122,13 +126,13 @@ public void canMakeHttpsRequestWhenKeyProviderConfigured() throws IOException { } @Test - public void requestFailsWhenKeyProviderNotConfigured() throws IOException { + public void prepareRequest_whenKeyProviderNotConfigured_throwsSslException() throws IOException { client = Apache5HttpClient.builder().tlsKeyManagersProvider(NoneTlsKeyManagersProvider.getInstance()).build(); assertThatThrownBy(() -> makeRequestWithHttpClient(client)).isInstanceOfAny(SSLException.class, SocketException.class); } @Test - public void authenticatesWithTlsProxy() throws IOException { + public void prepareRequest_whenTlsProxyConfigured_authenticatesSuccessfully() throws IOException { ProxyConfiguration proxyConfig = ProxyConfiguration.builder() .endpoint(URI.create("https://localhost:" + wireMockServer.httpsPort())) .build(); @@ -145,7 +149,7 @@ public void authenticatesWithTlsProxy() throws IOException { } @Test - public void defaultTlsKeyManagersProviderIsSystemPropertyProvider() throws IOException { + public void build_whenNoTlsKeyManagersProviderSet_usesSystemPropertyProvider() throws IOException { System.setProperty(SSL_KEY_STORE.property(), clientKeyStore.toAbsolutePath().toString()); System.setProperty(SSL_KEY_STORE_TYPE.property(), CLIENT_STORE_TYPE); System.setProperty(SSL_KEY_STORE_PASSWORD.property(), STORE_PASSWORD); @@ -161,7 +165,7 @@ public void defaultTlsKeyManagersProviderIsSystemPropertyProvider() throws IOExc } @Test - public void defaultTlsKeyManagersProviderIsSystemPropertyProvider_explicitlySetToNull() throws IOException { + public void build_whenTlsKeyManagersProviderExplicitlySetToNull_usesSystemPropertyProvider() throws IOException { System.setProperty(SSL_KEY_STORE.property(), clientKeyStore.toAbsolutePath().toString()); System.setProperty(SSL_KEY_STORE_TYPE.property(), CLIENT_STORE_TYPE); System.setProperty(SSL_KEY_STORE_PASSWORD.property(), STORE_PASSWORD); @@ -177,9 +181,7 @@ public void defaultTlsKeyManagersProviderIsSystemPropertyProvider_explicitlySetT } @Test - public void build_notSettingSocketFactory_configuresClientWithDefaultSocketFactory() throws IOException, - NoSuchAlgorithmException, - KeyManagementException { + public void build_whenSocketFactoryNotSet_configuresDefaultSocketFactory() throws Exception { System.setProperty(SSL_KEY_STORE.property(), clientKeyStore.toAbsolutePath().toString()); System.setProperty(SSL_KEY_STORE_TYPE.property(), CLIENT_STORE_TYPE); System.setProperty(SSL_KEY_STORE_PASSWORD.property(), STORE_PASSWORD); @@ -192,8 +194,9 @@ public void build_notSettingSocketFactory_configuresClientWithDefaultSocketFacto SSLContext sslcontext = SSLContext.getInstance("TLS"); sslcontext.init(keyManagers, null, null); - ConnectionSocketFactory socketFactory = new SdkTlsSocketFactory(sslcontext, NoopHostnameVerifier.INSTANCE); - ConnectionSocketFactory socketFactoryMock = Mockito.spy(socketFactory); + // Use TlsSocketStrategy instead of ConnectionSocketFactory + TlsSocketStrategy socketFactory = new SdkTlsSocketFactory(sslcontext, NoopHostnameVerifier.INSTANCE); + TlsSocketStrategy socketFactoryMock = Mockito.spy(socketFactory); client = Apache5HttpClient.builder().build(); @@ -209,35 +212,6 @@ public void build_notSettingSocketFactory_configuresClientWithDefaultSocketFacto Mockito.verifyNoInteractions(socketFactoryMock); } - @Test - public void build_settingCustomSocketFactory_configuresClientWithGivenSocketFactory() throws IOException, - NoSuchAlgorithmException, - KeyManagementException { - TlsKeyManagersProvider provider = FileStoreTlsKeyManagersProvider.create(clientKeyStore, - CLIENT_STORE_TYPE, - STORE_PASSWORD); - KeyManager[] keyManagers = provider.keyManagers(); - - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(keyManagers, null, null); - - SdkTlsSocketFactory socketFactory = new SdkTlsSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE); - SdkTlsSocketFactory socketFactorySpy = Mockito.spy(socketFactory); - - - client = Apache5HttpClient.builder() - .socketFactory(socketFactorySpy) - .build(); - makeRequestWithHttpClient(client); - - Mockito.verify(socketFactorySpy).createLayeredSocket( - Mockito.any(), // Socket - Mockito.anyString(), // Target host - Mockito.anyInt(), // Port - Mockito.any() // HttpContext - ); - } - private HttpExecuteResponse makeRequestWithHttpClient(SdkHttpClient httpClient) throws IOException { SdkHttpRequest httpRequest = SdkHttpFullRequest.builder() .method(SdkHttpMethod.GET) @@ -252,4 +226,34 @@ private HttpExecuteResponse makeRequestWithHttpClient(SdkHttpClient httpClient) return httpClient.prepareRequest(request).call(); } + @Test + public void build_whenTlsSocketStrategyConfigured_usesProvidedStrategy() throws Exception { + // Setup TLS context + KeyManager[] keyManagers = keyManagersProvider.keyManagers(); + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(keyManagers, null, null); + + // Create and spy on TlsSocketStrategy + TlsSocketStrategy tlsStrategy = new DefaultClientTlsStrategy(sslContext, NoopHostnameVerifier.INSTANCE); + TlsSocketStrategy tlsStrategySpy = Mockito.spy(tlsStrategy); + + // Build client with TLS strategy + client = Apache5HttpClient.builder() + .tlsSocketStrategy(tlsStrategySpy) + .build(); + + // Make request and verify + HttpExecuteResponse response = makeRequestWithHttpClient(client); + assertThat(response.httpResponse().isSuccessful()).isTrue(); + + // Verify upgrade method was called + Mockito.verify(tlsStrategySpy).upgrade( + Mockito.any(Socket.class), + Mockito.anyString(), + Mockito.anyInt(), + Mockito.any(), + Mockito.any(HttpContext.class) + ); + } + } diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/Apache5ClientTlsHalfCloseTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/Apache5ClientTlsHalfCloseTest.java index 334eb2ac6b5b..d6a15b337ad1 100644 --- a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/Apache5ClientTlsHalfCloseTest.java +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/Apache5ClientTlsHalfCloseTest.java @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -59,8 +60,11 @@ public void errorWhenServerHalfClosesSocketWhileStreamIsOpened() { IOException exception = assertThrows(IOException.class, () -> { executeHttpRequest(httpClient); }); - assertThat(exception.getMessage()) - .containsIgnoringCase("broken pipe"); + assertTrue( + exception.getMessage().equals("Connection or outbound has closed") || + exception.getMessage().equals("Connection is closed"), + "Expected connection closed message, but got: " + exception.getMessage()); + } @@ -78,7 +82,11 @@ public void errorWhenServerFullClosesSocketWhileStreamIsOpened() throws IOExcept }); if(halfCloseSupported()){ - assertEquals("Connection or outbound has closed", exception.getMessage()); + assertTrue( + exception.getMessage().equals("Connection or outbound has closed") || + exception.getMessage().equals("Connection is closed"), + "Expected connection closed message, but got: " + exception.getMessage()); + }else { assertEquals("Socket is closed", exception.getMessage()); diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/Apache5HttpClientWithSocketFactoryWireMockTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/Apache5HttpClientWithSocketFactoryWireMockTest.java new file mode 100644 index 000000000000..b0a0558f7487 --- /dev/null +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/Apache5HttpClientWithSocketFactoryWireMockTest.java @@ -0,0 +1,155 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.apache5; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import org.apache.hc.client5.http.socket.ConnectionSocketFactory; +import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; +import org.apache.hc.client5.http.ssl.TlsSocketStrategy; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.ssl.SSLContexts; +import org.apache.hc.core5.ssl.TrustStrategy; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import software.amazon.awssdk.http.HttpExecuteRequest; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.apache5.internal.conn.SdkTlsSocketFactory; +import software.amazon.awssdk.utils.IoUtils; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.Socket; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; + +class Apache5HttpClientWithSocketFactoryWireMockTest { + + private WireMockServer httpMockServer; + private WireMockServer httpsMockServer; + + @BeforeEach + void setUp() { + httpMockServer = new WireMockServer(WireMockConfiguration.options().dynamicPort()); + httpMockServer.start(); + + httpsMockServer = new WireMockServer(WireMockConfiguration.options().dynamicHttpsPort()); + httpsMockServer.start(); + } + + @AfterEach + void tearDown() { + httpMockServer.stop(); + httpsMockServer.stop(); + } + + @Test + void prepareRequest_withTlsStrategyOverHttp_doesNotCallUpgrade() throws Exception { + TlsSocketStrategy tlsStrategySpy = spy(new TlsSocketStrategy() { + @Override + public SSLSocket upgrade(Socket socket, String target, int port, + Object attachment, HttpContext context) { + fail("TLS upgrade should not be called for HTTP"); + return null; + } + }); + SdkHttpClient client = Apache5HttpClient.builder() + .tlsSocketStrategy(tlsStrategySpy) + .build(); + + stubForMockRequest(httpMockServer, 200); + SdkHttpFullRequest request = mockSdkRequest("http://localhost:" + httpMockServer.port(), SdkHttpMethod.GET); + HttpExecuteResponse response = client.prepareRequest(HttpExecuteRequest.builder() + .request(request) + .contentStreamProvider(request.contentStreamProvider().orElse(null)) + .build()) + .call(); + validateResponse(response, 200); + verify(tlsStrategySpy, never()).upgrade(any(), any(), anyInt(), any(), any()); + } + + @Test + void prepareRequest_withTlsStrategyOverHttps_callsUpgrade() throws Exception { + SSLContext trustAllContext = createTrustAllSslContext(); + TlsSocketStrategy tlsStrategySpy = spy(new SdkTlsSocketFactory( + trustAllContext, + NoopHostnameVerifier.INSTANCE + )); + SdkHttpClient client = Apache5HttpClient.builder() + .tlsSocketStrategy(tlsStrategySpy) + .build(); + + stubForMockRequest(httpsMockServer, 200); + SdkHttpFullRequest request = mockSdkRequest("https://localhost:" + httpsMockServer.httpsPort(), SdkHttpMethod.GET); + HttpExecuteResponse response = client.prepareRequest(HttpExecuteRequest.builder() + .request(request) + .contentStreamProvider(request.contentStreamProvider().orElse(null)) + .build()) + .call(); + + validateResponse(response, 200); + verify(tlsStrategySpy, atLeastOnce()).upgrade(any(), eq("localhost"), eq(httpsMockServer.httpsPort()), any(), any()); + } + + private void stubForMockRequest(WireMockServer server, int returnCode) { + server.stubFor(WireMock.get(WireMock.urlPathEqualTo("/test")) + .willReturn(WireMock.aResponse() + .withStatus(returnCode) + .withBody("test response body"))); + } + + private SdkHttpFullRequest mockSdkRequest(String url, SdkHttpMethod method) { + return SdkHttpFullRequest.builder() + .uri(URI.create(url + "/test")) + .method(method) + .build(); + } + + private void validateResponse(HttpExecuteResponse response, int expectedStatusCode) throws Exception { + assertThat(response).isNotNull(); + assertThat(response.httpResponse()).isNotNull(); + assertThat(response.httpResponse().statusCode()).isEqualTo(expectedStatusCode); + + if (response.responseBody().isPresent()) { + try (InputStream is = response.responseBody().get()) { + String body = IoUtils.toUtf8String(is); + assertThat(body).isEqualTo("test response body"); + } + } + } + + private SSLContext createTrustAllSslContext() throws Exception { + TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true; + return SSLContexts.custom() + .loadTrustMaterial(null, acceptingTrustStrategy) + .build(); + } +} diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/conn/SdkTlsSocketFactoryTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/conn/SdkTlsSocketFactoryTest.java index ed8e89686eb9..1ed5b7ea0d27 100644 --- a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/conn/SdkTlsSocketFactoryTest.java +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/conn/SdkTlsSocketFactoryTest.java @@ -42,7 +42,7 @@ void nullProtocols() { when(socket.getSupportedProtocols()).thenReturn(null); when(socket.getEnabledProtocols()).thenReturn(null); - factory.prepareSocket(socket); + factory.initializeSocket(socket); verify(socket, never()).setEnabledProtocols(any()); } @@ -56,7 +56,7 @@ void amazonCorretto_8_0_292_defaultEnabledProtocols() { "TLSv1.2", "TLSv1.1", "TLSv1" }); - factory.prepareSocket(socket); + factory.initializeSocket(socket); verify(socket, never()).setEnabledProtocols(any()); } @@ -70,7 +70,7 @@ void amazonCorretto_11_0_08_defaultEnabledProtocols() { "TLSv1.3", "TLSv1.2", "TLSv1.1", "TLSv1" }); - factory.prepareSocket(socket); + factory.initializeSocket(socket); verify(socket, never()).setEnabledProtocols(any()); } @@ -84,7 +84,7 @@ void amazonCorretto_17_0_1_defaultEnabledProtocols() { "TLSv1.3", "TLSv1.2" }); - factory.prepareSocket(socket); + factory.initializeSocket(socket); verify(socket, never()).setEnabledProtocols(any()); }