diff --git a/http-clients/apache5-client/pom.xml b/http-clients/apache5-client/pom.xml index 5abdca8517c9..3815601e7158 100644 --- a/http-clients/apache5-client/pom.xml +++ b/http-clients/apache5-client/pom.xml @@ -33,6 +33,47 @@ http-client-spi ${awsjavasdk.version} + + org.apache.httpcomponents.client5 + httpclient5 + 5.4.4 + + + org.apache.httpcomponents.core5 + httpcore5 + 5.3.4 + + + software.amazon.awssdk + http-client-tests + ${awsjavasdk.version} + test + + + org.junit.jupiter + junit-jupiter + test + + + org.junit.vintage + junit-vintage-engine + test + + + org.assertj + assertj-core + test + + + org.hamcrest + hamcrest-all + test + + + com.github.tomakehurst + wiremock-jre8 + test + 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 8ff17738ffd9..5680c6231759 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 @@ -15,36 +15,316 @@ package software.amazon.awssdk.http.apache5; +import static software.amazon.awssdk.http.HttpMetric.AVAILABLE_CONCURRENCY; +import static software.amazon.awssdk.http.HttpMetric.HTTP_CLIENT_NAME; +import static software.amazon.awssdk.http.HttpMetric.LEASED_CONCURRENCY; +import static software.amazon.awssdk.http.HttpMetric.MAX_CONCURRENCY; +import static software.amazon.awssdk.http.HttpMetric.PENDING_CONCURRENCY_ACQUIRES; +import static software.amazon.awssdk.http.apache5.internal.conn.ClientConnectionRequestFactory.THREAD_LOCAL_REQUEST_METRIC_COLLECTOR; +import static software.amazon.awssdk.utils.NumericUtils.saturatedCast; + +import java.io.IOException; +import java.net.InetAddress; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import org.apache.hc.client5.http.auth.CredentialsProvider; +import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; +import org.apache.hc.client5.http.impl.DefaultSchemePortResolver; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HeaderIterator; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.client5.http.auth.AuthSchemeProvider; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.client5.http.protocol.HttpClientContext; +import org.apache.hc.client5.http.routing.HttpRoutePlanner; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; +import org.apache.hc.core5.http.config.Registry; +import org.apache.hc.core5.http.config.RegistryBuilder; +import org.apache.hc.client5.http.ConnectionKeepAliveStrategy; +import org.apache.hc.client5.http.DnsResolver; +import org.apache.hc.client5.http.socket.ConnectionSocketFactory; +import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory; +import org.apache.hc.core5.http.impl.io.HttpRequestExecutor; +import org.apache.hc.core5.http.io.SocketConfig; +import org.apache.hc.core5.ssl.SSLInitializationException; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.apache.hc.core5.pool.PoolStats; import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.annotations.SdkTestInternalApi; +import software.amazon.awssdk.http.AbortableInputStream; import software.amazon.awssdk.http.ExecutableHttpRequest; import software.amazon.awssdk.http.HttpExecuteRequest; +import software.amazon.awssdk.http.HttpExecuteResponse; import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpConfigurationOption; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.http.SystemPropertyTlsKeyManagersProvider; +import software.amazon.awssdk.http.TlsKeyManagersProvider; +import software.amazon.awssdk.http.TlsTrustManagersProvider; +import software.amazon.awssdk.http.apache5.internal.Apache5HttpRequestConfig; +import software.amazon.awssdk.http.apache5.internal.DefaultConfiguration; +import software.amazon.awssdk.http.apache5.internal.SdkProxyRoutePlanner; +import software.amazon.awssdk.http.apache5.internal.conn.ClientConnectionManagerFactory; +import software.amazon.awssdk.http.apache5.internal.conn.IdleConnectionReaper; +import software.amazon.awssdk.http.apache5.internal.conn.SdkConnectionKeepAliveStrategy; +import software.amazon.awssdk.http.apache5.internal.conn.SdkTlsSocketFactory; +import software.amazon.awssdk.http.apache5.internal.impl.Apache5HttpRequestFactory; +import software.amazon.awssdk.http.apache5.internal.impl.Apache5SdkHttpClient; +import software.amazon.awssdk.http.apache5.internal.impl.ConnectionManagerAwareHttpClient; +import software.amazon.awssdk.http.apache5.internal.utils.Apache5Utils; +import software.amazon.awssdk.metrics.MetricCollector; +import software.amazon.awssdk.metrics.NoOpMetricCollector; +import software.amazon.awssdk.utils.AttributeMap; +import software.amazon.awssdk.utils.Logger; +import software.amazon.awssdk.utils.Validate; +// TODO: All the Java Doc will be updated to consider the reference of Apache4.x if required /** - * An implementation of {@link SdkHttpClient} that uses Apache HTTP Client 5.x to communicate with the service. This client - * provides enhanced functionality over the URL connection client, including support for HTTP proxies, connection pooling, - * and advanced configuration options. - * - *

This implementation leverages Apache HttpClient 5.x, offering improved performance characteristics and better compliance - * with HTTP standards compared to the Apache 4.x-based.

+ * An implementation of {@link SdkHttpClient} that uses Apache5 HTTP client to communicate with the service. This is the most + * powerful synchronous client that adds an extra dependency and additional startup latency in exchange for more functionality, + * like support for HTTP proxies. * - *

See software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient for a lighter alternative implementation - * with fewer dependencies but more limited functionality.

+ *

See software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient for an alternative implementation.

* + *

This can be created via {@link #builder()}

*/ @SdkPublicApi -public class Apache5HttpClient implements SdkHttpClient { +public final class Apache5HttpClient implements SdkHttpClient { public static final String CLIENT_NAME = "Apache5"; + private static final Logger log = Logger.loggerFor(Apache5HttpClient.class); + + private final Apache5HttpRequestFactory apacheHttpRequestFactory = new Apache5HttpRequestFactory(); + private final ConnectionManagerAwareHttpClient httpClient; + private final Apache5HttpRequestConfig requestConfig; + private final AttributeMap resolvedOptions; + + @SdkTestInternalApi + Apache5HttpClient(ConnectionManagerAwareHttpClient httpClient, + Apache5HttpRequestConfig requestConfig, + AttributeMap resolvedOptions) { + this.httpClient = httpClient; + this.requestConfig = requestConfig; + this.resolvedOptions = resolvedOptions; + } + + private Apache5HttpClient(DefaultBuilder builder, AttributeMap resolvedOptions) { + this.httpClient = createClient(builder, resolvedOptions); + this.requestConfig = createRequestConfig(builder, resolvedOptions); + this.resolvedOptions = resolvedOptions; + } + + public static Builder builder() { + return new DefaultBuilder(); + } + + /** + * Create a {@link Apache5HttpClient} with the default properties + * + * @return an {@link Apache5HttpClient} + */ + public static SdkHttpClient create() { + return new DefaultBuilder().build(); + } + + private ConnectionManagerAwareHttpClient createClient(Apache5HttpClient.DefaultBuilder configuration, + AttributeMap standardOptions) { + ApacheConnectionManagerFactory cmFactory = new ApacheConnectionManagerFactory(); + + HttpClientBuilder builder = HttpClients.custom(); + // Note that it is important we register the original connection manager with the + // IdleConnectionReaper as it's required for the successful deregistration of managers + // from the reaper. See https://github.com/aws/aws-sdk-java/issues/722. + HttpClientConnectionManager cm = cmFactory.create(configuration, standardOptions); + + Registry authSchemeProviderRegistry = configuration.authSchemeProviderRegistry; + if (authSchemeProviderRegistry != null) { + builder.setDefaultAuthSchemeRegistry(authSchemeProviderRegistry); + } + + + builder.setRequestExecutor(new HttpRequestExecutor()) + // SDK handles decompression + .disableContentCompression() + .setKeepAliveStrategy(buildKeepAliveStrategy(standardOptions)) + .disableRedirectHandling() + .disableAutomaticRetries() + .setUserAgent("") // SDK will set the user agent header in the pipeline. Don't let Apache5 waste time + .setConnectionManager(ClientConnectionManagerFactory.wrap(cm)); + + addProxyConfig(builder, configuration); + + if (useIdleConnectionReaper(standardOptions)) { + IdleConnectionReaper.getInstance().registerConnectionManager( + cm, standardOptions.get(SdkHttpConfigurationOption.CONNECTION_MAX_IDLE_TIMEOUT).toMillis()); + } + + return new Apache5SdkHttpClient(builder.build(), cm); + } + + private void addProxyConfig(HttpClientBuilder builder, + DefaultBuilder configuration) { + ProxyConfiguration proxyConfiguration = configuration.proxyConfiguration; + + Validate.isTrue(configuration.httpRoutePlanner == null || !isProxyEnabled(proxyConfiguration), + "The httpRoutePlanner and proxyConfiguration can't both be configured."); + Validate.isTrue(configuration.credentialsProvider == null || !isAuthenticatedProxy(proxyConfiguration), + "The credentialsProvider and proxyConfiguration username/password can't both be configured."); + + HttpRoutePlanner routePlanner = configuration.httpRoutePlanner; + if (isProxyEnabled(proxyConfiguration)) { + log.debug(() -> "Configuring Proxy. Proxy Host: " + proxyConfiguration.host()); + routePlanner = new SdkProxyRoutePlanner(proxyConfiguration.host(), + proxyConfiguration.port(), + proxyConfiguration.scheme(), + proxyConfiguration.nonProxyHosts()); + } + + CredentialsProvider credentialsProvider = configuration.credentialsProvider; + if (isAuthenticatedProxy(proxyConfiguration)) { + credentialsProvider = Apache5Utils.newProxyCredentialsProvider(proxyConfiguration); + } + + if (routePlanner != null) { + builder.setRoutePlanner(routePlanner); + } + + if (credentialsProvider != null) { + builder.setDefaultCredentialsProvider(credentialsProvider); + } + } + + private ConnectionKeepAliveStrategy buildKeepAliveStrategy(AttributeMap standardOptions) { + long maxIdle = standardOptions.get(SdkHttpConfigurationOption.CONNECTION_MAX_IDLE_TIMEOUT).toMillis(); + return maxIdle > 0 ? new SdkConnectionKeepAliveStrategy(maxIdle) : null; + } + + private boolean useIdleConnectionReaper(AttributeMap standardOptions) { + return Boolean.TRUE.equals(standardOptions.get(SdkHttpConfigurationOption.REAP_IDLE_CONNECTIONS)); + } + + private boolean isAuthenticatedProxy(ProxyConfiguration proxyConfiguration) { + return proxyConfiguration.username() != null && proxyConfiguration.password() != null; + } + + private boolean isProxyEnabled(ProxyConfiguration proxyConfiguration) { + return proxyConfiguration.host() != null + && proxyConfiguration.port() > 0; + } + @Override public ExecutableHttpRequest prepareRequest(HttpExecuteRequest request) { - throw new UnsupportedOperationException("API implementation is in progress"); + MetricCollector metricCollector = request.metricCollector().orElseGet(NoOpMetricCollector::create); + metricCollector.reportMetric(HTTP_CLIENT_NAME, clientName()); + HttpUriRequestBase apacheRequest = toApacheRequest(request); + return new ExecutableHttpRequest() { + @Override + public HttpExecuteResponse call() throws IOException { + HttpExecuteResponse executeResponse = execute(apacheRequest, metricCollector); + collectPoolMetric(metricCollector); + return executeResponse; + } + + @Override + public void abort() { + apacheRequest.abort(); + } + }; } @Override public void close() { - throw new UnsupportedOperationException("API implementation is in progress"); + HttpClientConnectionManager cm = httpClient.getHttpClientConnectionManager(); + IdleConnectionReaper.getInstance().deregisterConnectionManager(cm); + cm.shutdown(); + } + + private HttpExecuteResponse execute(HttpUriRequestBase apacheRequest, MetricCollector metricCollector) throws IOException { + HttpClientContext localRequestContext = Apache5Utils.newClientContext(requestConfig.proxyConfiguration()); + THREAD_LOCAL_REQUEST_METRIC_COLLECTOR.set(metricCollector); + try { + HttpResponse httpResponse = httpClient.execute(apacheRequest, localRequestContext); + return createResponse(httpResponse, apacheRequest); + } finally { + THREAD_LOCAL_REQUEST_METRIC_COLLECTOR.remove(); + } + } + + private HttpUriRequestBase toApacheRequest(HttpExecuteRequest request) { + return apacheHttpRequestFactory.create(request, requestConfig); + } + + /** + * Creates and initializes an HttpResponse object suitable to be passed to an HTTP response + * handler object. + * + * @return The new, initialized HttpResponse object ready to be passed to an HTTP response handler object. + * @throws IOException If there were any problems getting any response information from the + * HttpClient method object. + */ + private HttpExecuteResponse createResponse(HttpResponse apacheHttpResponse, + HttpUriRequestBase apacheRequest) throws IOException { + SdkHttpResponse.Builder responseBuilder = + SdkHttpResponse.builder() + .statusCode(apacheHttpResponse.getCode()) + .statusText(apacheHttpResponse.getReasonPhrase()); + + HeaderIterator headerIterator = apacheHttpResponse.headerIterator(); + while (headerIterator.hasNext()) { + Header header = headerIterator.nextHeader(); + responseBuilder.appendHeader(header.getName(), header.getValue()); + } + + AbortableInputStream responseBody = apacheHttpResponse.getEntity() != null ? + toAbortableInputStream(apacheHttpResponse, apacheRequest) : null; + + return HttpExecuteResponse.builder().response(responseBuilder.build()).responseBody(responseBody).build(); + + } + + private AbortableInputStream toAbortableInputStream(HttpResponse apacheHttpResponse, HttpUriRequestBase apacheRequest) + throws IOException { + return AbortableInputStream.create(apacheHttpResponse.getEntity().getContent(), apacheRequest::abort); + } + + private Apache5HttpRequestConfig createRequestConfig(DefaultBuilder builder, + AttributeMap resolvedOptions) { + return Apache5HttpRequestConfig.builder() + .socketTimeout(resolvedOptions.get(SdkHttpConfigurationOption.READ_TIMEOUT)) + .connectionTimeout(resolvedOptions.get(SdkHttpConfigurationOption.CONNECTION_TIMEOUT)) + .connectionAcquireTimeout( + resolvedOptions.get(SdkHttpConfigurationOption.CONNECTION_ACQUIRE_TIMEOUT)) + .proxyConfiguration(builder.proxyConfiguration) + .localAddress(Optional.ofNullable(builder.localAddress).orElse(null)) + .expectContinueEnabled(Optional.ofNullable(builder.expectContinueEnabled) + .orElse(DefaultConfiguration.EXPECT_CONTINUE_ENABLED)) + .build(); + } + + private void collectPoolMetric(MetricCollector metricCollector) { + HttpClientConnectionManager cm = httpClient.getHttpClientConnectionManager(); + if (cm instanceof PoolingHttpClientConnectionManager && !(metricCollector instanceof NoOpMetricCollector)) { + PoolingHttpClientConnectionManager poolingCm = (PoolingHttpClientConnectionManager) cm; + PoolStats totalStats = poolingCm.getTotalStats(); + metricCollector.reportMetric(MAX_CONCURRENCY, totalStats.getMax()); + metricCollector.reportMetric(AVAILABLE_CONCURRENCY, totalStats.getAvailable()); + metricCollector.reportMetric(LEASED_CONCURRENCY, totalStats.getLeased()); + metricCollector.reportMetric(PENDING_CONCURRENCY_ACQUIRES, totalStats.getPending()); + } } @Override @@ -52,4 +332,449 @@ public String clientName() { return CLIENT_NAME; } + /** + * Builder for creating an instance of {@link SdkHttpClient}. The factory can be configured through the builder {@link + * #builder()}, once built it can create a {@link SdkHttpClient} via {@link #build()} or can be passed to the SDK + * client builders directly to have the SDK create and manage the HTTP client. See documentation on the service's respective + * client builder for more information on configuring the HTTP layer. + * + *
+     * SdkHttpClient httpClient =
+     *     Apache5HttpClient.builder()
+     *                     .socketTimeout(Duration.ofSeconds(10))
+     *                     .build();
+     * 
+ */ + public interface Builder extends SdkHttpClient.Builder { + + /** + * The amount of time to wait for data to be transferred over an established, open connection before the connection is + * timed out. A duration of 0 means infinity, and is not recommended. + */ + Builder socketTimeout(Duration socketTimeout); + + /** + * The amount of time to wait when initially establishing a connection before giving up and timing out. A duration of 0 + * means infinity, and is not recommended. + */ + Builder connectionTimeout(Duration connectionTimeout); + + /** + * The amount of time to wait when acquiring a connection from the pool before giving up and timing out. + * @param connectionAcquisitionTimeout the timeout duration + * @return this builder for method chaining. + */ + Builder connectionAcquisitionTimeout(Duration connectionAcquisitionTimeout); + + /** + * The maximum number of connections allowed in the connection pool. Each built HTTP client has its own private + * connection pool. + */ + Builder maxConnections(Integer maxConnections); + + /** + * Configuration that defines how to communicate via an HTTP proxy. + */ + Builder proxyConfiguration(ProxyConfiguration proxyConfiguration); + + /** + * Configure the local address that the HTTP client should use for communication. + */ + Builder localAddress(InetAddress localAddress); + + /** + * Configure whether the client should send an HTTP expect-continue handshake before each request. + */ + Builder expectContinueEnabled(Boolean expectContinueEnabled); + + /** + * The maximum amount of time that a connection should be allowed to remain open, regardless of usage frequency. + */ + Builder connectionTimeToLive(Duration connectionTimeToLive); + + /** + * Configure the maximum amount of time that a connection should be allowed to remain open while idle. + */ + Builder connectionMaxIdleTime(Duration maxIdleConnectionTimeout); + + /** + * Configure whether the idle connections in the connection pool should be closed asynchronously. + *

+ * When enabled, connections left idling for longer than {@link #connectionMaxIdleTime(Duration)} will be + * closed. This will not close connections currently in use. By default, this is enabled. + */ + Builder useIdleConnectionReaper(Boolean useConnectionReaper); + + /** + * Configuration that defines a DNS resolver. If no matches are found, the default resolver is used. + */ + Builder dnsResolver(DnsResolver dnsResolver); + + /** + * Configuration that defines a custom Socket factory. If set to a null value, a default factory is used. + *

+ * 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. + */ + Builder socketFactory(ConnectionSocketFactory socketFactory); + + /** + * Configuration that defines an HTTP route planner that computes the route an HTTP request should take. + * May not be used in conjunction with {@link #proxyConfiguration(ProxyConfiguration)}. + */ + Builder httpRoutePlanner(HttpRoutePlanner proxyConfiguration); + + /** + * Configuration that defines a custom credential provider for HTTP requests. + * May not be used in conjunction with {@link ProxyConfiguration#username()} and {@link ProxyConfiguration#password()}. + */ + Builder credentialsProvider(CredentialsProvider credentialsProvider); + + /** + * Configure whether to enable or disable TCP KeepAlive. + * The configuration will be passed to the socket option {@link java.net.SocketOptions#SO_KEEPALIVE}. + *

+ * By default, this is disabled. + *

+ * When enabled, the actual KeepAlive mechanism is dependent on the Operating System and therefore additional TCP + * KeepAlive values (like timeout, number of packets, etc) must be configured via the Operating System (sysctl on + * Linux/Mac, and Registry values on Windows). + */ + Builder tcpKeepAlive(Boolean keepConnectionAlive); + + /** + * Configure the {@link TlsKeyManagersProvider} that will provide the {@link javax.net.ssl.KeyManager}s to use + * when constructing the SSL context. + *

+ * The default used by the client will be {@link SystemPropertyTlsKeyManagersProvider}. Configure an instance of + * {@link software.amazon.awssdk.internal.http.NoneTlsKeyManagersProvider} or another implementation of + * {@link TlsKeyManagersProvider} to override it. + */ + Builder tlsKeyManagersProvider(TlsKeyManagersProvider tlsKeyManagersProvider); + + /** + * Configure the {@link TlsTrustManagersProvider} that will provide the {@link javax.net.ssl.TrustManager}s to use + * when constructing the SSL context. + */ + Builder tlsTrustManagersProvider(TlsTrustManagersProvider tlsTrustManagersProvider); + + /** + * Configure the authentication scheme registry that can be used to obtain the corresponding authentication scheme + * implementation for a given type of authorization challenge. + */ + Builder authSchemeProviderRegistry(Registry authSchemeProviderRegistry); + } + + private static final class DefaultBuilder implements Builder { + private final AttributeMap.Builder standardOptions = AttributeMap.builder(); + private Registry authSchemeProviderRegistry; + private ProxyConfiguration proxyConfiguration = ProxyConfiguration.builder().build(); + private InetAddress localAddress; + private Boolean expectContinueEnabled; + private HttpRoutePlanner httpRoutePlanner; + private CredentialsProvider credentialsProvider; + private DnsResolver dnsResolver; + private ConnectionSocketFactory socketFactory; + + private DefaultBuilder() { + } + + @Override + public Builder socketTimeout(Duration socketTimeout) { + standardOptions.put(SdkHttpConfigurationOption.READ_TIMEOUT, socketTimeout); + return this; + } + + public void setSocketTimeout(Duration socketTimeout) { + socketTimeout(socketTimeout); + } + + @Override + public Builder connectionTimeout(Duration connectionTimeout) { + standardOptions.put(SdkHttpConfigurationOption.CONNECTION_TIMEOUT, connectionTimeout); + return this; + } + + public void setConnectionTimeout(Duration connectionTimeout) { + connectionTimeout(connectionTimeout); + } + + /** + * The amount of time to wait when acquiring a connection from the pool before giving up and timing out. + * @param connectionAcquisitionTimeout the timeout duration + * @return this builder for method chaining. + */ + @Override + public Builder connectionAcquisitionTimeout(Duration connectionAcquisitionTimeout) { + Validate.isPositive(connectionAcquisitionTimeout, "connectionAcquisitionTimeout"); + standardOptions.put(SdkHttpConfigurationOption.CONNECTION_ACQUIRE_TIMEOUT, connectionAcquisitionTimeout); + return this; + } + + public void setConnectionAcquisitionTimeout(Duration connectionAcquisitionTimeout) { + connectionAcquisitionTimeout(connectionAcquisitionTimeout); + } + + @Override + public Builder maxConnections(Integer maxConnections) { + standardOptions.put(SdkHttpConfigurationOption.MAX_CONNECTIONS, maxConnections); + return this; + } + + public void setMaxConnections(Integer maxConnections) { + maxConnections(maxConnections); + } + + @Override + public Builder proxyConfiguration(ProxyConfiguration proxyConfiguration) { + this.proxyConfiguration = proxyConfiguration; + return this; + } + + public void setProxyConfiguration(ProxyConfiguration proxyConfiguration) { + proxyConfiguration(proxyConfiguration); + } + + @Override + public Builder localAddress(InetAddress localAddress) { + this.localAddress = localAddress; + return this; + } + + public void setLocalAddress(InetAddress localAddress) { + localAddress(localAddress); + } + + @Override + public Builder expectContinueEnabled(Boolean expectContinueEnabled) { + this.expectContinueEnabled = expectContinueEnabled; + return this; + } + + public void setExpectContinueEnabled(Boolean useExpectContinue) { + this.expectContinueEnabled = useExpectContinue; + } + + @Override + public Builder connectionTimeToLive(Duration connectionTimeToLive) { + standardOptions.put(SdkHttpConfigurationOption.CONNECTION_TIME_TO_LIVE, connectionTimeToLive); + return this; + } + + public void setConnectionTimeToLive(Duration connectionTimeToLive) { + connectionTimeToLive(connectionTimeToLive); + } + + @Override + public Builder connectionMaxIdleTime(Duration maxIdleConnectionTimeout) { + standardOptions.put(SdkHttpConfigurationOption.CONNECTION_MAX_IDLE_TIMEOUT, maxIdleConnectionTimeout); + return this; + } + + public void setConnectionMaxIdleTime(Duration connectionMaxIdleTime) { + connectionMaxIdleTime(connectionMaxIdleTime); + } + + @Override + public Builder useIdleConnectionReaper(Boolean useIdleConnectionReaper) { + standardOptions.put(SdkHttpConfigurationOption.REAP_IDLE_CONNECTIONS, useIdleConnectionReaper); + return this; + } + + public void setUseIdleConnectionReaper(Boolean useIdleConnectionReaper) { + useIdleConnectionReaper(useIdleConnectionReaper); + } + + @Override + public Builder dnsResolver(DnsResolver dnsResolver) { + this.dnsResolver = dnsResolver; + return this; + } + + public void setDnsResolver(DnsResolver dnsResolver) { + dnsResolver(dnsResolver); + } + + @Override + public Builder socketFactory(ConnectionSocketFactory socketFactory) { + this.socketFactory = socketFactory; + return this; + } + + public void setSocketFactory(ConnectionSocketFactory socketFactory) { + socketFactory(socketFactory); + } + + @Override + public Builder httpRoutePlanner(HttpRoutePlanner httpRoutePlanner) { + this.httpRoutePlanner = httpRoutePlanner; + return this; + } + + public void setHttpRoutePlanner(HttpRoutePlanner httpRoutePlanner) { + httpRoutePlanner(httpRoutePlanner); + } + + @Override + public Builder credentialsProvider(CredentialsProvider credentialsProvider) { + this.credentialsProvider = credentialsProvider; + return this; + } + + public void setCredentialsProvider(CredentialsProvider credentialsProvider) { + credentialsProvider(credentialsProvider); + } + + @Override + public Builder tcpKeepAlive(Boolean keepConnectionAlive) { + standardOptions.put(SdkHttpConfigurationOption.TCP_KEEPALIVE, keepConnectionAlive); + return this; + } + + public void setTcpKeepAlive(Boolean keepConnectionAlive) { + tcpKeepAlive(keepConnectionAlive); + } + + @Override + public Builder tlsKeyManagersProvider(TlsKeyManagersProvider tlsKeyManagersProvider) { + standardOptions.put(SdkHttpConfigurationOption.TLS_KEY_MANAGERS_PROVIDER, tlsKeyManagersProvider); + return this; + } + + public void setTlsKeyManagersProvider(TlsKeyManagersProvider tlsKeyManagersProvider) { + tlsKeyManagersProvider(tlsKeyManagersProvider); + } + + @Override + public Builder tlsTrustManagersProvider(TlsTrustManagersProvider tlsTrustManagersProvider) { + standardOptions.put(SdkHttpConfigurationOption.TLS_TRUST_MANAGERS_PROVIDER, tlsTrustManagersProvider); + return this; + } + + public void setTlsTrustManagersProvider(TlsTrustManagersProvider tlsTrustManagersProvider) { + tlsTrustManagersProvider(tlsTrustManagersProvider); + } + + @Override + public Builder authSchemeProviderRegistry(Registry authSchemeProviderRegistry) { + this.authSchemeProviderRegistry = authSchemeProviderRegistry; + return this; + } + + public void setAuthSchemeProviderRegistry(Registry authSchemeProviderRegistry) { + authSchemeProviderRegistry(authSchemeProviderRegistry); + } + + @Override + public SdkHttpClient buildWithDefaults(AttributeMap serviceDefaults) { + AttributeMap resolvedOptions = standardOptions.build().merge(serviceDefaults).merge( + SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS); + return new Apache5HttpClient(this, resolvedOptions); + } + } + + private static class ApacheConnectionManagerFactory { + + public HttpClientConnectionManager create(Apache5HttpClient.DefaultBuilder configuration, + AttributeMap standardOptions) { + ConnectionSocketFactory sslsf = getPreferredSocketFactory(configuration, standardOptions); + + PoolingHttpClientConnectionManager cm = new + PoolingHttpClientConnectionManager( + createSocketFactoryRegistry(sslsf), + null, + DefaultSchemePortResolver.INSTANCE, + configuration.dnsResolver, + standardOptions.get(SdkHttpConfigurationOption.CONNECTION_TIME_TO_LIVE).toMillis(), + TimeUnit.MILLISECONDS); + + cm.setDefaultMaxPerRoute(standardOptions.get(SdkHttpConfigurationOption.MAX_CONNECTIONS)); + cm.setMaxTotal(standardOptions.get(SdkHttpConfigurationOption.MAX_CONNECTIONS)); + cm.setDefaultSocketConfig(buildSocketConfig(standardOptions)); + + return cm; + } + + private ConnectionSocketFactory getPreferredSocketFactory(Apache5HttpClient.DefaultBuilder configuration, + AttributeMap standardOptions) { + return Optional.ofNullable(configuration.socketFactory) + .orElseGet(() -> new SdkTlsSocketFactory(getSslContext(standardOptions), + getHostNameVerifier(standardOptions))); + } + + private HostnameVerifier getHostNameVerifier(AttributeMap standardOptions) { + return standardOptions.get(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES) + ? NoopHostnameVerifier.INSTANCE + : SSLConnectionSocketFactory.getDefaultHostnameVerifier(); + } + + private SSLContext getSslContext(AttributeMap standardOptions) { + Validate.isTrue(standardOptions.get(SdkHttpConfigurationOption.TLS_TRUST_MANAGERS_PROVIDER) == null || + !standardOptions.get(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES), + "A TlsTrustManagerProvider can't be provided if TrustAllCertificates is also set"); + + TrustManager[] trustManagers = null; + if (standardOptions.get(SdkHttpConfigurationOption.TLS_TRUST_MANAGERS_PROVIDER) != null) { + trustManagers = standardOptions.get(SdkHttpConfigurationOption.TLS_TRUST_MANAGERS_PROVIDER).trustManagers(); + } + + if (standardOptions.get(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES)) { + log.warn(() -> "SSL Certificate verification is disabled. This is not a safe setting and should only be " + + "used for testing."); + trustManagers = trustAllTrustManager(); + } + + TlsKeyManagersProvider provider = standardOptions.get(SdkHttpConfigurationOption.TLS_KEY_MANAGERS_PROVIDER); + KeyManager[] keyManagers = provider.keyManagers(); + + try { + SSLContext sslcontext = SSLContext.getInstance("TLS"); + // http://download.java.net/jdk9/docs/technotes/guides/security/jsse/JSSERefGuide.html + sslcontext.init(keyManagers, trustManagers, null); + return sslcontext; + } catch (final NoSuchAlgorithmException | KeyManagementException ex) { + throw new SSLInitializationException(ex.getMessage(), ex); + } + } + + /** + * Insecure trust manager to trust all certs. Should only be used for testing. + */ + private static TrustManager[] trustAllTrustManager() { + return new TrustManager[] { + new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + log.debug(() -> "Accepting a client certificate: " + x509Certificates[0].getSubjectDN()); + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + log.debug(() -> "Accepting a client certificate: " + x509Certificates[0].getSubjectDN()); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } + }; + } + + private SocketConfig buildSocketConfig(AttributeMap standardOptions) { + return SocketConfig.custom() + .setSoKeepAlive(standardOptions.get(SdkHttpConfigurationOption.TCP_KEEPALIVE)) + .setSoTimeout(saturatedCast(standardOptions.get(SdkHttpConfigurationOption.READ_TIMEOUT) + .toMillis()), TimeUnit.MILLISECONDS) + .setTcpNoDelay(true) + .build(); + } + + private Registry createSocketFactoryRegistry(ConnectionSocketFactory sslSocketFactory) { + return RegistryBuilder.create() + .register("http", PlainConnectionSocketFactory.getSocketFactory()) + .register("https", sslSocketFactory) + .build(); + } + } } diff --git a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/Apache5SdkHttpService.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/Apache5SdkHttpService.java new file mode 100644 index 000000000000..b3fad1617efe --- /dev/null +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/Apache5SdkHttpService.java @@ -0,0 +1,31 @@ +/* + * 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 software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpService; + +/** + * Service binding for the Apache5 implementation. + */ +@SdkPublicApi +public class Apache5SdkHttpService implements SdkHttpService { + @Override + public SdkHttpClient.Builder createHttpClientBuilder() { + return Apache5HttpClient.builder(); + } +} diff --git a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/ProxyConfiguration.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/ProxyConfiguration.java new file mode 100644 index 000000000000..14bf452d80a6 --- /dev/null +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/ProxyConfiguration.java @@ -0,0 +1,450 @@ +/* + * 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 software.amazon.awssdk.utils.ProxyConfigProvider.fromSystemEnvironmentSettings; +import static software.amazon.awssdk.utils.StringUtils.isEmpty; + +import java.net.URI; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.utils.ProxyConfigProvider; +import software.amazon.awssdk.utils.ProxySystemSetting; +import software.amazon.awssdk.utils.ToString; +import software.amazon.awssdk.utils.Validate; +import software.amazon.awssdk.utils.builder.CopyableBuilder; +import software.amazon.awssdk.utils.builder.ToCopyableBuilder; + +/** + * Configuration that defines how to communicate via an HTTP or HTTPS proxy. + */ +@SdkPublicApi +public final class ProxyConfiguration implements ToCopyableBuilder { + private final URI endpoint; + private final String username; + private final String password; + private final String ntlmDomain; + private final String ntlmWorkstation; + private final Set nonProxyHosts; + private final Boolean preemptiveBasicAuthenticationEnabled; + private final Boolean useSystemPropertyValues; + private final String host; + private final int port; + private final String scheme; + private final Boolean useEnvironmentVariablesValues; + + /** + * Initialize this configuration. Private to require use of {@link #builder()}. + */ + private ProxyConfiguration(DefaultClientProxyConfigurationBuilder builder) { + this.endpoint = builder.endpoint; + String resolvedScheme = getResolvedScheme(builder); + this.scheme = resolvedScheme; + ProxyConfigProvider proxyConfiguration = fromSystemEnvironmentSettings(builder.useSystemPropertyValues, + builder.useEnvironmentVariableValues, + resolvedScheme); + this.username = resolveUsername(builder, proxyConfiguration); + this.password = resolvePassword(builder, proxyConfiguration); + this.ntlmDomain = builder.ntlmDomain; + this.ntlmWorkstation = builder.ntlmWorkstation; + this.nonProxyHosts = resolveNonProxyHosts(builder, proxyConfiguration); + this.preemptiveBasicAuthenticationEnabled = builder.preemptiveBasicAuthenticationEnabled == null ? Boolean.FALSE : + builder.preemptiveBasicAuthenticationEnabled; + this.useSystemPropertyValues = builder.useSystemPropertyValues; + this.useEnvironmentVariablesValues = builder.useEnvironmentVariableValues; + + if (this.endpoint != null) { + this.host = endpoint.getHost(); + this.port = endpoint.getPort(); + } else { + this.host = proxyConfiguration != null ? proxyConfiguration.host() : null; + this.port = proxyConfiguration != null ? proxyConfiguration.port() : 0; + } + } + + private static String resolvePassword(DefaultClientProxyConfigurationBuilder builder, + ProxyConfigProvider proxyConfiguration) { + return !isEmpty(builder.password) || proxyConfiguration == null ? builder.password : + proxyConfiguration.password().orElseGet(() -> builder.password); + } + + private static String resolveUsername(DefaultClientProxyConfigurationBuilder builder, + ProxyConfigProvider proxyConfiguration) { + return !isEmpty(builder.username) || proxyConfiguration == null ? builder.username : + proxyConfiguration.userName().orElseGet(() -> builder.username); + } + + + private static Set resolveNonProxyHosts(DefaultClientProxyConfigurationBuilder builder, + ProxyConfigProvider proxyConfiguration) { + if (builder.nonProxyHosts != null || proxyConfiguration == null) { + return builder.nonProxyHosts; + } + return proxyConfiguration.nonProxyHosts(); + } + + private String getResolvedScheme(DefaultClientProxyConfigurationBuilder builder) { + return endpoint != null ? endpoint.getScheme() : builder.scheme; + } + + /** + * Returns the proxy host name from the configured endpoint if set, else from the "https.proxyHost" or "http.proxyHost" system + * property, based on the scheme used, if {@link Builder#useSystemPropertyValues(Boolean)} is set to true. + */ + public String host() { + return host; + } + + /** + * Returns the proxy port from the configured endpoint if set, else from the "https.proxyPort" or "http.proxyPort" system + * property, based on the scheme used, if {@link Builder#useSystemPropertyValues(Boolean)} is set to true. + * If no value is found in none of the above options, the default value of 0 is returned. + */ + public int port() { + return port; + } + + /** + * Returns the {@link URI#scheme} from the configured endpoint. Otherwise return null. + */ + public String scheme() { + return scheme; + } + + /** + * The username to use when connecting through a proxy. + * + * @see Builder#password(String) + */ + public String username() { + return username; + } + + /** + * The password to use when connecting through a proxy. + * + * @see Builder#password(String) + */ + public String password() { + + return password; + } + + /** + * For NTLM proxies: The Windows domain name to use when authenticating with the proxy. + * + * @see Builder#ntlmDomain(String) + */ + public String ntlmDomain() { + return ntlmDomain; + } + + /** + * For NTLM proxies: The Windows workstation name to use when authenticating with the proxy. + * + * @see Builder#ntlmWorkstation(String) + */ + public String ntlmWorkstation() { + return ntlmWorkstation; + } + + /** + * The hosts that the client is allowed to access without going through the proxy. + * If the value is not set on the object, the value represent by "http.nonProxyHosts" system property is returned. + * If system property is also not set, an unmodifiable empty set is returned. + * + * @see Builder#nonProxyHosts(Set) + */ + public Set nonProxyHosts() { + return Collections.unmodifiableSet(nonProxyHosts != null ? nonProxyHosts : Collections.emptySet()); + } + + /** + * Whether to attempt to authenticate preemptively against the proxy server using basic authentication. + * + * @see Builder#preemptiveBasicAuthenticationEnabled(Boolean) + */ + public Boolean preemptiveBasicAuthenticationEnabled() { + return preemptiveBasicAuthenticationEnabled; + } + + @Override + public Builder toBuilder() { + return builder() + .endpoint(endpoint) + .username(username) + .password(password) + .ntlmDomain(ntlmDomain) + .ntlmWorkstation(ntlmWorkstation) + .nonProxyHosts(nonProxyHosts) + .preemptiveBasicAuthenticationEnabled(preemptiveBasicAuthenticationEnabled) + .useSystemPropertyValues(useSystemPropertyValues) + .scheme(scheme) + .useEnvironmentVariableValues(useEnvironmentVariablesValues); + } + + /** + * Create a {@link Builder}, used to create a {@link ProxyConfiguration}. + */ + public static Builder builder() { + return new DefaultClientProxyConfigurationBuilder(); + } + + @Override + public String toString() { + return ToString.builder("ProxyConfiguration") + .add("endpoint", endpoint) + .add("username", username) + .add("ntlmDomain", ntlmDomain) + .add("ntlmWorkstation", ntlmWorkstation) + .add("nonProxyHosts", nonProxyHosts) + .add("preemptiveBasicAuthenticationEnabled", preemptiveBasicAuthenticationEnabled) + .add("useSystemPropertyValues", useSystemPropertyValues) + .add("useEnvironmentVariablesValues", useEnvironmentVariablesValues) + .add("scheme", scheme) + .build(); + } + + public String resolveScheme() { + return endpoint != null ? endpoint.getScheme() : scheme; + } + + /** + * A builder for {@link ProxyConfiguration}. + * + *

All implementations of this interface are mutable and not thread safe.

+ */ + public interface Builder extends CopyableBuilder { + + /** + * Configure the endpoint of the proxy server that the SDK should connect through. Currently, the endpoint is limited to + * a host and port. Any other URI components will result in an exception being raised. + */ + Builder endpoint(URI endpoint); + + /** + * Configure the username to use when connecting through a proxy. + */ + Builder username(String username); + + /** + * Configure the password to use when connecting through a proxy. + */ + Builder password(String password); + + /** + * For NTLM proxies: Configure the Windows domain name to use when authenticating with the proxy. + */ + Builder ntlmDomain(String proxyDomain); + + /** + * For NTLM proxies: Configure the Windows workstation name to use when authenticating with the proxy. + */ + Builder ntlmWorkstation(String proxyWorkstation); + + /** + * Configure the hosts that the client is allowed to access without going through the proxy. + */ + Builder nonProxyHosts(Set nonProxyHosts); + + /** + * Add a host that the client is allowed to access without going through the proxy. + * + * @see ProxyConfiguration#nonProxyHosts() + */ + Builder addNonProxyHost(String nonProxyHost); + + /** + * Configure whether to attempt to authenticate pre-emptively against the proxy server using basic authentication. + */ + Builder preemptiveBasicAuthenticationEnabled(Boolean preemptiveBasicAuthenticationEnabled); + + /** + * Option whether to use system property values from {@link ProxySystemSetting} if any of the config options are missing. + *

+ * This value is set to "true" by default which means SDK will automatically use system property values for options that + * are not provided during building the {@link ProxyConfiguration} object. To disable this behavior, set this value to + * "false".It is important to note that when this property is set to "true," all proxy settings will exclusively originate + * from system properties, and no partial settings will be obtained from EnvironmentVariableValues. + */ + Builder useSystemPropertyValues(Boolean useSystemPropertyValues); + + /** + * Option whether to use environment variable values for proxy configuration if any of the config options are missing. + *

+ * This value is set to "true" by default, which means the SDK will automatically use environment variable values for + * proxy configuration options that are not provided during the building of the {@link ProxyConfiguration} object. To + * disable this behavior, set this value to "false". It is important to note that when this property is set to "true," all + * proxy settings will exclusively originate from environment variableValues, and no partial settings will be obtained + * from SystemPropertyValues. + *

Comma-separated host names in the NO_PROXY environment variable indicate multiple hosts to exclude from + * proxy settings. + * + * @param useEnvironmentVariableValues The option whether to use environment variable values. + * @return This object for method chaining. + */ + Builder useEnvironmentVariableValues(Boolean useEnvironmentVariableValues); + + /** + * The HTTP scheme to use for connecting to the proxy. Valid values are {@code http} and {@code https}. + *

+ * The client defaults to {@code http} if none is given. + * + * @param scheme The proxy scheme. + * @return This object for method chaining. + */ + Builder scheme(String scheme); + + } + + /** + * An SDK-internal implementation of {@link Builder}. + */ + private static final class DefaultClientProxyConfigurationBuilder implements Builder { + + private URI endpoint; + private String username; + private String password; + private String ntlmDomain; + private String ntlmWorkstation; + private Set nonProxyHosts; + private Boolean preemptiveBasicAuthenticationEnabled; + private Boolean useSystemPropertyValues = Boolean.TRUE; + private Boolean useEnvironmentVariableValues = Boolean.TRUE; + private String scheme = "http"; + + @Override + public Builder endpoint(URI endpoint) { + if (endpoint != null) { + Validate.isTrue(isEmpty(endpoint.getUserInfo()), "Proxy endpoint user info is not supported."); + Validate.isTrue(isEmpty(endpoint.getPath()), "Proxy endpoint path is not supported."); + Validate.isTrue(isEmpty(endpoint.getQuery()), "Proxy endpoint query is not supported."); + Validate.isTrue(isEmpty(endpoint.getFragment()), "Proxy endpoint fragment is not supported."); + } + + this.endpoint = endpoint; + return this; + } + + public void setEndpoint(URI endpoint) { + endpoint(endpoint); + } + + @Override + public Builder username(String username) { + this.username = username; + return this; + } + + public void setUsername(String username) { + username(username); + } + + @Override + public Builder password(String password) { + this.password = password; + return this; + } + + public void setPassword(String password) { + password(password); + } + + @Override + public Builder ntlmDomain(String proxyDomain) { + this.ntlmDomain = proxyDomain; + return this; + } + + public void setNtlmDomain(String ntlmDomain) { + ntlmDomain(ntlmDomain); + } + + @Override + public Builder ntlmWorkstation(String proxyWorkstation) { + this.ntlmWorkstation = proxyWorkstation; + return this; + } + + public void setNtlmWorkstation(String ntlmWorkstation) { + ntlmWorkstation(ntlmWorkstation); + } + + @Override + public Builder nonProxyHosts(Set nonProxyHosts) { + this.nonProxyHosts = nonProxyHosts != null ? new HashSet<>(nonProxyHosts) : null; + return this; + } + + @Override + public Builder addNonProxyHost(String nonProxyHost) { + if (this.nonProxyHosts == null) { + this.nonProxyHosts = new HashSet<>(); + } + this.nonProxyHosts.add(nonProxyHost); + return this; + } + + public void setNonProxyHosts(Set nonProxyHosts) { + nonProxyHosts(nonProxyHosts); + } + + @Override + public Builder preemptiveBasicAuthenticationEnabled(Boolean preemptiveBasicAuthenticationEnabled) { + this.preemptiveBasicAuthenticationEnabled = preemptiveBasicAuthenticationEnabled; + return this; + } + + public void setPreemptiveBasicAuthenticationEnabled(Boolean preemptiveBasicAuthenticationEnabled) { + preemptiveBasicAuthenticationEnabled(preemptiveBasicAuthenticationEnabled); + } + + @Override + public Builder useSystemPropertyValues(Boolean useSystemPropertyValues) { + this.useSystemPropertyValues = useSystemPropertyValues; + return this; + } + + public void setUseSystemPropertyValues(Boolean useSystemPropertyValues) { + useSystemPropertyValues(useSystemPropertyValues); + } + + + @Override + public Builder useEnvironmentVariableValues(Boolean useEnvironmentVariableValues) { + this.useEnvironmentVariableValues = useEnvironmentVariableValues; + return this; + } + + @Override + public Builder scheme(String scheme) { + this.scheme = scheme; + return this; + } + + public void setuseEnvironmentVariableValues(Boolean useEnvironmentVariableValues) { + useEnvironmentVariableValues(useEnvironmentVariableValues); + } + + @Override + public ProxyConfiguration build() { + return new ProxyConfiguration(this); + } + } + +} diff --git a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/Apache5HttpRequestConfig.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/Apache5HttpRequestConfig.java new file mode 100644 index 000000000000..a494cfe220da --- /dev/null +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/Apache5HttpRequestConfig.java @@ -0,0 +1,130 @@ +/* + * 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; + +import java.net.InetAddress; +import java.time.Duration; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.http.apache5.ProxyConfiguration; + +/** + * Configuration needed when building an Apache request. Note that at this time, we only support client level configuration so + * all of these settings are supplied when creating the client. + */ +@SdkInternalApi +public final class Apache5HttpRequestConfig { + + private final Duration socketTimeout; + private final Duration connectionTimeout; + private final Duration connectionAcquireTimeout; + private final InetAddress localAddress; + private final boolean expectContinueEnabled; + private final ProxyConfiguration proxyConfiguration; + + private Apache5HttpRequestConfig(Builder builder) { + this.socketTimeout = builder.socketTimeout; + this.connectionTimeout = builder.connectionTimeout; + this.connectionAcquireTimeout = builder.connectionAcquireTimeout; + this.localAddress = builder.localAddress; + this.expectContinueEnabled = builder.expectContinueEnabled; + this.proxyConfiguration = builder.proxyConfiguration; + } + + public Duration socketTimeout() { + return socketTimeout; + } + + public Duration connectionTimeout() { + return connectionTimeout; + } + + public Duration connectionAcquireTimeout() { + return connectionAcquireTimeout; + } + + public InetAddress localAddress() { + return localAddress; + } + + public boolean expectContinueEnabled() { + return expectContinueEnabled; + } + + public ProxyConfiguration proxyConfiguration() { + return proxyConfiguration; + } + + /** + * @return Builder instance to construct a {@link Apache5HttpRequestConfig}. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for a {@link Apache5HttpRequestConfig}. + */ + public static final class Builder { + + private Duration socketTimeout; + private Duration connectionTimeout; + private Duration connectionAcquireTimeout; + private InetAddress localAddress; + private boolean expectContinueEnabled; + private ProxyConfiguration proxyConfiguration; + + private Builder() { + } + + public Builder socketTimeout(Duration socketTimeout) { + this.socketTimeout = socketTimeout; + return this; + } + + public Builder connectionTimeout(Duration connectionTimeout) { + this.connectionTimeout = connectionTimeout; + return this; + } + + public Builder connectionAcquireTimeout(Duration connectionAcquireTimeout) { + this.connectionAcquireTimeout = connectionAcquireTimeout; + return this; + } + + public Builder localAddress(InetAddress localAddress) { + this.localAddress = localAddress; + return this; + } + + public Builder expectContinueEnabled(boolean expectContinueEnabled) { + this.expectContinueEnabled = expectContinueEnabled; + return this; + } + + public Builder proxyConfiguration(ProxyConfiguration proxyConfiguration) { + this.proxyConfiguration = proxyConfiguration; + return this; + } + + /** + * @return An immutable {@link Apache5HttpRequestConfig} object. + */ + public Apache5HttpRequestConfig build() { + return new Apache5HttpRequestConfig(this); + } + } + +} diff --git a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/DefaultConfiguration.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/DefaultConfiguration.java new file mode 100644 index 000000000000..0128d6f18911 --- /dev/null +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/DefaultConfiguration.java @@ -0,0 +1,29 @@ +/* + * 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; + +import software.amazon.awssdk.annotations.SdkInternalApi; + +/** + * Default configuration values. + */ +@SdkInternalApi +public final class DefaultConfiguration { + public static final Boolean EXPECT_CONTINUE_ENABLED = Boolean.TRUE; + + private DefaultConfiguration() { + } +} diff --git a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/RepeatableInputStreamRequestEntity.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/RepeatableInputStreamRequestEntity.java new file mode 100644 index 000000000000..cf6d308ae2d1 --- /dev/null +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/RepeatableInputStreamRequestEntity.java @@ -0,0 +1,166 @@ +/* + * 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; + +import static software.amazon.awssdk.http.Header.CHUNKED; +import static software.amazon.awssdk.http.Header.TRANSFER_ENCODING; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Optional; +import org.apache.hc.core5.http.io.entity.BasicHttpEntity; +import org.apache.hc.core5.http.io.entity.InputStreamEntity; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.http.ContentStreamProvider; +import software.amazon.awssdk.http.HttpExecuteRequest; +import software.amazon.awssdk.utils.Logger; + +/** + * Custom implementation of {@link org.apache.hc.core5.http.HttpEntity} that delegates to an + * {@link InputStreamEntity}, with the one notable difference, that if + * the underlying InputStream supports being reset, this RequestEntity will + * report that it is repeatable and will reset the stream on all subsequent + * attempts to write out the request. + */ +@SdkInternalApi +public class RepeatableInputStreamRequestEntity extends BasicHttpEntity { + + private static final Logger log = Logger.loggerFor(RepeatableInputStreamRequestEntity.class); + + /** + * True if the request entity hasn't been written out yet + */ + private boolean firstAttempt = true; + + /** + * True if the "Transfer-Encoding:chunked" header is present + */ + private boolean isChunked; + + /** + * The underlying InputStreamEntity being delegated to + */ + private InputStreamEntity inputStreamRequestEntity; + + /** + * The InputStream containing the content to write out + */ + private InputStream content; + + /** + * Record the original exception if we do attempt a retry, so that if the + * retry fails, we can report the original exception. Otherwise, we're most + * likely masking the real exception with an error about not being able to + * reset far enough back in the input stream. + */ + private IOException originalException; + + + /** + * Creates a new RepeatableInputStreamRequestEntity using the information + * from the specified request. If the input stream containing the request's + * contents is repeatable, then this RequestEntity will report as being + * repeatable. + * + * @param request The details of the request being written out (content type, + * content length, and content). + */ + public RepeatableInputStreamRequestEntity(final HttpExecuteRequest request) { + isChunked = request.httpRequest().matchingHeaders(TRANSFER_ENCODING).contains(CHUNKED); + setChunked(isChunked); + + /* + * If we don't specify a content length when we instantiate our + * InputStreamRequestEntity, then HttpClient will attempt to + * buffer the entire stream contents into memory to determine + * the content length. + */ + long contentLength = request.httpRequest().firstMatchingHeader("Content-Length") + .map(this::parseContentLength) + .orElse(-1L); + + content = getContent(request.contentStreamProvider()); + // TODO v2 MetricInputStreamEntity + inputStreamRequestEntity = new InputStreamEntity(content, contentLength); + setContent(content); + setContentLength(contentLength); + + request.httpRequest().firstMatchingHeader("Content-Type").ifPresent(contentType -> { + inputStreamRequestEntity.setContentType(contentType); + setContentType(contentType); + }); + } + + private long parseContentLength(String contentLength) { + try { + return Long.parseLong(contentLength); + } catch (NumberFormatException nfe) { + log.warn(() -> "Unable to parse content length from request. Buffering contents in memory."); + return -1; + } + } + + /** + * @return The request content input stream or an empty input stream if there is no content. + */ + private InputStream getContent(Optional contentStreamProvider) { + return contentStreamProvider.map(ContentStreamProvider::newStream).orElseGet(() -> new ByteArrayInputStream(new byte[0])); + } + + @Override + public boolean isChunked() { + return isChunked; + } + + /** + * Returns true if the underlying InputStream supports marking/reseting or + * if the underlying InputStreamRequestEntity is repeatable. + */ + @Override + public boolean isRepeatable() { + return content.markSupported() || inputStreamRequestEntity.isRepeatable(); + } + + /** + * Resets the underlying InputStream if this isn't the first attempt to + * write out the request, otherwise simply delegates to + * InputStreamRequestEntity to write out the data. + *

+ * If an error is encountered the first time we try to write the request + * entity, we remember the original exception, and report that as the root + * cause if we continue to encounter errors, rather than masking the + * original error. + */ + @Override + public void writeTo(OutputStream output) throws IOException { + try { + if (!firstAttempt && isRepeatable()) { + content.reset(); + } + + firstAttempt = false; + inputStreamRequestEntity.writeTo(output); + } catch (IOException ioe) { + if (originalException == null) { + originalException = ioe; + } + throw originalException; + } + } + +} diff --git a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/SdkProxyRoutePlanner.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/SdkProxyRoutePlanner.java new file mode 100644 index 000000000000..64cae495fba2 --- /dev/null +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/SdkProxyRoutePlanner.java @@ -0,0 +1,64 @@ +/* + * 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; + +import static software.amazon.awssdk.utils.StringUtils.lowerCase; + +import java.util.Set; +import org.apache.hc.client5.http.impl.DefaultSchemePortResolver; +import org.apache.hc.client5.http.impl.routing.DefaultRoutePlanner; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.protocol.HttpContext; +import software.amazon.awssdk.annotations.SdkInternalApi; + +/** + * SdkProxyRoutePlanner delegates a Proxy Route Planner from the settings instead of the + * system properties. It will use the proxy created from proxyHost and proxyPort and + * filter the hosts who matches nonProxyHosts pattern. + */ +@SdkInternalApi +public class SdkProxyRoutePlanner extends DefaultRoutePlanner { + + private HttpHost proxy; + private Set hostPatterns; + + public SdkProxyRoutePlanner(String proxyHost, int proxyPort, String proxyProtocol, Set nonProxyHosts) { + super(DefaultSchemePortResolver.INSTANCE); + proxy = new HttpHost(proxyProtocol, proxyHost, proxyPort); + this.hostPatterns = nonProxyHosts; + } + + private boolean doesTargetMatchNonProxyHosts(HttpHost target) { + if (hostPatterns == null) { + return false; + } + String targetHost = lowerCase(target.getHostName()); + for (String pattern : hostPatterns) { + if (targetHost.matches(pattern)) { + return true; + } + } + return false; + } + + @Override + protected HttpHost determineProxy(HttpHost target, HttpContext context) throws HttpException { + return doesTargetMatchNonProxyHosts(target) ? null : proxy; + } + +} 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 new file mode 100644 index 000000000000..7f1f8b06e81c --- /dev/null +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/ClientConnectionManagerFactory.java @@ -0,0 +1,114 @@ +/* + * 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.conn; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import org.apache.hc.client5.http.ConnectionRequest; +import org.apache.hc.client5.http.HttpRoute; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.core5.http.io.HttpClientConnection; +import org.apache.hc.core5.http.protocol.HttpContext; +import software.amazon.awssdk.annotations.SdkInternalApi; + +@SdkInternalApi +public final class ClientConnectionManagerFactory { + + private ClientConnectionManagerFactory() { + } + + /** + * Returns a wrapped instance of {@link HttpClientConnectionManager} + * to capture the necessary performance metrics. + * + * @param orig the target instance to be wrapped + */ + public static HttpClientConnectionManager wrap(HttpClientConnectionManager orig) { + if (orig instanceof DelegatingHttpClientConnectionManager) { + throw new IllegalArgumentException(); + } + return new InstrumentedHttpClientConnectionManager(orig); + } + + /** + * Further wraps {@link ConnectionRequest} to capture performance metrics. + */ + private static class InstrumentedHttpClientConnectionManager extends DelegatingHttpClientConnectionManager { + + private InstrumentedHttpClientConnectionManager(HttpClientConnectionManager delegate) { + super(delegate); + } + + @Override + public ConnectionRequest requestConnection(HttpRoute route, Object state) { + ConnectionRequest connectionRequest = super.requestConnection(route, state); + return ClientConnectionRequestFactory.wrap(connectionRequest); + } + } + + /** + * Delegates all methods to {@link HttpClientConnectionManager}. Subclasses can override select methods to change behavior. + */ + private static class DelegatingHttpClientConnectionManager implements HttpClientConnectionManager { + + private final HttpClientConnectionManager delegate; + + protected DelegatingHttpClientConnectionManager(HttpClientConnectionManager delegate) { + this.delegate = delegate; + } + + @Override + public ConnectionRequest requestConnection(HttpRoute route, Object state) { + return delegate.requestConnection(route, state); + } + + @Override + public void releaseConnection(HttpClientConnection conn, Object newState, long validDuration, TimeUnit timeUnit) { + delegate.releaseConnection(conn, newState, validDuration, timeUnit); + } + + @Override + public void connect(HttpClientConnection conn, HttpRoute route, int connectTimeout, HttpContext context) + throws IOException { + delegate.connect(conn, route, connectTimeout, context); + } + + @Override + public void upgrade(HttpClientConnection conn, HttpRoute route, HttpContext context) throws IOException { + delegate.upgrade(conn, route, context); + } + + @Override + public void routeComplete(HttpClientConnection conn, HttpRoute route, HttpContext context) throws IOException { + delegate.routeComplete(conn, route, context); + } + + @Override + public void closeIdleConnections(long idletime, TimeUnit timeUnit) { + delegate.closeIdleConnections(idletime, timeUnit); + } + + @Override + public void closeExpiredConnections() { + delegate.closeExpiredConnections(); + } + + @Override + public void shutdown() { + delegate.shutdown(); + } + } +} 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 new file mode 100644 index 000000000000..6a6892ce3e2a --- /dev/null +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/ClientConnectionRequestFactory.java @@ -0,0 +1,100 @@ +/* + * 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.conn; + +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import org.apache.hc.client5.http.ConnectionPoolTimeoutException; +import org.apache.hc.client5.http.ConnectionRequest; +import org.apache.hc.core5.http.io.HttpClientConnection; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.http.HttpMetric; +import software.amazon.awssdk.http.apache5.Apache5HttpClient; +import software.amazon.awssdk.metrics.MetricCollector; + +@SdkInternalApi +public final class ClientConnectionRequestFactory { + + /** + * {@link ThreadLocal}, request-level {@link MetricCollector}, set and removed by {@link Apache5HttpClient}. + */ + public static final ThreadLocal THREAD_LOCAL_REQUEST_METRIC_COLLECTOR = new ThreadLocal<>(); + + private ClientConnectionRequestFactory() { + } + + /** + * Returns a wrapped instance of {@link ConnectionRequest} + * to capture the necessary performance metrics. + * + * @param orig the target instance to be wrapped + */ + static ConnectionRequest wrap(ConnectionRequest orig) { + if (orig instanceof DelegatingConnectionRequest) { + throw new IllegalArgumentException(); + } + return new InstrumentedConnectionRequest(orig); + } + + /** + * Measures the latency of {@link ConnectionRequest#get(long, TimeUnit)}. + */ + private static class InstrumentedConnectionRequest extends DelegatingConnectionRequest { + + private InstrumentedConnectionRequest(ConnectionRequest delegate) { + super(delegate); + } + + @Override + public HttpClientConnection get(long timeout, TimeUnit timeUnit) throws InterruptedException, ExecutionException, + ConnectionPoolTimeoutException { + Instant startTime = Instant.now(); + try { + return super.get(timeout, timeUnit); + } finally { + Duration elapsed = Duration.between(startTime, Instant.now()); + MetricCollector metricCollector = THREAD_LOCAL_REQUEST_METRIC_COLLECTOR.get(); + metricCollector.reportMetric(HttpMetric.CONCURRENCY_ACQUIRE_DURATION, elapsed); + } + } + } + + /** + * Delegates all methods to {@link ConnectionRequest}. Subclasses can override select methods to change behavior. + */ + private static class DelegatingConnectionRequest implements ConnectionRequest { + + private final ConnectionRequest delegate; + + private DelegatingConnectionRequest(ConnectionRequest delegate) { + this.delegate = delegate; + } + + @Override + public HttpClientConnection get(long timeout, TimeUnit timeUnit) + throws InterruptedException, ExecutionException, ConnectionPoolTimeoutException { + return delegate.get(timeout, timeUnit); + } + + @Override + public boolean cancel() { + return delegate.cancel(); + } + } +} diff --git a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/IdleConnectionReaper.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/IdleConnectionReaper.java new file mode 100644 index 000000000000..8362bb7bcf95 --- /dev/null +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/IdleConnectionReaper.java @@ -0,0 +1,171 @@ +/* + * 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.conn; + +import java.time.Duration; +import java.util.Collections; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.SdkTestInternalApi; + +/** + * Manages the reaping of idle connections. + */ +@SdkInternalApi +public final class IdleConnectionReaper { + private static final Logger log = LoggerFactory.getLogger(IdleConnectionReaper.class); + + private static final IdleConnectionReaper INSTANCE = new IdleConnectionReaper(); + + private final Map connectionManagers; + + private final Supplier executorServiceSupplier; + + private final long sleepPeriod; + + private volatile ExecutorService exec; + + private volatile ReaperTask reaperTask; + + private IdleConnectionReaper() { + this.connectionManagers = Collections.synchronizedMap(new WeakHashMap<>()); + + this.executorServiceSupplier = () -> { + ExecutorService e = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "idle-connection-reaper"); + t.setDaemon(true); + return t; + }); + return e; + }; + + this.sleepPeriod = Duration.ofMinutes(1).toMillis(); + } + + @SdkTestInternalApi + IdleConnectionReaper(Map connectionManagers, + Supplier executorServiceSupplier, + long sleepPeriod) { + + this.connectionManagers = connectionManagers; + this.executorServiceSupplier = executorServiceSupplier; + this.sleepPeriod = sleepPeriod; + } + + /** + * Register the connection manager with this reaper. + * + * @param manager The connection manager. + * @param maxIdleTime The maximum time connections in the connection manager are to remain idle before being reaped. + * @return {@code true} If the connection manager was not previously registered with this reaper, {@code false} + * otherwise. + */ + public synchronized boolean registerConnectionManager(HttpClientConnectionManager manager, long maxIdleTime) { + boolean notPreviouslyRegistered = connectionManagers.put(manager, maxIdleTime) == null; + setupExecutorIfNecessary(); + return notPreviouslyRegistered; + } + + /** + * Deregister this connection manager with this reaper. + * + * @param manager The connection manager. + * @return {@code true} If this connection manager was previously registered with this reaper and it was removed, {@code + * false} otherwise. + */ + public synchronized boolean deregisterConnectionManager(HttpClientConnectionManager manager) { + boolean wasRemoved = connectionManagers.remove(manager) != null; + cleanupExecutorIfNecessary(); + return wasRemoved; + } + + /** + * @return The singleton instance of this class. + */ + public static IdleConnectionReaper getInstance() { + return INSTANCE; + } + + private void setupExecutorIfNecessary() { + if (exec != null) { + return; + } + + ExecutorService e = executorServiceSupplier.get(); + + this.reaperTask = new ReaperTask(connectionManagers, sleepPeriod); + + e.execute(this.reaperTask); + + exec = e; + } + + private void cleanupExecutorIfNecessary() { + if (exec == null || !connectionManagers.isEmpty()) { + return; + } + + reaperTask.stop(); + reaperTask = null; + exec.shutdownNow(); + exec = null; + } + + private static final class ReaperTask implements Runnable { + private final Map connectionManagers; + private final long sleepPeriod; + + private volatile boolean stopping = false; + + private ReaperTask(Map connectionManagers, + long sleepPeriod) { + this.connectionManagers = connectionManagers; + this.sleepPeriod = sleepPeriod; + } + + @Override + public void run() { + while (!stopping) { + try { + Thread.sleep(sleepPeriod); + + for (Map.Entry entry : connectionManagers.entrySet()) { + try { + entry.getKey().closeIdleConnections(entry.getValue(), TimeUnit.MILLISECONDS); + } catch (Exception t) { + log.warn("Unable to close idle connections", t); + } + } + } catch (Throwable t) { + log.debug("Reaper thread: ", t); + } + } + log.debug("Shutting down reaper thread."); + } + + private void stop() { + stopping = true; + } + } +} diff --git a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/SdkConnectionKeepAliveStrategy.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/SdkConnectionKeepAliveStrategy.java new file mode 100644 index 000000000000..16a883cd9d81 --- /dev/null +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/SdkConnectionKeepAliveStrategy.java @@ -0,0 +1,59 @@ +/* + * 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.conn; + +import org.apache.hc.client5.http.ConnectionKeepAliveStrategy; +import org.apache.hc.client5.http.impl.DefaultConnectionKeepAliveStrategy; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.protocol.HttpContext; +import software.amazon.awssdk.annotations.SdkInternalApi; + +/** + * The AWS SDK for Java's implementation of the + * {@code ConnectionKeepAliveStrategy} interface. Allows a user-configurable + * maximum idle time for connections. + */ +@SdkInternalApi +public class SdkConnectionKeepAliveStrategy implements ConnectionKeepAliveStrategy { + + private final long maxIdleTime; + + /** + * @param maxIdleTime the maximum time a connection may be idle + */ + public SdkConnectionKeepAliveStrategy(long maxIdleTime) { + this.maxIdleTime = maxIdleTime; + } + + @Override + public long getKeepAliveDuration( + HttpResponse response, + HttpContext context) { + + // If there's a Keep-Alive timeout directive in the response and it's + // shorter than our configured max, honor that. Otherwise go with the + // configured maximum. + + long duration = DefaultConnectionKeepAliveStrategy.INSTANCE + .getKeepAliveDuration(response, context); + + if (0 < duration && duration < maxIdleTime) { + return duration; + } + + return maxIdleTime; + } +} 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 new file mode 100644 index 000000000000..8ff4273ec573 --- /dev/null +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/SdkTlsSocketFactory.java @@ -0,0 +1,72 @@ +/* + * 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.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.core5.http.protocol.HttpContext; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.http.apache5.internal.net.InputShutdownCheckingSslSocket; +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 { + + private static final Logger log = Logger.loggerFor(SdkTlsSocketFactory.class); + + public SdkTlsSocketFactory(SSLContext sslContext, HostnameVerifier hostnameVerifier) { + super(sslContext, hostnameVerifier); + if (sslContext == null) { + throw new IllegalArgumentException( + "sslContext must not be null. " + "Use SSLContext.getDefault() if you are unsure."); + } + } + + @Override + protected final void prepareSocket(SSLSocket socket) { + log.debug(() -> String.format("socket.getSupportedProtocols(): %s, socket.getEnabledProtocols(): %s", + Arrays.toString(socket.getSupportedProtocols()), + Arrays.toString(socket.getEnabledProtocols()))); + } + + @Override + public Socket connectSocket(int 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())); + + Socket connectedSocket = super.connectSocket(connectTimeout, socket, host, remoteAddress, localAddress, context); + + if (connectedSocket instanceof SSLSocket) { + return new InputShutdownCheckingSslSocket(new SdkSslSocket((SSLSocket) connectedSocket)); + } + + return new SdkSocket(connectedSocket); + } + +} diff --git a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/Wrapped.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/Wrapped.java new file mode 100644 index 000000000000..28ad6edce113 --- /dev/null +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/conn/Wrapped.java @@ -0,0 +1,25 @@ +/* + * 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.conn; + +import software.amazon.awssdk.annotations.SdkInternalApi; + +/** + * An internal marker interface to defend against accidental recursive wrappings. + */ +@SdkInternalApi +public interface Wrapped { +} diff --git a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/impl/Apache5HttpRequestFactory.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/impl/Apache5HttpRequestFactory.java new file mode 100644 index 000000000000..340751c0bc91 --- /dev/null +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/impl/Apache5HttpRequestFactory.java @@ -0,0 +1,198 @@ +/* + * 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.impl; + +import static software.amazon.awssdk.utils.NumericUtils.saturatedCast; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import org.apache.hc.client5.http.classic.methods.HttpDelete; +import org.apache.hc.client5.http.classic.methods.HttpEntityEnclosingRequestBase; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.classic.methods.HttpHead; +import org.apache.hc.client5.http.classic.methods.HttpOptions; +import org.apache.hc.client5.http.classic.methods.HttpPatch; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.classic.methods.HttpPut; +import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHeaders; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.http.HttpExecuteRequest; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.apache5.internal.Apache5HttpRequestConfig; +import software.amazon.awssdk.http.apache5.internal.RepeatableInputStreamRequestEntity; +import software.amazon.awssdk.http.apache5.internal.utils.Apache5Utils; +import software.amazon.awssdk.utils.StringUtils; +import software.amazon.awssdk.utils.http.SdkHttpUtils; + +/** + * Responsible for creating Apache HttpClient 4 request objects. + */ +@SdkInternalApi +public class Apache5HttpRequestFactory { + + private static final List IGNORE_HEADERS = Arrays.asList(HttpHeaders.CONTENT_LENGTH, HttpHeaders.HOST, + HttpHeaders.TRANSFER_ENCODING); + + public HttpUriRequestBase create(final HttpExecuteRequest request, final Apache5HttpRequestConfig requestConfig) { + HttpUriRequestBase base = createApacheRequest(request, sanitizeUri(request.httpRequest())); + addHeadersToRequest(base, request.httpRequest()); + addRequestConfig(base, request.httpRequest(), requestConfig); + return base; + } + + //TODO : check if this is still valid + /** + * + * The Apache HTTP client doesn't allow consecutive slashes in the URI. For S3 + * and other AWS services, this is allowed and required. This methods replaces + * any occurrence of "//" in the URI path with "/%2F". + * + * @see SdkHttpRequest#getUri() + * @param request The existing request + * @return a new String containing the modified URI + */ + private URI sanitizeUri(SdkHttpRequest request) { + String path = request.encodedPath(); + if (path.contains("//")) { + int port = request.port(); + String protocol = request.protocol(); + String newPath = StringUtils.replace(path, "//", "/%2F"); + String encodedQueryString = request.encodedQueryParameters().map(value -> "?" + value).orElse(""); + + // Do not include the port in the URI when using the default port for the protocol. + String portString = SdkHttpUtils.isUsingStandardPort(protocol, port) ? + "" : ":" + port; + + return URI.create(protocol + "://" + request.host() + portString + newPath + encodedQueryString); + } + + return request.getUri(); + } + + private void addRequestConfig(final HttpUriRequestBase base, + final SdkHttpRequest request, + final Apache5HttpRequestConfig requestConfig) { + int connectTimeout = saturatedCast(requestConfig.connectionTimeout().toMillis()); + int connectAcquireTimeout = saturatedCast(requestConfig.connectionAcquireTimeout().toMillis()); + RequestConfig.Builder requestConfigBuilder = RequestConfig + .custom() + .setConnectionRequestTimeout(connectAcquireTimeout, TimeUnit.MILLISECONDS) + .setConnectTimeout(connectTimeout, TimeUnit.MILLISECONDS) + .setResponseTimeout(saturatedCast(requestConfig.socketTimeout().toMillis()), TimeUnit.MILLISECONDS) + .setLocalAddress(requestConfig.localAddress()); + + Apache5Utils.disableNormalizeUri(requestConfigBuilder); + + /* + * Enable 100-continue support for PUT operations, since this is + * where we're potentially uploading large amounts of data and want + * to find out as early as possible if an operation will fail. We + * don't want to do this for all operations since it will cause + * extra latency in the network interaction. + */ + if (SdkHttpMethod.PUT == request.method() && requestConfig.expectContinueEnabled()) { + requestConfigBuilder.setExpectContinueEnabled(true); + } + + base.setConfig(requestConfigBuilder.build()); + } + + + private HttpUriRequestBase createApacheRequest(HttpExecuteRequest request, URI uri) { + switch (request.httpRequest().method()) { + case HEAD: + return new HttpHead(uri); + case GET: + return new HttpGet(uri); + case DELETE: + return new HttpDelete(uri); + case OPTIONS: + return new HttpOptions(uri); + case PATCH: + return wrapEntity(request, new HttpPatch(uri)); + case POST: + return wrapEntity(request, new HttpPost(uri)); + case PUT: + return wrapEntity(request, new HttpPut(uri)); + default: + throw new RuntimeException("Unknown HTTP method name: " + request.httpRequest().method()); + } + } + + private HttpUriRequestBase wrapEntity(HttpExecuteRequest request, + HttpEntityEnclosingRequestBase entityEnclosingRequest) { + + /* + * We should never reuse the entity of the previous request, since + * reading from the buffered entity will bypass reading from the + * original request content. And if the content contains InputStream + * wrappers that were added for validation-purpose (e.g. + * Md5DigestCalculationInputStream), these wrappers would never be + * read and updated again after AmazonHttpClient resets it in + * preparation for the retry. Eventually, these wrappers would + * return incorrect validation result. + */ + if (request.contentStreamProvider().isPresent()) { + HttpEntity entity = new RepeatableInputStreamRequestEntity(request); + if (!request.httpRequest().firstMatchingHeader(HttpHeaders.CONTENT_LENGTH).isPresent() && !entity.isChunked()) { + entity = Apache5Utils.newBufferedHttpEntity(entity); + } + entityEnclosingRequest.setEntity(entity); + } + + return entityEnclosingRequest; + } + + /** + * Configures the headers in the specified Apache5 HTTP request. + */ + private void addHeadersToRequest(HttpUriRequestBase httpRequest, SdkHttpRequest request) { + httpRequest.addHeader(HttpHeaders.HOST, getHostHeaderValue(request)); + + // Copy over any other headers already in our request + request.forEachHeader((name, value) -> { + // HttpClient4 fills in the Content-Length header and complains if + // it's already present, so we skip it here. We also skip the Host + // header to avoid sending it twice, which will interfere with some + // signing schemes. + if (IGNORE_HEADERS.stream().noneMatch(name::equalsIgnoreCase)) { + for (String headerValue : value) { + httpRequest.addHeader(name, headerValue); + } + } + }); + } + + private String getHostHeaderValue(SdkHttpRequest request) { + // Respect any user-specified Host header when present + Optional existingHostHeader = request.firstMatchingHeader(HttpHeaders.HOST); + if (existingHostHeader.isPresent()) { + return existingHostHeader.get(); + } + // Apache doesn't allow us to include the port in the host header if it's a standard port for that protocol. For that + // reason, we don't include the port when we sign the message. See {@link SdkHttpRequest#port()}. + return !SdkHttpUtils.isUsingStandardPort(request.protocol(), request.port()) + ? request.host() + ":" + request.port() + : request.host(); + } +} diff --git a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/impl/Apache5SdkHttpClient.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/impl/Apache5SdkHttpClient.java new file mode 100644 index 000000000000..ea47196e60bb --- /dev/null +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/impl/Apache5SdkHttpClient.java @@ -0,0 +1,114 @@ +/* + * 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.impl; + +import java.io.IOException; +import org.apache.hc.client5.http.ClientConnectionManager; +import org.apache.hc.client5.http.ResponseHandler; +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.classic.methods.HttpUriRequest; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.params.HttpParams; +import org.apache.hc.core5.http.protocol.HttpContext; +import software.amazon.awssdk.annotations.SdkInternalApi; + +/** + * An instance of {@link ConnectionManagerAwareHttpClient} that delegates all the requests to the given http client. + */ +@SdkInternalApi +public class Apache5SdkHttpClient implements ConnectionManagerAwareHttpClient { + + private final HttpClient delegate; + + private final HttpClientConnectionManager cm; + + public Apache5SdkHttpClient(final HttpClient delegate, + final HttpClientConnectionManager cm) { + if (delegate == null) { + throw new IllegalArgumentException("delegate " + + "cannot be null"); + } + if (cm == null) { + throw new IllegalArgumentException("connection manager " + + "cannot be null"); + } + this.delegate = delegate; + this.cm = cm; + } + + @Override + public HttpParams getParams() { + return delegate.getParams(); + } + + @Override + public ClientConnectionManager getConnectionManager() { + return delegate.getConnectionManager(); + } + + @Override + public HttpResponse execute(HttpUriRequest request) throws IOException { + return delegate.execute(request); + } + + @Override + public HttpResponse execute(HttpUriRequest request, HttpContext context) throws IOException { + return delegate.execute(request, context); + } + + @Override + public HttpResponse execute(HttpHost target, HttpRequest request) throws IOException { + return delegate.execute(target, request); + } + + @Override + public HttpResponse execute(HttpHost target, HttpRequest request, HttpContext context) throws IOException { + return delegate.execute(target, request, context); + } + + @Override + public T execute(HttpUriRequest request, ResponseHandler responseHandler) throws IOException { + return delegate.execute(request, responseHandler); + } + + @Override + public T execute(HttpUriRequest request, + ResponseHandler responseHandler, + HttpContext context) throws IOException { + return delegate.execute(request, responseHandler, context); + } + + @Override + public T execute(HttpHost target, + HttpRequest request, + ResponseHandler responseHandler) throws IOException { + return delegate.execute(target, request, responseHandler); + } + + @Override + public T execute(HttpHost target, HttpRequest request, ResponseHandler responseHandler, + HttpContext context) throws IOException { + return delegate.execute(target, request, responseHandler, context); + } + + @Override + public HttpClientConnectionManager getHttpClientConnectionManager() { + return cm; + } +} diff --git a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/impl/ConnectionManagerAwareHttpClient.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/impl/ConnectionManagerAwareHttpClient.java new file mode 100644 index 000000000000..5231f170021b --- /dev/null +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/impl/ConnectionManagerAwareHttpClient.java @@ -0,0 +1,34 @@ +/* + * 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.impl; + +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import software.amazon.awssdk.annotations.SdkInternalApi; + +/** + * An extension of Apache's HttpClient that expose the connection manager + * associated with the client. + */ +@SdkInternalApi +public interface ConnectionManagerAwareHttpClient extends HttpClient { + + /** + * Returns the {@link HttpClientConnectionManager} associated with the + * http client. + */ + HttpClientConnectionManager getHttpClientConnectionManager(); +} 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 new file mode 100644 index 000000000000..cc203f059630 --- /dev/null +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/net/DelegateSocket.java @@ -0,0 +1,246 @@ +/* + * 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/DelegateSslSocket.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/net/DelegateSslSocket.java new file mode 100644 index 000000000000..d3d590725277 --- /dev/null +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/net/DelegateSslSocket.java @@ -0,0 +1,335 @@ +/* + * 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.SocketAddress; +import java.net.SocketException; +import java.nio.channels.SocketChannel; +import javax.net.ssl.HandshakeCompletedListener; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import software.amazon.awssdk.annotations.SdkInternalApi; + +@SdkInternalApi +public class DelegateSslSocket extends SSLSocket { + protected final SSLSocket sock; + + public DelegateSslSocket(SSLSocket 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); + } + + @Override + public String[] getSupportedCipherSuites() { + return sock.getSupportedCipherSuites(); + } + + @Override + public String[] getEnabledCipherSuites() { + return sock.getEnabledCipherSuites(); + } + + @Override + public void setEnabledCipherSuites(String[] suites) { + sock.setEnabledCipherSuites(suites); + } + + @Override + public String[] getSupportedProtocols() { + return sock.getSupportedProtocols(); + } + + @Override + public String[] getEnabledProtocols() { + return sock.getEnabledProtocols(); + } + + @Override + public void setEnabledProtocols(String[] protocols) { + sock.setEnabledProtocols(protocols); + } + + @Override + public SSLSession getSession() { + return sock.getSession(); + } + + @Override + public void addHandshakeCompletedListener( + HandshakeCompletedListener listener) { + sock.addHandshakeCompletedListener(listener); + } + + @Override + public void removeHandshakeCompletedListener( + HandshakeCompletedListener listener) { + sock.removeHandshakeCompletedListener(listener); + } + + @Override + public void startHandshake() throws IOException { + sock.startHandshake(); + } + + @Override + public void setUseClientMode(boolean mode) { + sock.setUseClientMode(mode); + } + + @Override + public boolean getUseClientMode() { + return sock.getUseClientMode(); + } + + @Override + public void setNeedClientAuth(boolean need) { + sock.setNeedClientAuth(need); + } + + @Override + public boolean getNeedClientAuth() { + return sock.getNeedClientAuth(); + } + + @Override + public void setWantClientAuth(boolean want) { + sock.setWantClientAuth(want); + } + + @Override + public boolean getWantClientAuth() { + return sock.getWantClientAuth(); + } + + @Override + public void setEnableSessionCreation(boolean flag) { + sock.setEnableSessionCreation(flag); + } + + @Override + public boolean getEnableSessionCreation() { + return sock.getEnableSessionCreation(); + } +} diff --git a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/net/InputShutdownCheckingSslSocket.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/net/InputShutdownCheckingSslSocket.java new file mode 100644 index 000000000000..9cb548d110ff --- /dev/null +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/net/InputShutdownCheckingSslSocket.java @@ -0,0 +1,84 @@ +/* + * 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.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import javax.net.ssl.SSLSocket; +import software.amazon.awssdk.annotations.SdkInternalApi; + + + +// TODO : This class will be removed in further PR , keeping it now so that we have a clear baseleine to compare +/** + * Wrapper socket that ensures the read end of the socket is still open before performing a {@code write()}. In TLS 1.3, it is + * permitted for the connection to be in a half-closed state, which is dangerous for the Apache5 client because it can get stuck in + * a state where it continues to write to the socket and potentially end up a blocked state writing to the socket indefinitely. + */ +@SdkInternalApi +public final class InputShutdownCheckingSslSocket extends DelegateSslSocket { + + public InputShutdownCheckingSslSocket(SSLSocket sock) { + super(sock); + } + + @Override + public OutputStream getOutputStream() throws IOException { + return new InputShutdownCheckingOutputStream(sock.getOutputStream(), sock); + } + + private static class InputShutdownCheckingOutputStream extends FilterOutputStream { + private final SSLSocket sock; + + InputShutdownCheckingOutputStream(OutputStream out, SSLSocket sock) { + super(out); + this.sock = sock; + } + + @Override + public void write(int b) throws IOException { + checkInputShutdown(); + out.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + checkInputShutdown(); + out.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + checkInputShutdown(); + out.write(b, off, len); + } + + private void checkInputShutdown() throws IOException { + if (sock.isInputShutdown()) { + throw new IOException("Remote end is closed."); + } + + try { + sock.getInputStream(); + } catch (IOException inputStreamException) { + IOException e = new IOException("Remote end is closed."); + e.addSuppressed(inputStreamException); + throw e; + } + } + } +} 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 new file mode 100644 index 000000000000..286ce1b87d65 --- /dev/null +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/net/SdkSocket.java @@ -0,0 +1,71 @@ +/* + * 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/main/java/software/amazon/awssdk/http/apache5/internal/net/SdkSslSocket.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/net/SdkSslSocket.java new file mode 100644 index 000000000000..9848e7786249 --- /dev/null +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/net/SdkSslSocket.java @@ -0,0 +1,71 @@ +/* + * 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.SocketAddress; +import javax.net.ssl.SSLSocket; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.utils.Logger; + +@SdkInternalApi +public class SdkSslSocket extends DelegateSslSocket { + private static final Logger log = Logger.loggerFor(SdkSslSocket.class); + + public SdkSslSocket(SSLSocket 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/main/java/software/amazon/awssdk/http/apache5/internal/utils/Apache5Utils.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/utils/Apache5Utils.java new file mode 100644 index 000000000000..068dc88bcaae --- /dev/null +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/utils/Apache5Utils.java @@ -0,0 +1,168 @@ +/* + * 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.utils; + +import java.io.IOException; +import java.io.UncheckedIOException; +import org.apache.hc.client5.http.auth.AuthCache; +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.Credentials; +import org.apache.hc.client5.http.auth.CredentialsProvider; +import org.apache.hc.client5.http.auth.NTCredentials; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.auth.BasicAuthCache; +import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; +import org.apache.hc.client5.http.impl.auth.BasicScheme; +import org.apache.hc.client5.http.protocol.HttpClientContext; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.io.entity.BufferedHttpEntity; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.http.apache5.ProxyConfiguration; +import software.amazon.awssdk.utils.Logger; +import software.amazon.awssdk.utils.ReflectionMethodInvoker; + +@SdkInternalApi +public final class Apache5Utils { + private static final Logger logger = Logger.loggerFor(Apache5Utils.class); + private static final ReflectionMethodInvoker NORMALIZE_URI_INVOKER; + + static { + // Attempt to initialize the invoker once on class-load. If it fails, it will not be attempted again, but we'll + // use that opportunity to log a warning. + NORMALIZE_URI_INVOKER = + new ReflectionMethodInvoker<>(RequestConfig.Builder.class, + RequestConfig.Builder.class, + "setNormalizeUri", + boolean.class); + + try { + NORMALIZE_URI_INVOKER.initialize(); + } catch (NoSuchMethodException ignored) { + noSuchMethodThrownByNormalizeUriInvoker(); + } + } + + private Apache5Utils() { + } + + /** + * Utility function for creating a new BufferedEntity and wrapping any errors + * as a SdkClientException. + * + * @param entity The HTTP entity to wrap with a buffered HTTP entity. + * @return A new BufferedHttpEntity wrapping the specified entity. + */ + public static HttpEntity newBufferedHttpEntity(HttpEntity entity) { + try { + return new BufferedHttpEntity(entity); + } catch (IOException e) { + throw new UncheckedIOException("Unable to create HTTP entity: " + e.getMessage(), e); + } + } + + /** + * Returns a new HttpClientContext used for request execution. + */ + public static HttpClientContext newClientContext(ProxyConfiguration proxyConfiguration) { + HttpClientContext clientContext = new HttpClientContext(); + addPreemptiveAuthenticationProxy(clientContext, proxyConfiguration); + + RequestConfig.Builder builder = RequestConfig.custom(); + disableNormalizeUri(builder); + + clientContext.setRequestConfig(builder.build()); + return clientContext; + + } + + /** + * From Apache v4.5.8, normalization should be disabled or AWS requests with special characters in URI path will fail + * with Signature Errors. + *

+ * setNormalizeUri is added only in 4.5.8, so customers using the latest version of SDK with old versions (4.5.6 or less) + * of Apache httpclient will see NoSuchMethodError. Hence this method will suppress the error. + * + * Do not use Apache version 4.5.7 as it breaks URI paths with special characters and there is no option + * to disable normalization. + *

+ * + * For more information, See https://github.com/aws/aws-sdk-java/issues/1919 + */ + public static void disableNormalizeUri(RequestConfig.Builder requestConfigBuilder) { + // For efficiency, do not attempt to call the invoker again if it failed to initialize on class-load + if (NORMALIZE_URI_INVOKER.isInitialized()) { + try { + NORMALIZE_URI_INVOKER.invoke(requestConfigBuilder, false); + } catch (NoSuchMethodException ignored) { + noSuchMethodThrownByNormalizeUriInvoker(); + } + } + } + + /** + * Returns a new Credentials Provider for use with proxy authentication. + */ + public static CredentialsProvider newProxyCredentialsProvider(ProxyConfiguration proxyConfiguration) { + CredentialsProvider provider = new BasicCredentialsProvider(); + provider.setCredentials(newAuthScope(proxyConfiguration), newNtCredentials(proxyConfiguration)); + return provider; + } + + /** + * Returns a new instance of NTCredentials used for proxy authentication. + */ + private static Credentials newNtCredentials(ProxyConfiguration proxyConfiguration) { + return new NTCredentials(proxyConfiguration.username(), + proxyConfiguration.password(), + proxyConfiguration.ntlmWorkstation(), + proxyConfiguration.ntlmDomain()); + } + + /** + * Returns a new instance of AuthScope used for proxy authentication. + */ + private static AuthScope newAuthScope(ProxyConfiguration proxyConfiguration) { + return new AuthScope(proxyConfiguration.host(), proxyConfiguration.port()); + } + + private static void addPreemptiveAuthenticationProxy(HttpClientContext clientContext, + ProxyConfiguration proxyConfiguration) { + + if (proxyConfiguration.preemptiveBasicAuthenticationEnabled()) { + HttpHost targetHost = new HttpHost(proxyConfiguration.host(), proxyConfiguration.port()); + CredentialsProvider credsProvider = newProxyCredentialsProvider(proxyConfiguration); + // Create AuthCache instance + AuthCache authCache = new BasicAuthCache(); + // Generate BASIC scheme object and add it to the local auth cache + BasicScheme basicAuth = new BasicScheme(); + authCache.put(targetHost, basicAuth); + + clientContext.setCredentialsProvider(credsProvider); + clientContext.setAuthCache(authCache); + } + } + + // Just log and then swallow the exception + private static void noSuchMethodThrownByNormalizeUriInvoker() { + // setNormalizeUri method was added in httpclient 4.5.8 + logger.warn(() -> "NoSuchMethodException was thrown when disabling normalizeUri. This indicates you are using " + + "an old version (< 4.5.8) of Apache http client. It is recommended to use http client " + + "version >= 4.5.9 to avoid the breaking change introduced in apache client 4.5.7 and " + + "the latency in exception handling. See https://github.com/aws/aws-sdk-java/issues/1919" + + " for more information"); + } +} diff --git a/http-clients/apache5-client/src/main/resources/META-INF/native-image/software.amazon.awssdk/apache-client/proxy-config.json b/http-clients/apache5-client/src/main/resources/META-INF/native-image/software.amazon.awssdk/apache-client/proxy-config.json new file mode 100644 index 000000000000..014f866bc2be --- /dev/null +++ b/http-clients/apache5-client/src/main/resources/META-INF/native-image/software.amazon.awssdk/apache-client/proxy-config.json @@ -0,0 +1,15 @@ +[ + [ + "org.apache.http.conn.HttpClientConnectionManager", + "org.apache.http.pool.ConnPoolControl", + "import software.amazon.awssdk.http.apache5.internal.conn.Wrapped" + ], + [ + "org.apache.http.conn.HttpClientConnectionManager", + "import software.amazon.awssdk.http.apache5.internal.conn.Wrapped" + ], + [ + "org.apache.http.conn.ConnectionRequest", + "import software.amazon.awssdk.http.apache5.internal.conn.Wrapped" + ] +] \ No newline at end of file diff --git a/http-clients/apache5-client/src/main/resources/META-INF/native-image/software.amazon.awssdk/apache-client/reflect-config.json b/http-clients/apache5-client/src/main/resources/META-INF/native-image/software.amazon.awssdk/apache-client/reflect-config.json new file mode 100644 index 000000000000..ce80a77b59d4 --- /dev/null +++ b/http-clients/apache5-client/src/main/resources/META-INF/native-image/software.amazon.awssdk/apache-client/reflect-config.json @@ -0,0 +1,43 @@ +[ + { + "name": "import software.amazon.awssdk.http.apache5.ApacheSdkHttpService", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.apache.http.client.config.RequestConfig$Builder", + "methods": [ + { + "name": "setNormalizeUri" + } + ] + }, + { + "name": "org.apache.commons.logging.LogFactory", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name":"org.apache.commons.logging.impl.Jdk14Logger", + "methods":[{"name":"","parameterTypes":["java.lang.String"] }] + }, + { + "name":"org.apache.commons.logging.impl.Log4JLogger" + }, + { + "name":"org.apache.commons.logging.impl.LogFactoryImpl", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"org.apache.commons.logging.impl.WeakHashtable", + "methods":[{"name":"","parameterTypes":[] }] + } +] diff --git a/http-clients/apache5-client/src/main/resources/META-INF/native-image/software.amazon.awssdk/apache-client/resource-config.json b/http-clients/apache5-client/src/main/resources/META-INF/native-image/software.amazon.awssdk/apache-client/resource-config.json new file mode 100644 index 000000000000..3a0b7729fff7 --- /dev/null +++ b/http-clients/apache5-client/src/main/resources/META-INF/native-image/software.amazon.awssdk/apache-client/resource-config.json @@ -0,0 +1,7 @@ +{ + "resources": [ + { + "pattern": "\\Qsoftware.amazon.awssdk.http.SdkHttpService\\E" + } + ] +} \ No newline at end of file diff --git a/http-clients/apache5-client/src/main/resources/META-INF/services/software.amazon.awssdk.http.SdkHttpService b/http-clients/apache5-client/src/main/resources/META-INF/services/software.amazon.awssdk.http.SdkHttpService new file mode 100644 index 000000000000..ff8dfa4345a6 --- /dev/null +++ b/http-clients/apache5-client/src/main/resources/META-INF/services/software.amazon.awssdk.http.SdkHttpService @@ -0,0 +1,46 @@ +# +# 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. +# + +# +# 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. +# + +# +# 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. +# + +software.amazon.awssdk.http.apache5.Apache5SdkHttpService diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheClientProxyConfigurationTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheClientProxyConfigurationTest.java new file mode 100644 index 000000000000..4b871dad1f51 --- /dev/null +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheClientProxyConfigurationTest.java @@ -0,0 +1,52 @@ +/* + * 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 java.net.ConnectException; +import org.apache.hc.client5.http.HttpHostConnectException; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.http.proxy.HttpClientDefaultProxyConfigTestSuite; + +public class ApacheClientProxyConfigurationTest extends HttpClientDefaultProxyConfigTestSuite { + + @Override + protected Class getProxyFailedExceptionType() { + return HttpHostConnectException.class; + + } + + @Override + protected Class getProxyFailedCauseExceptionType() { + return ConnectException.class; + } + + @Override + protected boolean isSyncClient() { + return true; + } + + @Override + protected SdkAsyncHttpClient createHttpClientWithDefaultProxy() { + throw new IllegalArgumentException("Async client is not supported for this test."); + } + + @Override + protected SdkHttpClient createSyncHttpClientWithDefaultProxy() { + return ApacheHttpClient.create(); + } + +} \ No newline at end of file diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheClientTlsAuthTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheClientTlsAuthTest.java new file mode 100644 index 000000000000..e3cd85a55557 --- /dev/null +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheClientTlsAuthTest.java @@ -0,0 +1,251 @@ +/* + * 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 com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.any; +import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.instanceOf; +import static software.amazon.awssdk.utils.JavaSystemSetting.SSL_KEY_STORE; +import static software.amazon.awssdk.utils.JavaSystemSetting.SSL_KEY_STORE_PASSWORD; +import static software.amazon.awssdk.utils.JavaSystemSetting.SSL_KEY_STORE_TYPE; + +import com.github.tomakehurst.wiremock.WireMockServer; +import java.io.IOException; +import java.net.SocketException; +import java.net.URI; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +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.NoopHostnameVerifier; +import org.apache.hc.core5.http.NoHttpResponseException; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Mockito; +import software.amazon.awssdk.http.FileStoreTlsKeyManagersProvider; +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.SdkHttpRequest; +import software.amazon.awssdk.http.TlsKeyManagersProvider; +import software.amazon.awssdk.http.apache5.internal.conn.SdkTlsSocketFactory; +import software.amazon.awssdk.internal.http.NoneTlsKeyManagersProvider; + +/** + * Tests to ensure that {@link ApacheHttpClient} can properly support TLS + * client authentication. + */ +public class ApacheClientTlsAuthTest extends ClientTlsAuthTestBase { + private static WireMockServer wireMockServer; + private static TlsKeyManagersProvider keyManagersProvider; + private SdkHttpClient client; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @BeforeClass + public static void setUp() throws IOException { + ClientTlsAuthTestBase.setUp(); + + // Will be used by both client and server to trust the self-signed + // cert they present to each other + System.setProperty("javax.net.ssl.trustStore", serverKeyStore.toAbsolutePath().toString()); + System.setProperty("javax.net.ssl.trustStorePassword", STORE_PASSWORD); + System.setProperty("javax.net.ssl.trustStoreType", "jks"); + + wireMockServer = new WireMockServer(wireMockConfig() + .dynamicHttpsPort() + .needClientAuth(true) + .keystorePath(serverKeyStore.toAbsolutePath().toString()) + .keystorePassword(STORE_PASSWORD) + ); + + wireMockServer.start(); + + keyManagersProvider = FileStoreTlsKeyManagersProvider.create(clientKeyStore, CLIENT_STORE_TYPE, STORE_PASSWORD); + } + + @Before + public void methodSetup() { + wireMockServer.stubFor(any(urlMatching(".*")).willReturn(aResponse().withStatus(200).withBody("{}"))); + } + + @AfterClass + public static void teardown() throws IOException { + wireMockServer.stop(); + System.clearProperty("javax.net.ssl.trustStore"); + System.clearProperty("javax.net.ssl.trustStorePassword"); + System.clearProperty("javax.net.ssl.trustStoreType"); + ClientTlsAuthTestBase.teardown(); + } + + @After + public void methodTeardown() { + if (client != null) { + client.close(); + } + client = null; + } + + @Test + public void canMakeHttpsRequestWhenKeyProviderConfigured() throws IOException { + client = ApacheHttpClient.builder() + .tlsKeyManagersProvider(keyManagersProvider) + .build(); + HttpExecuteResponse httpExecuteResponse = makeRequestWithHttpClient(client); + assertThat(httpExecuteResponse.httpResponse().isSuccessful()).isTrue(); + } + + @Test + public void requestFailsWhenKeyProviderNotConfigured() throws IOException { + thrown.expect(anyOf(instanceOf(NoHttpResponseException.class), instanceOf(SSLException.class), instanceOf(SocketException.class))); + client = ApacheHttpClient.builder().tlsKeyManagersProvider(NoneTlsKeyManagersProvider.getInstance()).build(); + makeRequestWithHttpClient(client); + } + + @Test + public void authenticatesWithTlsProxy() throws IOException { + ProxyConfiguration proxyConfig = ProxyConfiguration.builder() + .endpoint(URI.create("https://localhost:" + wireMockServer.httpsPort())) + .build(); + + client = ApacheHttpClient.builder() + .proxyConfiguration(proxyConfig) + .tlsKeyManagersProvider(keyManagersProvider) + .build(); + + HttpExecuteResponse httpExecuteResponse = makeRequestWithHttpClient(client); + + // WireMock doesn't mock 'CONNECT' methods and will return a 404 for this + assertThat(httpExecuteResponse.httpResponse().statusCode()).isEqualTo(404); + } + + @Test + public void defaultTlsKeyManagersProviderIsSystemPropertyProvider() 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); + + client = ApacheHttpClient.builder().build(); + try { + makeRequestWithHttpClient(client); + } finally { + System.clearProperty(SSL_KEY_STORE.property()); + System.clearProperty(SSL_KEY_STORE_TYPE.property()); + System.clearProperty(SSL_KEY_STORE_PASSWORD.property()); + } + } + + @Test + public void defaultTlsKeyManagersProviderIsSystemPropertyProvider_explicitlySetToNull() 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); + + client = ApacheHttpClient.builder().tlsKeyManagersProvider(null).build(); + try { + makeRequestWithHttpClient(client); + } finally { + System.clearProperty(SSL_KEY_STORE.property()); + System.clearProperty(SSL_KEY_STORE_TYPE.property()); + System.clearProperty(SSL_KEY_STORE_PASSWORD.property()); + } + } + + @Test + public void build_notSettingSocketFactory_configuresClientWithDefaultSocketFactory() throws IOException, + NoSuchAlgorithmException, + KeyManagementException { + 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); + + TlsKeyManagersProvider provider = FileStoreTlsKeyManagersProvider.create(clientKeyStore, + CLIENT_STORE_TYPE, + STORE_PASSWORD); + KeyManager[] keyManagers = provider.keyManagers(); + + SSLContext sslcontext = SSLContext.getInstance("TLS"); + sslcontext.init(keyManagers, null, null); + + ConnectionSocketFactory socketFactory = new SdkTlsSocketFactory(sslcontext, NoopHostnameVerifier.INSTANCE); + ConnectionSocketFactory socketFactoryMock = Mockito.spy(socketFactory); + + client = ApacheHttpClient.builder().build(); + + try { + HttpExecuteResponse httpExecuteResponse = makeRequestWithHttpClient(client); + assertThat(httpExecuteResponse.httpResponse().statusCode()).isEqualTo(200); + } finally { + System.clearProperty(SSL_KEY_STORE.property()); + System.clearProperty(SSL_KEY_STORE_TYPE.property()); + System.clearProperty(SSL_KEY_STORE_PASSWORD.property()); + } + + 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); + + ConnectionSocketFactory socketFactory = new SdkTlsSocketFactory(sslcontext, NoopHostnameVerifier.INSTANCE); + ConnectionSocketFactory socketFactoryMock = Mockito.spy(socketFactory); + + client = ApacheHttpClient.builder() + .socketFactory(socketFactoryMock) + .build(); + makeRequestWithHttpClient(client); + + Mockito.verify(socketFactoryMock).createSocket(Mockito.any()); + } + + private HttpExecuteResponse makeRequestWithHttpClient(SdkHttpClient httpClient) throws IOException { + SdkHttpRequest httpRequest = SdkHttpFullRequest.builder() + .method(SdkHttpMethod.GET) + .protocol("https") + .host("localhost:" + wireMockServer.httpsPort()) + .build(); + + HttpExecuteRequest request = HttpExecuteRequest.builder() + .request(httpRequest) + .build(); + + return httpClient.prepareRequest(request).call(); + } + +} diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheClientTlsHalfCloseTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheClientTlsHalfCloseTest.java new file mode 100644 index 000000000000..d4f6b3bbc0f9 --- /dev/null +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheClientTlsHalfCloseTest.java @@ -0,0 +1,145 @@ +/* + * 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.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; +import software.amazon.awssdk.http.ContentStreamProvider; +import software.amazon.awssdk.http.FileStoreTlsKeyManagersProvider; +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.SdkHttpRequest; +import software.amazon.awssdk.http.TlsKeyManagersProvider; +import software.amazon.awssdk.http.server.MockServer; + +public class ApacheClientTlsHalfCloseTest extends ClientTlsAuthTestBase { + + private static TlsKeyManagersProvider tlsKeyManagersProvider; + private static MockServer mockServer; + private SdkHttpClient httpClient; + + private static final int TWO_MB = 2 * 1024 * 1024; + private static final byte[] CONTENT = new byte[TWO_MB]; + + @Test + @EnabledIf("halfCloseSupported") + public void errorWhenServerHalfClosesSocketWhileStreamIsOpened() { + + mockServer = MockServer.createMockServer(MockServer.ServerBehavior.HALF_CLOSE); + mockServer.startServer(tlsKeyManagersProvider); + + httpClient = ApacheHttpClient.builder() + .tlsKeyManagersProvider(tlsKeyManagersProvider) + .build(); + IOException exception = assertThrows(IOException.class, () -> { + executeHttpRequest(httpClient); + }); + assertEquals("Remote end is closed.", exception.getMessage()); + } + + + @Test + public void errorWhenServerFullClosesSocketWhileStreamIsOpened() throws IOException { + mockServer = MockServer.createMockServer(MockServer.ServerBehavior.FULL_CLOSE_IN_BETWEEN); + mockServer.startServer(tlsKeyManagersProvider); + + httpClient = ApacheHttpClient.builder() + .tlsKeyManagersProvider(tlsKeyManagersProvider) + .build(); + + IOException exception = assertThrows(IOException.class, () -> { + executeHttpRequest(httpClient); + }); + + if(halfCloseSupported()){ + assertEquals("Remote end is closed.", exception.getMessage()); + + }else { + assertEquals("Socket is closed", exception.getMessage()); + + } + } + + @Test + public void successfulRequestForFullCloseSocketAtTheEnd() throws IOException { + mockServer = MockServer.createMockServer(MockServer.ServerBehavior.FULL_CLOSE_AT_THE_END); + mockServer.startServer(tlsKeyManagersProvider); + + httpClient = ApacheHttpClient.builder() + .tlsKeyManagersProvider(tlsKeyManagersProvider) + .build(); + + HttpExecuteResponse response = executeHttpRequest(httpClient); + + assertThat(response.httpResponse().isSuccessful()).isTrue(); + } + + @AfterEach + void tearDown() { + if (mockServer != null) { + mockServer.stopServer(); + } + } + + @BeforeAll + public static void setUp() throws IOException { + ClientTlsAuthTestBase.setUp(); + System.setProperty("javax.net.ssl.trustStore", serverKeyStore.toAbsolutePath().toString()); + System.setProperty("javax.net.ssl.trustStorePassword", STORE_PASSWORD); + System.setProperty("javax.net.ssl.trustStoreType", "jks"); + tlsKeyManagersProvider = FileStoreTlsKeyManagersProvider.create(clientKeyStore, CLIENT_STORE_TYPE, STORE_PASSWORD); + } + + @AfterAll + public static void clear() throws IOException { + System.clearProperty("javax.net.ssl.trustStore"); + System.clearProperty("javax.net.ssl.trustStorePassword"); + System.clearProperty("javax.net.ssl.trustStoreType"); + ClientTlsAuthTestBase.teardown(); + + } + + private static HttpExecuteResponse executeHttpRequest(SdkHttpClient client) throws IOException { + ContentStreamProvider contentStreamProvider = () -> new ByteArrayInputStream(CONTENT); + SdkHttpRequest httpRequest = SdkHttpFullRequest.builder() + .method(SdkHttpMethod.PUT) + .protocol("https") + .host("localhost:" + mockServer.getPort()) + .build(); + HttpExecuteRequest request = HttpExecuteRequest.builder() + .request(httpRequest) + .contentStreamProvider(contentStreamProvider) + .build(); + + return client.prepareRequest(request).call(); + } + + public static boolean halfCloseSupported(){ + return MockServer.isTlsHalfCloseSupported(); + } +} \ No newline at end of file diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheHttpClientAuthRegistryTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheHttpClientAuthRegistryTest.java new file mode 100644 index 000000000000..03b43814ee13 --- /dev/null +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheHttpClientAuthRegistryTest.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 com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; + +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import java.net.URI; +import org.apache.hc.client5.http.auth.AuthSchemeProvider; +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.CredentialsProvider; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.apache.hc.client5.http.config.AuthSchemes; +import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; +import org.apache.hc.client5.http.impl.auth.BasicSchemeFactory; +import org.apache.hc.client5.http.impl.auth.KerberosSchemeFactory; +import org.apache.hc.core5.http.config.Registry; +import org.apache.hc.core5.http.config.RegistryBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import software.amazon.awssdk.http.ExecutableHttpRequest; +import software.amazon.awssdk.http.HttpExecuteRequest; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.SdkHttpRequest; + + +class ApacheHttpClientAuthRegistryTest { + + @RegisterExtension + static WireMockExtension proxyWireMock = WireMockExtension.newInstance() + .options(wireMockConfig().dynamicPort()) + .build(); + @RegisterExtension + static WireMockExtension serverWireMock = WireMockExtension.newInstance() + .options(wireMockConfig().dynamicPort()) + .build(); + + private ApacheHttpClient httpClient; + private static final String PROXY_AUTH_SCENARIO = "Proxy Auth"; + private static final String SERVER_AUTH_SCENARIO = "Server Auth"; + private static final String CHALLENGED_STATE = "Challenged"; + + + private Registry createAuthSchemeRegistry(String scheme, AuthSchemeProvider provider) { + return RegistryBuilder.create() + .register(scheme, provider) + .build(); + } + + private ApacheHttpClient createHttpClient(Registry authSchemeRegistry) { + CredentialsProvider credsProvider = new BasicCredentialsProvider(); + credsProvider.setCredentials( + new AuthScope("localhost", AuthScope.ANY_PORT), + new UsernamePasswordCredentials("u1", "p1".toCharArray())); + + return (ApacheHttpClient) ApacheHttpClient.builder() + .proxyConfiguration(ProxyConfiguration.builder().endpoint(URI.create("http://localhost:" + proxyWireMock.getPort())) + .build()) + .authSchemeProviderRegistry(authSchemeRegistry) + .credentialsProvider(credsProvider) + .build(); + } + + private SdkHttpRequest createHttpRequest() { + return SdkHttpRequest.builder() + .uri(URI.create("http://localhost:" + serverWireMock.getPort())) + .method(SdkHttpMethod.GET) + .build(); + } + private void setupProxyWireMockStub() { + proxyWireMock.stubFor(get(urlMatching(".*")) + .inScenario(PROXY_AUTH_SCENARIO) + .whenScenarioStateIs(STARTED) + .willReturn(aResponse() + .withStatus(401) + .withHeader("WWW-Authenticate", "Basic")) + .willSetStateTo(CHALLENGED_STATE)); + + proxyWireMock.stubFor(get(urlMatching(".*")) + .inScenario(PROXY_AUTH_SCENARIO) + .whenScenarioStateIs(CHALLENGED_STATE) + //.withHeader("WWW-Authenticate", matching(".*")) + .willReturn(aResponse() + .withStatus(200)) + .willSetStateTo("success")); + } + + private void setupWireMockStub() { + serverWireMock.stubFor(get(urlMatching(".*")) + .inScenario(SERVER_AUTH_SCENARIO) + .whenScenarioStateIs(STARTED) + .withHeader("Authorization", matching(".*")) + .willReturn(aResponse().withStatus(200))); + } + + private HttpExecuteResponse executeRequest(SdkHttpRequest request) throws Exception { + HttpExecuteRequest executeRequest = HttpExecuteRequest.builder() + .request(request) + .build(); + ExecutableHttpRequest executableRequest = httpClient.prepareRequest(executeRequest); + return executableRequest.call(); + } + + @Test + void authSchemeRegistryConfigured_registeredAuthShouldPass() throws Exception { + Registry authSchemeRegistry = createAuthSchemeRegistry( + AuthSchemes.BASIC, + new BasicSchemeFactory() + ); + + httpClient = createHttpClient(authSchemeRegistry); + setupProxyWireMockStub(); + setupWireMockStub(); + + HttpExecuteResponse response = executeRequest(createHttpRequest()); + + proxyWireMock.verify(1, getRequestedFor(urlMatching(".*")) + .withHeader("Authorization", matching(".*")) + ); + } + + @Test + void authSchemeRegistryConfigured_unRegisteredAuthShouldWarn() throws Exception { + Registry authSchemeRegistry = createAuthSchemeRegistry( + AuthSchemes.KERBEROS, + new KerberosSchemeFactory() + ); + + httpClient = createHttpClient(authSchemeRegistry); + setupProxyWireMockStub(); + setupWireMockStub(); + + HttpExecuteResponse response = executeRequest(createHttpRequest()); + proxyWireMock.verify(0, getRequestedFor(urlMatching(".*")) + .withHeader("Authorization", matching(".*")) + ); + } +} diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheHttpClientDefaultWireMockTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheHttpClientDefaultWireMockTest.java new file mode 100644 index 000000000000..06d7edc20b3e --- /dev/null +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheHttpClientDefaultWireMockTest.java @@ -0,0 +1,28 @@ +/* + * 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 software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpClientDefaultTestSuite; + +public class ApacheHttpClientDefaultWireMockTest extends SdkHttpClientDefaultTestSuite { + + @Override + protected SdkHttpClient createSdkHttpClient() { + return ApacheHttpClient.create(); + } + +} diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheHttpClientTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheHttpClientTest.java new file mode 100644 index 000000000000..b412b2a7e71f --- /dev/null +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheHttpClientTest.java @@ -0,0 +1,148 @@ +/* + * 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.assertThatThrownBy; + +import java.net.InetAddress; +import java.net.URI; +import java.net.UnknownHostException; +import org.apache.hc.client5.http.DnsResolver; +import org.apache.hc.client5.http.SystemDefaultDnsResolver; +import org.apache.hc.client5.http.auth.CredentialsProvider; +import org.apache.hc.client5.http.routing.HttpRoutePlanner; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +/** + * @see ApacheHttpClientWireMockTest + */ +public class ApacheHttpClientTest { + @AfterEach + public void cleanup() { + System.clearProperty("http.proxyHost"); + System.clearProperty("http.proxyPort"); + System.clearProperty("http.proxyUser"); + System.clearProperty("http.proxyPassword"); + } + + @Test + public void connectionReaperCanBeManuallyEnabled() { + ApacheHttpClient.builder() + .useIdleConnectionReaper(true) + .build() + .close(); + } + + @Test + public void httpRoutePlannerCantBeUsedWithProxy() { + ProxyConfiguration proxyConfig = ProxyConfiguration.builder() + .endpoint(URI.create("http://localhost:1234")) + .useSystemPropertyValues(Boolean.FALSE) + .build(); + assertThatThrownBy(() -> { + ApacheHttpClient.builder() + .proxyConfiguration(proxyConfig) + .httpRoutePlanner(Mockito.mock(HttpRoutePlanner.class)) + .build(); + }).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void httpRoutePlannerCantBeUsedWithProxy_SystemPropertiesEnabled() { + System.setProperty("http.proxyHost", "localhost"); + System.setProperty("http.proxyPort", "1234"); + + assertThatThrownBy(() -> { + ApacheHttpClient.builder() + .httpRoutePlanner(Mockito.mock(HttpRoutePlanner.class)) + .build(); + }).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void httpRoutePlannerCantBeUsedWithProxy_SystemPropertiesDisabled() { + System.setProperty("http.proxyHost", "localhost"); + System.setProperty("http.proxyPort", "1234"); + + ProxyConfiguration proxyConfig = ProxyConfiguration.builder() + .useSystemPropertyValues(Boolean.FALSE) + .build(); + + ApacheHttpClient.builder() + .proxyConfiguration(proxyConfig) + .httpRoutePlanner(Mockito.mock(HttpRoutePlanner.class)) + .build(); + } + + @Test + public void credentialProviderCantBeUsedWithProxyCredentials() { + ProxyConfiguration proxyConfig = ProxyConfiguration.builder() + .endpoint(URI.create("http://localhost:1234")) + .username("foo") + .password("bar") + .build(); + assertThatThrownBy(() -> { + ApacheHttpClient.builder() + .proxyConfiguration(proxyConfig) + .credentialsProvider(Mockito.mock(CredentialsProvider.class)) + .build(); + }).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void credentialProviderCantBeUsedWithProxyCredentials_SystemProperties() { + System.setProperty("http.proxyUser", "foo"); + System.setProperty("http.proxyPassword", "bar"); + + assertThatThrownBy(() -> { + ApacheHttpClient.builder() + .credentialsProvider(Mockito.mock(CredentialsProvider.class)) + .build(); + }).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void credentialProviderCanBeUsedWithProxy() { + ProxyConfiguration proxyConfig = ProxyConfiguration.builder() + .endpoint(URI.create("http://localhost:1234")) + .build(); + ApacheHttpClient.builder() + .proxyConfiguration(proxyConfig) + .credentialsProvider(Mockito.mock(CredentialsProvider.class)) + .build(); + } + + @Test + public void dnsResolverCanBeUsed() { + DnsResolver dnsResolver = new SystemDefaultDnsResolver() { + @Override + public InetAddress[] resolve(final String host) throws UnknownHostException { + if (host.equalsIgnoreCase("my.host.com")) { + return new InetAddress[] { InetAddress.getByName("127.0.0.1") }; + } else { + return super.resolve(host); + } + } + }; + + ApacheHttpClient.builder() + .dnsResolver(dnsResolver) + .build() + .close(); + } +} diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheHttpClientWireMockTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheHttpClientWireMockTest.java new file mode 100644 index 000000000000..70cfc0f67181 --- /dev/null +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheHttpClientWireMockTest.java @@ -0,0 +1,226 @@ +/* + * 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 com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.any; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static software.amazon.awssdk.http.SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES; + +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.URI; +import java.net.UnknownHostException; +import org.apache.hc.client5.http.DnsResolver; +import org.apache.hc.client5.http.HttpRoute; +import org.apache.hc.client5.http.SystemDefaultDnsResolver; +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.Credentials; +import org.apache.hc.client5.http.auth.CredentialsProvider; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.core5.http.HttpHost; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import software.amazon.awssdk.http.HttpExecuteRequest; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpClientTestSuite; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.apache5.internal.Apache5HttpRequestConfig; +import software.amazon.awssdk.http.apache5.internal.impl.ConnectionManagerAwareHttpClient; +import software.amazon.awssdk.utils.AttributeMap; + +@RunWith(MockitoJUnitRunner.class) +public class ApacheHttpClientWireMockTest extends SdkHttpClientTestSuite { + @Rule + public WireMockRule mockProxyServer = new WireMockRule(wireMockConfig().dynamicPort().dynamicHttpsPort()); + + @Mock + private ConnectionManagerAwareHttpClient httpClient; + + @Mock + private HttpClientConnectionManager connectionManager; + + @Override + protected SdkHttpClient createSdkHttpClient(SdkHttpClientOptions options) { + ApacheHttpClient.Builder builder = ApacheHttpClient.builder(); + + AttributeMap.Builder attributeMap = AttributeMap.builder(); + + if (options.tlsTrustManagersProvider() != null) { + builder.tlsTrustManagersProvider(options.tlsTrustManagersProvider()); + } + + if (options.trustAll()) { + attributeMap.put(TRUST_ALL_CERTIFICATES, options.trustAll()); + } + + return builder.buildWithDefaults(attributeMap.build()); + } + + @Test + public void closeClient_shouldCloseUnderlyingResources() { + ApacheHttpClient client = new ApacheHttpClient(httpClient, Apache5HttpRequestConfig.builder().build(), AttributeMap.empty()); + when(httpClient.getHttpClientConnectionManager()).thenReturn(connectionManager); + + client.close(); + verify(connectionManager).shutdown(); + } + + @Test + public void routePlannerIsInvoked() throws Exception { + mockProxyServer.resetToDefaultMappings(); + mockProxyServer.addStubMapping(WireMock.any(urlPathEqualTo("/")) + .willReturn(aResponse().proxiedFrom("http://localhost:" + mockServer.port())) + .build()); + + SdkHttpClient client = ApacheHttpClient.builder() + .httpRoutePlanner( + (host, request, context) -> + new HttpRoute( + new HttpHost("https", "localhost", mockProxyServer.httpsPort()) + ) + ) + .buildWithDefaults(AttributeMap.builder() + .put(TRUST_ALL_CERTIFICATES, Boolean.TRUE) + .build()); + + testForResponseCodeUsingHttps(client, HttpURLConnection.HTTP_OK); + + mockProxyServer.verify(1, RequestPatternBuilder.allRequests()); + } + + @Test + public void credentialPlannerIsInvoked() throws Exception { + mockProxyServer.addStubMapping(WireMock.any(urlPathEqualTo("/")) + .willReturn(aResponse() + .withHeader("WWW-Authenticate", "Basic realm=\"proxy server\"") + .withStatus(401)) + .build()); + + mockProxyServer.addStubMapping(WireMock.any(urlPathEqualTo("/")) + .withBasicAuth("foo", "bar") + .willReturn(aResponse() + .proxiedFrom("http://localhost:" + mockServer.port())) + .build()); + + SdkHttpClient client = ApacheHttpClient.builder() + .credentialsProvider(new CredentialsProvider() { + @Override + public void setCredentials(AuthScope authScope, Credentials credentials) { + + } + + @Override + public Credentials getCredentials(AuthScope authScope) { + return new UsernamePasswordCredentials("foo", "bar".toCharArray()); + } + + @Override + public void clear() { + + } + }) + .httpRoutePlanner( + (host, request, context) -> + new HttpRoute( + new HttpHost("https", "localhost", mockProxyServer.httpsPort()) + ) + ) + .buildWithDefaults(AttributeMap.builder() + .put(TRUST_ALL_CERTIFICATES, Boolean.TRUE) + .build()); + testForResponseCodeUsingHttps(client, HttpURLConnection.HTTP_OK); + + mockProxyServer.verify(2, RequestPatternBuilder.allRequests()); + } + + @Test + public void overrideDnsResolver_WithDnsMatchingResolver_successful() throws Exception { + overrideDnsResolver("magic.local.host"); + } + + @Test(expected = UnknownHostException.class) + public void overrideDnsResolver_WithUnknownHost_throwsException() throws Exception { + overrideDnsResolver("sad.local.host"); + } + + @Test + public void overrideDnsResolver_WithLocalhost_successful() throws Exception { + overrideDnsResolver("localhost"); + } + + @Test + public void explicitNullDnsResolver_WithLocalhost_successful() throws Exception { + overrideDnsResolver("localhost", true); + } + + private void overrideDnsResolver(String hostName) throws IOException { + overrideDnsResolver(hostName, false); + } + + private void overrideDnsResolver(String hostName, boolean nullifyResolver) throws IOException { + + DnsResolver dnsResolver = new SystemDefaultDnsResolver() { + @Override + public InetAddress[] resolve(final String host) throws UnknownHostException { + if (host.equalsIgnoreCase("magic.local.host")) { + return new InetAddress[] { InetAddress.getByName("127.0.0.1") }; + } else { + return super.resolve(host); + } + } + }; + if (nullifyResolver) { + dnsResolver = null; + } + + SdkHttpClient client = ApacheHttpClient.builder() + .dnsResolver(dnsResolver) + .buildWithDefaults(AttributeMap.builder() + .put(TRUST_ALL_CERTIFICATES, Boolean.TRUE) + .build()); + + mockProxyServer.resetToDefaultMappings(); + mockProxyServer.stubFor(any(urlPathEqualTo("/")).willReturn(aResponse().withStatus(HttpURLConnection.HTTP_OK))); + + URI uri = URI.create("https://" + hostName + ":" + mockProxyServer.httpsPort()); + SdkHttpFullRequest req = SdkHttpFullRequest.builder() + .uri(uri) + .method(SdkHttpMethod.POST) + .putHeader("Host", uri.getHost()) + .build(); + + client.prepareRequest(HttpExecuteRequest.builder() + .request(req) + .contentStreamProvider(req.contentStreamProvider().orElse(null)) + .build()) + .call(); + + mockProxyServer.verify(1, RequestPatternBuilder.allRequests()); + } +} diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheHttpProxyTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheHttpProxyTest.java new file mode 100644 index 000000000000..937a29d053f9 --- /dev/null +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheHttpProxyTest.java @@ -0,0 +1,71 @@ +/* + * 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.assertThat; + +import java.net.URI; +import java.util.Set; +import software.amazon.awssdk.http.HttpProxyTestSuite; +import software.amazon.awssdk.http.proxy.TestProxySetting; + +public class ApacheHttpProxyTest extends HttpProxyTestSuite { + @Override + protected void assertProxyConfiguration(TestProxySetting userSetProxySettings, + TestProxySetting expectedProxySettings, + Boolean useSystemProperty, + Boolean useEnvironmentVariable, + String protocol) { + + ProxyConfiguration.Builder builder = ProxyConfiguration.builder(); + + if (userSetProxySettings != null) { + String hostName = userSetProxySettings.getHost(); + Integer portNumber = userSetProxySettings.getPort(); + String userName = userSetProxySettings.getUserName(); + String password = userSetProxySettings.getPassword(); + Set nonProxyHosts = userSetProxySettings.getNonProxyHosts(); + + if (hostName != null && portNumber != null) { + builder.endpoint(URI.create(String.format("%s://%s:%d", protocol, hostName, portNumber))); + } + if (userName != null) { + builder.username(userName); + } + if (password != null) { + builder.password(password); + } + if (nonProxyHosts != null && !nonProxyHosts.isEmpty()) { + builder.nonProxyHosts(nonProxyHosts); + } + } + if (!"http".equals(protocol)) { + builder.scheme(protocol); + } + if (useSystemProperty != null) { + builder.useSystemPropertyValues(useSystemProperty); + } + if (useEnvironmentVariable != null) { + builder.useEnvironmentVariableValues(useEnvironmentVariable); + } + ProxyConfiguration proxyConfiguration = builder.build(); + assertThat(proxyConfiguration.host()).isEqualTo(expectedProxySettings.getHost()); + assertThat(proxyConfiguration.port()).isEqualTo(expectedProxySettings.getPort()); + assertThat(proxyConfiguration.username()).isEqualTo(expectedProxySettings.getUserName()); + assertThat(proxyConfiguration.password()).isEqualTo(expectedProxySettings.getPassword()); + assertThat(proxyConfiguration.nonProxyHosts()).isEqualTo(expectedProxySettings.getNonProxyHosts()); + } +} diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheMetricsTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheMetricsTest.java new file mode 100644 index 000000000000..821433d2eaae --- /dev/null +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ApacheMetricsTest.java @@ -0,0 +1,99 @@ +/* + * 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 com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.any; +import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.http.HttpMetric.CONCURRENCY_ACQUIRE_DURATION; + +import com.github.tomakehurst.wiremock.WireMockServer; +import java.io.IOException; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +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.SdkHttpRequest; +import software.amazon.awssdk.metrics.MetricCollection; +import software.amazon.awssdk.metrics.MetricCollector; + + +public class ApacheMetricsTest { + private static WireMockServer wireMockServer; + private SdkHttpClient client; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @BeforeClass + public static void setUp() throws IOException { + wireMockServer = new WireMockServer(); + wireMockServer.start(); + } + + @Before + public void methodSetup() { + wireMockServer.stubFor(any(urlMatching(".*")).willReturn(aResponse().withStatus(200).withBody("{}"))); + } + + @AfterClass + public static void teardown() throws IOException { + wireMockServer.stop(); + } + + @After + public void methodTeardown() { + if (client != null) { + client.close(); + } + client = null; + } + + @Test + public void concurrencyAcquireDurationIsRecorded() throws IOException { + client = ApacheHttpClient.create(); + MetricCollector collector = MetricCollector.create("test"); + makeRequestWithMetrics(client, collector); + + MetricCollection collection = collector.collect(); + + assertThat(collection.metricValues(CONCURRENCY_ACQUIRE_DURATION)).isNotEmpty(); + } + + private HttpExecuteResponse makeRequestWithMetrics(SdkHttpClient httpClient, MetricCollector metricCollector) throws IOException { + SdkHttpRequest httpRequest = SdkHttpFullRequest.builder() + .method(SdkHttpMethod.GET) + .protocol("http") + .host("localhost:" + wireMockServer.port()) + .build(); + + HttpExecuteRequest request = HttpExecuteRequest.builder() + .request(httpRequest) + .metricCollector(metricCollector) + .build(); + + return httpClient.prepareRequest(request).call(); + } +} diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ClientTlsAuthTestBase.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ClientTlsAuthTestBase.java new file mode 100644 index 000000000000..ea2ecc9431d7 --- /dev/null +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ClientTlsAuthTestBase.java @@ -0,0 +1,64 @@ +/* + * 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 java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; + +abstract class ClientTlsAuthTestBase { + protected static final String STORE_PASSWORD = "password"; + protected static final String CLIENT_STORE_TYPE = "pkcs12"; + protected static final String TEST_KEY_STORE = "/software/amazon/awssdk/http/apache5/server-keystore"; + protected static final String CLIENT_KEY_STORE = "/software/amazon/awssdk/http/apache5/client1.p12"; + + protected static Path tempDir; + protected static Path serverKeyStore; + protected static Path clientKeyStore; + + @BeforeAll + public static void setUp() throws IOException { + tempDir = Files.createTempDirectory(ClientTlsAuthTestBase.class.getSimpleName()); + copyCertsToTmpDir(); + } + + @AfterAll + public static void teardown() throws IOException { + Files.deleteIfExists(serverKeyStore); + Files.deleteIfExists(clientKeyStore); + Files.deleteIfExists(tempDir); + } + + private static void copyCertsToTmpDir() throws IOException { + InputStream sksStream = ClientTlsAuthTestBase.class.getResourceAsStream(TEST_KEY_STORE); + Path sks = copyToTmpDir(sksStream, "server-keystore"); + + InputStream cksStream = ClientTlsAuthTestBase.class.getResourceAsStream(CLIENT_KEY_STORE); + Path cks = copyToTmpDir(cksStream, "client1.p12"); + + serverKeyStore = sks; + clientKeyStore = cks; + } + + private static Path copyToTmpDir(InputStream srcStream, String name) throws IOException { + Path dst = tempDir.resolve(name); + Files.copy(srcStream, dst); + return dst; + } +} diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/MetricReportingTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/MetricReportingTest.java new file mode 100644 index 000000000000..4e192e867b1f --- /dev/null +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/MetricReportingTest.java @@ -0,0 +1,130 @@ +/* + * 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.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static software.amazon.awssdk.http.HttpMetric.AVAILABLE_CONCURRENCY; +import static software.amazon.awssdk.http.HttpMetric.HTTP_CLIENT_NAME; +import static software.amazon.awssdk.http.HttpMetric.LEASED_CONCURRENCY; +import static software.amazon.awssdk.http.HttpMetric.MAX_CONCURRENCY; +import static software.amazon.awssdk.http.HttpMetric.PENDING_CONCURRENCY_ACQUIRES; + +import java.io.IOException; +import java.time.Duration; +import org.apache.hc.client5.http.classic.methods.HttpUriRequest; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.core5.http.HttpVersion; +import org.apache.hc.core5.http.message.BasicHttpResponse; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.pool.PoolStats; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import software.amazon.awssdk.http.HttpExecuteRequest; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.apache5.internal.Apache5HttpRequestConfig; +import software.amazon.awssdk.http.apache5.internal.impl.ConnectionManagerAwareHttpClient; +import software.amazon.awssdk.metrics.MetricCollection; +import software.amazon.awssdk.metrics.MetricCollector; +import software.amazon.awssdk.utils.AttributeMap; + +@RunWith(MockitoJUnitRunner.class) +public class MetricReportingTest { + + @Mock + public ConnectionManagerAwareHttpClient mockHttpClient; + + @Mock + public PoolingHttpClientConnectionManager cm; + + @Before + public void methodSetup() throws IOException { + when(mockHttpClient.execute(any(HttpUriRequest.class), any(HttpContext.class))) + .thenReturn(new BasicHttpResponse(HttpVersion.HTTP_1_1, 200, "OK")); + when(mockHttpClient.getHttpClientConnectionManager()).thenReturn(cm); + + PoolStats stats = new PoolStats(1, 2, 3, 4); + when(cm.getTotalStats()).thenReturn(stats); + } + + @Test + public void prepareRequest_callableCalled_metricsReported() throws IOException { + ApacheHttpClient client = newClient(); + MetricCollector collector = MetricCollector.create("test"); + HttpExecuteRequest executeRequest = newRequest(collector); + + client.prepareRequest(executeRequest).call(); + + MetricCollection collected = collector.collect(); + + assertThat(collected.metricValues(HTTP_CLIENT_NAME)).containsExactly("Apache5"); + assertThat(collected.metricValues(LEASED_CONCURRENCY)).containsExactly(1); + assertThat(collected.metricValues(PENDING_CONCURRENCY_ACQUIRES)).containsExactly(2); + assertThat(collected.metricValues(AVAILABLE_CONCURRENCY)).containsExactly(3); + assertThat(collected.metricValues(MAX_CONCURRENCY)).containsExactly(4); + } + + @Test + public void prepareRequest_connectionManagerNotPooling_callableCalled_metricsReported() throws IOException { + ApacheHttpClient client = newClient(); + when(mockHttpClient.getHttpClientConnectionManager()).thenReturn(mock(HttpClientConnectionManager.class)); + MetricCollector collector = MetricCollector.create("test"); + HttpExecuteRequest executeRequest = newRequest(collector); + + client.prepareRequest(executeRequest).call(); + + MetricCollection collected = collector.collect(); + + assertThat(collected.metricValues(HTTP_CLIENT_NAME)).containsExactly("Apache5"); + assertThat(collected.metricValues(LEASED_CONCURRENCY)).isEmpty(); + assertThat(collected.metricValues(PENDING_CONCURRENCY_ACQUIRES)).isEmpty(); + assertThat(collected.metricValues(AVAILABLE_CONCURRENCY)).isEmpty(); + assertThat(collected.metricValues(MAX_CONCURRENCY)).isEmpty(); + } + + private ApacheHttpClient newClient() { + Apache5HttpRequestConfig config = Apache5HttpRequestConfig.builder() + .connectionAcquireTimeout(Duration.ofDays(1)) + .connectionTimeout(Duration.ofDays(1)) + .socketTimeout(Duration.ofDays(1)) + .proxyConfiguration(ProxyConfiguration.builder().build()) + .build(); + + return new ApacheHttpClient(mockHttpClient, config, AttributeMap.empty()); + } + + private HttpExecuteRequest newRequest(MetricCollector collector) { + final SdkHttpFullRequest sdkRequest = SdkHttpFullRequest.builder() + .method(SdkHttpMethod.HEAD) + .host("amazonaws.com") + .protocol("https") + .build(); + + HttpExecuteRequest executeRequest = HttpExecuteRequest.builder() + .request(sdkRequest) + .metricCollector(collector) + .build(); + + return executeRequest; + } +} diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ProxyConfigurationTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ProxyConfigurationTest.java new file mode 100644 index 000000000000..85368704bd56 --- /dev/null +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/ProxyConfigurationTest.java @@ -0,0 +1,216 @@ +/* + * 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.assertThat; + +import java.net.URI; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.testutils.EnvironmentVariableHelper; + +public class ProxyConfigurationTest { + + private static final EnvironmentVariableHelper ENVIRONMENT_VARIABLE_HELPER = new EnvironmentVariableHelper(); + + @BeforeEach + public void setup() { + clearProxyProperties(); + ENVIRONMENT_VARIABLE_HELPER.reset(); + } + + @AfterAll + public static void cleanup() { + clearProxyProperties(); + ENVIRONMENT_VARIABLE_HELPER.reset(); + } + + @Test + void testEndpointValues_Http_SystemPropertyEnabled() { + String host = "foo.com"; + int port = 7777; + System.setProperty("http.proxyHost", host); + System.setProperty("http.proxyPort", Integer.toString(port)); + ENVIRONMENT_VARIABLE_HELPER.set("http_proxy", "http://UserOne:passwordSecret@bar.com:555/"); + ProxyConfiguration config = ProxyConfiguration.builder().useSystemPropertyValues(true).build(); + + assertThat(config.host()).isEqualTo(host); + assertThat(config.port()).isEqualTo(port); + assertThat(config.scheme()).isEqualTo("http"); + + } + + @Test + void testEndpointValues_Http_EnvironmentVariableEnabled() { + String host = "bar.com"; + int port = 7777; + System.setProperty("http.proxyHost", "foo.com"); + System.setProperty("http.proxyPort", Integer.toString(8888)); + + ENVIRONMENT_VARIABLE_HELPER.set("http_proxy", String.format("http://%s:%d/", host, port)); + + ProxyConfiguration config = + ProxyConfiguration.builder().useSystemPropertyValues(false).useEnvironmentVariableValues(true).build(); + + assertThat(config.host()).isEqualTo(host); + assertThat(config.port()).isEqualTo(port); + assertThat(config.scheme()).isEqualTo("http"); + } + + @Test + void testEndpointValues_Https_SystemPropertyEnabled() { + String host = "foo.com"; + int port = 7777; + System.setProperty("https.proxyHost", host); + System.setProperty("https.proxyPort", Integer.toString(port)); + + ProxyConfiguration config = ProxyConfiguration.builder() + .endpoint(URI.create("https://foo.com:7777")) + .useSystemPropertyValues(true).build(); + + assertThat(config.host()).isEqualTo(host); + assertThat(config.port()).isEqualTo(port); + assertThat(config.scheme()).isEqualTo("https"); + } + + + @Test + void testEndpointValues_Https_EnvironmentVariableEnabled() { + String host = "bar.com"; + int port = 7777; + System.setProperty("https.proxyHost", "foo.com"); + System.setProperty("https.proxyPort", Integer.toString(8888)); + + ENVIRONMENT_VARIABLE_HELPER.set("http_proxy", String.format("http://%s:%d/", "foo.com", 8888)); + ENVIRONMENT_VARIABLE_HELPER.set("https_proxy", String.format("http://%s:%d/", host, port)); + + ProxyConfiguration config = + ProxyConfiguration.builder() + .scheme("https") + .useSystemPropertyValues(false) + .useEnvironmentVariableValues(true) + .build(); + + assertThat(config.host()).isEqualTo(host); + assertThat(config.port()).isEqualTo(port); + assertThat(config.scheme()).isEqualTo("https"); + } + + + @Test + void testEndpointValues_SystemPropertyDisabled() { + ProxyConfiguration config = ProxyConfiguration.builder() + .endpoint(URI.create("http://localhost:1234")) + .useSystemPropertyValues(Boolean.FALSE) + .build(); + + assertThat(config.host()).isEqualTo("localhost"); + assertThat(config.port()).isEqualTo(1234); + assertThat(config.scheme()).isEqualTo("http"); + } + + @Test + void testProxyConfigurationWithSystemPropertyDisabled() throws Exception { + Set nonProxyHosts = new HashSet<>(); + nonProxyHosts.add("foo.com"); + + // system property should not be used + System.setProperty("http.proxyHost", "foo.com"); + System.setProperty("http.proxyPort", "5555"); + System.setProperty("http.nonProxyHosts", "bar.com"); + System.setProperty("http.proxyUser", "user"); + + ProxyConfiguration config = ProxyConfiguration.builder() + .endpoint(URI.create("http://localhost:1234")) + .nonProxyHosts(nonProxyHosts) + .useSystemPropertyValues(Boolean.FALSE) + .build(); + + assertThat(config.host()).isEqualTo("localhost"); + assertThat(config.port()).isEqualTo(1234); + assertThat(config.nonProxyHosts()).isEqualTo(nonProxyHosts); + assertThat(config.username()).isNull(); + } + + @Test + void testProxyConfigurationWithSystemPropertyEnabled_Http() throws Exception { + Set nonProxyHosts = new HashSet<>(); + nonProxyHosts.add("foo.com"); + + // system property should not be used + System.setProperty("http.proxyHost", "foo.com"); + System.setProperty("http.proxyPort", "5555"); + System.setProperty("http.nonProxyHosts", "bar.com"); + System.setProperty("http.proxyUser", "user"); + + ProxyConfiguration config = ProxyConfiguration.builder() + .nonProxyHosts(nonProxyHosts) + .build(); + + assertThat(config.nonProxyHosts()).isEqualTo(nonProxyHosts); + assertThat(config.host()).isEqualTo("foo.com"); + assertThat(config.username()).isEqualTo("user"); + } + + @Test + void testProxyConfigurationWithSystemPropertyEnabled_Https() throws Exception { + Set nonProxyHosts = new HashSet<>(); + nonProxyHosts.add("foo.com"); + + // system property should not be used + System.setProperty("https.proxyHost", "foo.com"); + System.setProperty("https.proxyPort", "5555"); + System.setProperty("http.nonProxyHosts", "bar.com"); + System.setProperty("https.proxyUser", "user"); + + ProxyConfiguration config = ProxyConfiguration.builder() + .endpoint(URI.create("https://foo.com:1234")) + .nonProxyHosts(nonProxyHosts) + .build(); + + assertThat(config.nonProxyHosts()).isEqualTo(nonProxyHosts); + assertThat(config.host()).isEqualTo("foo.com"); + assertThat(config.username()).isEqualTo("user"); + } + + @Test + void testProxyConfigurationWithoutNonProxyHosts_toBuilder_shouldNotThrowNPE() { + ProxyConfiguration proxyConfiguration = + ProxyConfiguration.builder() + .endpoint(URI.create("http://localhost:4321")) + .username("username") + .password("password") + .build(); + + assertThat(proxyConfiguration.toBuilder()).isNotNull(); + } + + private static void clearProxyProperties() { + System.clearProperty("http.proxyHost"); + System.clearProperty("http.proxyPort"); + System.clearProperty("http.nonProxyHosts"); + System.clearProperty("http.proxyUser"); + System.clearProperty("http.proxyPassword"); + + System.clearProperty("https.proxyHost"); + System.clearProperty("https.proxyPort"); + System.clearProperty("https.proxyUser"); + System.clearProperty("https.proxyPassword"); + } +} diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/SdkProxyRoutePlannerTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/SdkProxyRoutePlannerTest.java new file mode 100644 index 000000000000..f442f7ed7be7 --- /dev/null +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/SdkProxyRoutePlannerTest.java @@ -0,0 +1,52 @@ +/* + * 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;import software.amazon.awssdk.http.apache5.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collections; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.protocol.HttpClientContext; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpHost; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link SdkProxyRoutePlanner}. + */ +public class SdkProxyRoutePlannerTest { + private static final HttpHost S3_HOST = new HttpHost("https", "s3.us-west-2.amazonaws.com", 443); + private static final HttpGet S3_REQUEST = new HttpGet("/my-bucket/my-object"); + private static final HttpClientContext CONTEXT = new HttpClientContext(); + + @Test + public void testSetsCorrectSchemeBasedOnProcotol_HTTPS() throws HttpException { + SdkProxyRoutePlanner planner = new SdkProxyRoutePlanner("localhost", 1234, "https", Collections.emptySet()); + + HttpHost proxyHost = planner.determineRoute(S3_HOST, S3_REQUEST, CONTEXT).getProxyHost(); + assertEquals("localhost", proxyHost.getHostName()); + assertEquals("https", proxyHost.getSchemeName()); + } + + @Test + public void testSetsCorrectSchemeBasedOnProcotol_HTTP() throws HttpException { + SdkProxyRoutePlanner planner = new SdkProxyRoutePlanner("localhost", 1234, "http", Collections.emptySet()); + + HttpHost proxyHost = planner.determineRoute(S3_HOST, S3_REQUEST, CONTEXT).getProxyHost(); + assertEquals("localhost", proxyHost.getHostName()); + assertEquals("http", proxyHost.getSchemeName()); + } +} diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/conn/ClientConnectionManagerFactoryTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/conn/ClientConnectionManagerFactoryTest.java new file mode 100644 index 000000000000..7b013db027f7 --- /dev/null +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/conn/ClientConnectionManagerFactoryTest.java @@ -0,0 +1,81 @@ +/* + * 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.conn;import software.amazon.awssdk.http.apache5.internal.conn; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import org.apache.hc.client5.http.ConnectionRequest; +import org.apache.hc.client5.http.HttpRoute; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.core5.http.io.HttpClientConnection; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.junit.Test; + +public class ClientConnectionManagerFactoryTest { + HttpClientConnectionManager noop = new HttpClientConnectionManager() { + @Override + public void connect(HttpClientConnection conn, HttpRoute route, int connectTimeout, HttpContext context) throws + IOException { + + } + + @Override + public void upgrade(HttpClientConnection conn, HttpRoute route, HttpContext context) throws IOException { + + } + + @Override + public void routeComplete(HttpClientConnection conn, HttpRoute route, HttpContext context) throws IOException { + + } + + @Override + public ConnectionRequest requestConnection(HttpRoute route, + Object state) { + return null; + } + + @Override + public void releaseConnection(HttpClientConnection conn, + Object newState, + long validDuration, + TimeUnit timeUnit) { + } + + @Override + public void closeIdleConnections(long idletime, TimeUnit tunit) { + } + + @Override + public void closeExpiredConnections() { + } + + @Override + public void shutdown() { + } + }; + + @Test + public void wrapOnce() { + ClientConnectionManagerFactory.wrap(noop); + } + + @Test(expected = IllegalArgumentException.class) + public void wrapTwice() { + HttpClientConnectionManager wrapped = ClientConnectionManagerFactory.wrap(noop); + ClientConnectionManagerFactory.wrap(wrapped); + } +} diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/conn/IdleConnectionReaperTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/conn/IdleConnectionReaperTest.java new file mode 100644 index 000000000000..510ba047a11e --- /dev/null +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/conn/IdleConnectionReaperTest.java @@ -0,0 +1,96 @@ +/* + * 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.conn;import software.amazon.awssdk.http.apache5.internal.conn; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** + * Tests for {@link IdleConnectionReaper}. + */ +@RunWith(MockitoJUnitRunner.class) +public class IdleConnectionReaperTest { + private static final long SLEEP_PERIOD = 250; + + private final Map connectionManagers = new HashMap<>(); + + @Mock + public ExecutorService executorService; + + @Mock + public HttpClientConnectionManager connectionManager; + + private IdleConnectionReaper idleConnectionReaper; + + @Before + public void methodSetup() { + this.connectionManagers.clear(); + idleConnectionReaper = new IdleConnectionReaper(connectionManagers, () -> executorService, SLEEP_PERIOD); + } + + @Test + public void setsUpExecutorIfManagerNotPreviouslyRegistered() { + idleConnectionReaper.registerConnectionManager(connectionManager, 1L); + verify(executorService).execute(any(Runnable.class)); + } + + @Test + public void shutsDownExecutorIfMapEmptied() { + // use register method so it sets up the executor + idleConnectionReaper.registerConnectionManager(connectionManager, 1L); + idleConnectionReaper.deregisterConnectionManager(connectionManager); + verify(executorService).shutdownNow(); + } + + @Test + public void doesNotShutDownExecutorIfNoManagerRemoved() { + idleConnectionReaper.registerConnectionManager(connectionManager, 1L); + HttpClientConnectionManager someOtherConnectionManager = mock(HttpClientConnectionManager.class); + idleConnectionReaper.deregisterConnectionManager(someOtherConnectionManager); + verify(executorService, times(0)).shutdownNow(); + } + + @Test(timeout = 1000L) + public void testReapsConnections() throws InterruptedException { + IdleConnectionReaper reaper = new IdleConnectionReaper(new HashMap<>(), + Executors::newSingleThreadExecutor, + SLEEP_PERIOD); + final long idleTime = 1L; + reaper.registerConnectionManager(connectionManager, idleTime); + try { + Thread.sleep(SLEEP_PERIOD * 2); + verify(connectionManager, atLeastOnce()).closeIdleConnections(eq(idleTime), eq(TimeUnit.MILLISECONDS)); + } finally { + reaper.deregisterConnectionManager(connectionManager); + } + } +} diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/conn/InputShutdownCheckingSslSocketTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/conn/InputShutdownCheckingSslSocketTest.java new file mode 100644 index 000000000000..8c2c7ce8e356 --- /dev/null +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/conn/InputShutdownCheckingSslSocketTest.java @@ -0,0 +1,118 @@ +/* + * 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.conn; + +import software.amazon.awssdk.http.apache5.internal.conn; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.OutputStream; +import javax.net.ssl.SSLSocket; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.http.apache5.internal.net.InputShutdownCheckingSslSocket; + +public class InputShutdownCheckingSslSocketTest { + + @Test + public void outputStreamChecksInputShutdown() throws IOException { + SSLSocket mockSocket = mock(SSLSocket.class); + when(mockSocket.isInputShutdown()).thenReturn(true); + InputShutdownCheckingSslSocket socket = new InputShutdownCheckingSslSocket(mockSocket); + OutputStream os = socket.getOutputStream(); + assertThrows(IOException.class, () -> os.write(1)); + } + + @Test + public void outputStreamWritesNormallyWhenInputNotShutdown() throws IOException { + SSLSocket mockSocket = mock(SSLSocket.class); + OutputStream mockOutputStream = mock(OutputStream.class); + when(mockSocket.isInputShutdown()).thenReturn(false); + when(mockSocket.getOutputStream()).thenReturn(mockOutputStream); + InputShutdownCheckingSslSocket socket = new InputShutdownCheckingSslSocket(mockSocket); + OutputStream os = socket.getOutputStream(); + os.write(1); + verify(mockOutputStream).write(1); + } + + @Test + public void writeByteArrayThrowsIOExceptionWhenInputIsShutdown() throws IOException { + SSLSocket mockSocket = mock(SSLSocket.class); + when(mockSocket.isInputShutdown()).thenReturn(true); + InputShutdownCheckingSslSocket socket = new InputShutdownCheckingSslSocket(mockSocket); + OutputStream os = socket.getOutputStream(); + assertThrows(IOException.class, () -> os.write(new byte[10])); + } + + @Test + public void writeByteArraySucceedsWhenInputNotShutdown() throws IOException { + SSLSocket mockSocket = mock(SSLSocket.class); + OutputStream mockOutputStream = mock(OutputStream.class); + when(mockSocket.isInputShutdown()).thenReturn(false); + when(mockSocket.getOutputStream()).thenReturn(mockOutputStream); + + InputShutdownCheckingSslSocket socket = new InputShutdownCheckingSslSocket(mockSocket); + OutputStream os = socket.getOutputStream(); + + byte[] data = new byte[10]; + os.write(data); + verify(mockOutputStream).write(data); + } + + @Test + public void writeByteArrayWithOffsetThrowsIOExceptionWhenInputIsShutdown() throws IOException { + SSLSocket mockSocket = mock(SSLSocket.class); + when(mockSocket.isInputShutdown()).thenReturn(true); + + InputShutdownCheckingSslSocket socket = new InputShutdownCheckingSslSocket(mockSocket); + OutputStream os = socket.getOutputStream(); + + assertThrows(IOException.class, () -> os.write(new byte[10], 0, 10)); + } + + @Test + public void writeByteArrayWithOffsetSucceedsWhenInputNotShutdown() throws IOException { + SSLSocket mockSocket = mock(SSLSocket.class); + OutputStream mockOutputStream = mock(OutputStream.class); + when(mockSocket.isInputShutdown()).thenReturn(false); + when(mockSocket.getOutputStream()).thenReturn(mockOutputStream); + + InputShutdownCheckingSslSocket socket = new InputShutdownCheckingSslSocket(mockSocket); + OutputStream os = socket.getOutputStream(); + + byte[] data = new byte[10]; + os.write(data, 0, 10); + verify(mockOutputStream).write(data, 0, 10); + } + + @Test + public void checkInputShutdownThrowsIOExceptionWithSuppressed() throws IOException { + SSLSocket mockSocket = mock(SSLSocket.class); + when(mockSocket.isInputShutdown()).thenReturn(false); + when(mockSocket.getInputStream()).thenThrow(new IOException("InputStream exception")); + + InputShutdownCheckingSslSocket socket = new InputShutdownCheckingSslSocket(mockSocket); + OutputStream os = socket.getOutputStream(); + + IOException thrown = assertThrows(IOException.class, () -> os.write(1)); + assertTrue(thrown.getMessage().contains("Remote end is closed.")); + assertTrue(thrown.getSuppressed()[0].getMessage().contains("InputStream exception")); + } +} \ No newline at end of file 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 new file mode 100644 index 000000000000..2d74ad48ca8d --- /dev/null +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/conn/SdkTlsSocketFactoryTest.java @@ -0,0 +1,91 @@ +/* + * 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.conn;import software.amazon.awssdk.http.apache5.internal.conn; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class SdkTlsSocketFactoryTest { + + SdkTlsSocketFactory factory; + SSLSocket socket; + + @BeforeEach + public void before() throws Exception { + factory = new SdkTlsSocketFactory(SSLContext.getDefault(), null); + socket = Mockito.mock(SSLSocket.class); + } + + @Test + void nullProtocols() { + when(socket.getSupportedProtocols()).thenReturn(null); + when(socket.getEnabledProtocols()).thenReturn(null); + + factory.prepareSocket(socket); + + verify(socket, never()).setEnabledProtocols(any()); + } + + @Test + void amazonCorretto_8_0_292_defaultEnabledProtocols() { + when(socket.getSupportedProtocols()).thenReturn(new String[] { + "TLSv1.3", "TLSv1.2", "TLSv1.1", "TLSv1", "SSLv3", "SSLv2Hello" + }); + when(socket.getEnabledProtocols()).thenReturn(new String[] { + "TLSv1.2", "TLSv1.1", "TLSv1" + }); + + factory.prepareSocket(socket); + + verify(socket, never()).setEnabledProtocols(any()); + } + + @Test + void amazonCorretto_11_0_08_defaultEnabledProtocols() { + when(socket.getSupportedProtocols()).thenReturn(new String[] { + "TLSv1.3", "TLSv1.2", "TLSv1.1", "TLSv1", "SSLv3", "SSLv2Hello" + }); + when(socket.getEnabledProtocols()).thenReturn(new String[] { + "TLSv1.3", "TLSv1.2", "TLSv1.1", "TLSv1" + }); + + factory.prepareSocket(socket); + + verify(socket, never()).setEnabledProtocols(any()); + } + + @Test + void amazonCorretto_17_0_1_defaultEnabledProtocols() { + when(socket.getSupportedProtocols()).thenReturn(new String[] { + "TLSv1.3", "TLSv1.2", "TLSv1.1", "TLSv1", "SSLv3", "SSLv2Hello" + }); + when(socket.getEnabledProtocols()).thenReturn(new String[] { + "TLSv1.3", "TLSv1.2" + }); + + factory.prepareSocket(socket); + + verify(socket, never()).setEnabledProtocols(any()); + } +} diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/impl/ApacheHttpRequestFactoryTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/impl/ApacheHttpRequestFactoryTest.java new file mode 100644 index 000000000000..8bad950f552c --- /dev/null +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/impl/ApacheHttpRequestFactoryTest.java @@ -0,0 +1,198 @@ +/* + * 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.impl;import software.amazon.awssdk.http.apache5.internal.impl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import org.apache.hc.client5.http.classic.methods.HttpEntityEnclosingRequestBase; +import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.io.entity.BufferedHttpEntity; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.http.HttpExecuteRequest; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.apache5.internal.Apache5HttpRequestConfig; +import software.amazon.awssdk.http.apache5.internal.RepeatableInputStreamRequestEntity; + +public class ApacheHttpRequestFactoryTest { + + private Apache5HttpRequestConfig requestConfig; + private ApacheHttpRequestFactory instance; + + @BeforeEach + public void setup() { + instance = new ApacheHttpRequestFactory(); + requestConfig = Apache5HttpRequestConfig.builder() + .connectionAcquireTimeout(Duration.ZERO) + .connectionTimeout(Duration.ZERO) + .localAddress(InetAddress.getLoopbackAddress()) + .socketTimeout(Duration.ZERO) + .build(); + } + + @Test + public void createSetsHostHeaderByDefault() { + SdkHttpRequest sdkRequest = SdkHttpRequest.builder() + .uri(URI.create("http://localhost:12345/")) + .method(SdkHttpMethod.HEAD) + .build(); + HttpExecuteRequest request = HttpExecuteRequest.builder() + .request(sdkRequest) + .build(); + HttpUriRequestBase result = instance.create(request, requestConfig); + Header[] hostHeaders = result.getHeaders(HttpHeaders.HOST); + assertNotNull(hostHeaders); + assertEquals(1, hostHeaders.length); + assertEquals("localhost:12345", hostHeaders[0].getValue()); + } + + @Test + public void createRespectsUserHostHeader() { + String hostOverride = "virtual.host:123"; + SdkHttpRequest sdkRequest = SdkHttpRequest.builder() + .uri(URI.create("http://localhost:12345/")) + .method(SdkHttpMethod.HEAD) + .putHeader("Host", hostOverride) + .build(); + HttpExecuteRequest request = HttpExecuteRequest.builder() + .request(sdkRequest) + .build(); + + HttpUriRequestBase result = instance.create(request, requestConfig); + + Header[] hostHeaders = result.getHeaders(HttpHeaders.HOST); + assertNotNull(hostHeaders); + assertEquals(1, hostHeaders.length); + assertEquals(hostOverride, hostHeaders[0].getValue()); + } + + @Test + public void createRespectsLowercaseUserHostHeader() { + String hostOverride = "virtual.host:123"; + SdkHttpRequest sdkRequest = SdkHttpRequest.builder() + .uri(URI.create("http://localhost:12345/")) + .method(SdkHttpMethod.HEAD) + .putHeader("host", hostOverride) + .build(); + HttpExecuteRequest request = HttpExecuteRequest.builder() + .request(sdkRequest) + .build(); + + HttpUriRequestBase result = instance.create(request, requestConfig); + + Header[] hostHeaders = result.getHeaders(HttpHeaders.HOST); + assertNotNull(hostHeaders); + assertEquals(1, hostHeaders.length); + assertEquals(hostOverride, hostHeaders[0].getValue()); + } + + @Test + public void putRequest_withTransferEncodingChunked_isChunkedAndDoesNotIncludeHeader() { + SdkHttpRequest sdkRequest = SdkHttpRequest.builder() + .uri(URI.create("http://localhost:12345/")) + .method(SdkHttpMethod.PUT) + .putHeader("Transfer-Encoding", "chunked") + .build(); + InputStream inputStream = new ByteArrayInputStream("TestStream".getBytes(StandardCharsets.UTF_8)); + HttpExecuteRequest request = HttpExecuteRequest.builder() + .request(sdkRequest) + .contentStreamProvider(() -> inputStream) + .build(); + HttpUriRequestBase result = instance.create(request, requestConfig); + Header[] transferEncodingHeaders = result.getHeaders("Transfer-Encoding"); + assertThat(transferEncodingHeaders).isEmpty(); + + HttpEntityEnclosingRequestBase enclosingRequest = (HttpEntityEnclosingRequestBase) result; + HttpEntity httpEntity = enclosingRequest.getEntity(); + assertThat(httpEntity.isChunked()).isTrue(); + assertThat(httpEntity).isNotInstanceOf(BufferedHttpEntity.class); + assertThat(httpEntity).isInstanceOf(RepeatableInputStreamRequestEntity.class); + } + + @Test + public void defaultHttpPortsAreNotInDefaultHostHeader() { + SdkHttpRequest sdkRequest = SdkHttpRequest.builder() + .uri(URI.create("http://localhost:80/")) + .method(SdkHttpMethod.HEAD) + .build(); + HttpExecuteRequest request = HttpExecuteRequest.builder() + .request(sdkRequest) + .build(); + HttpUriRequestBase result = instance.create(request, requestConfig); + Header[] hostHeaders = result.getHeaders(HttpHeaders.HOST); + assertNotNull(hostHeaders); + assertEquals(1, hostHeaders.length); + assertEquals("localhost", hostHeaders[0].getValue()); + + sdkRequest = SdkHttpRequest.builder() + .uri(URI.create("https://localhost:443/")) + .method(SdkHttpMethod.HEAD) + .build(); + request = HttpExecuteRequest.builder() + .request(sdkRequest) + .build(); + result = instance.create(request, requestConfig); + hostHeaders = result.getHeaders(HttpHeaders.HOST); + assertNotNull(hostHeaders); + assertEquals(1, hostHeaders.length); + assertEquals("localhost", hostHeaders[0].getValue()); + } + + @Test + public void pathWithLeadingSlash_shouldEncode() { + assertThat(sanitizedUri("/foobar")).isEqualTo("http://localhost/%2Ffoobar"); + } + + @Test + public void pathWithOnlySlash_shouldEncode() { + assertThat(sanitizedUri("/")).isEqualTo("http://localhost/%2F"); + } + + @Test + public void pathWithoutSlash_shouldReturnSameUri() { + assertThat(sanitizedUri("path")).isEqualTo("http://localhost/path"); + } + + @Test + public void pathWithSpecialChars_shouldPreserveEncoding() { + assertThat(sanitizedUri("/special-chars-%40%24%25")).isEqualTo("http://localhost/%2Fspecial-chars-%40%24%25"); + } + + private String sanitizedUri(String path) { + SdkHttpRequest sdkRequest = SdkHttpRequest.builder() + .uri(URI.create("http://localhost:80")) + .encodedPath("/" + path) + .method(SdkHttpMethod.HEAD) + .build(); + HttpExecuteRequest request = HttpExecuteRequest.builder() + .request(sdkRequest) + .build(); + + return instance.create(request, requestConfig).getUri().toString(); + } +} diff --git a/http-clients/apache5-client/src/test/resources/apache/client1.p12 b/http-clients/apache5-client/src/test/resources/apache/client1.p12 new file mode 100644 index 000000000000..a56e38c196b5 Binary files /dev/null and b/http-clients/apache5-client/src/test/resources/apache/client1.p12 differ diff --git a/http-clients/apache5-client/src/test/resources/apache/server-keystore b/http-clients/apache5-client/src/test/resources/apache/server-keystore new file mode 100644 index 000000000000..55e8a7998c2d Binary files /dev/null and b/http-clients/apache5-client/src/test/resources/apache/server-keystore differ