/* * 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.urlconnection; import static software.amazon.awssdk.http.Header.ACCEPT; import static software.amazon.awssdk.http.Header.CONTENT_LENGTH; import static software.amazon.awssdk.http.HttpStatusFamily.CLIENT_ERROR; import static software.amazon.awssdk.http.HttpStatusFamily.SERVER_ERROR; import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely; import static software.amazon.awssdk.utils.NumericUtils.saturatedCast; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UncheckedIOException; import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.URI; import java.nio.charset.StandardCharsets; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.cert.X509Certificate; import java.time.Duration; import java.util.Base64; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Collectors; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.http.AbortableInputStream; import software.amazon.awssdk.http.ContentStreamProvider; import software.amazon.awssdk.http.ExecutableHttpRequest; import software.amazon.awssdk.http.HttpExecuteRequest; import software.amazon.awssdk.http.HttpExecuteResponse; import software.amazon.awssdk.http.HttpStatusFamily; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.SdkHttpConfigurationOption; import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.SdkHttpResponse; import software.amazon.awssdk.http.TlsKeyManagersProvider; import software.amazon.awssdk.http.TlsTrustManagersProvider; import software.amazon.awssdk.utils.AttributeMap; import software.amazon.awssdk.utils.IoUtils; import software.amazon.awssdk.utils.Logger; import software.amazon.awssdk.utils.StringUtils; import software.amazon.awssdk.utils.Validate; /** * An implementation of {@link SdkHttpClient} that uses {@link HttpURLConnection} to communicate with the service. This is the * leanest synchronous client that optimizes for minimum dependencies and startup latency in exchange for having less * functionality than other implementations. * *
See software.amazon.awssdk.http.apache.ApacheHttpClient for an alternative implementation.
* *This can be created via {@link #builder()}
*/ @SdkPublicApi public final class UrlConnectionHttpClient implements SdkHttpClient { private static final Logger log = Logger.loggerFor(UrlConnectionHttpClient.class); private static final String CLIENT_NAME = "UrlConnection"; private final AttributeMap options; private final UrlConnectionFactory connectionFactory; private final ProxyConfiguration proxyConfiguration; private UrlConnectionHttpClient(AttributeMap options, UrlConnectionFactory connectionFactory, DefaultBuilder builder) { this.options = options; this.proxyConfiguration = builder != null ? builder.proxyConfiguration : null; if (connectionFactory != null) { this.connectionFactory = connectionFactory; } else { // Note: This socket factory MUST be reused between requests because the connection pool in the JVM is keyed by both // URL and SSLSocketFactory. If the socket factory is not reused, connections will not be reused between requests. SSLSocketFactory socketFactory = getSslContext(options).getSocketFactory(); this.connectionFactory = url -> createDefaultConnection(url, socketFactory); } } private UrlConnectionHttpClient(AttributeMap options, UrlConnectionFactory connectionFactory) { this(options, connectionFactory, null); } public static Builder builder() { return new DefaultBuilder(); } /** * Create a {@link HttpURLConnection} client with the default properties * * @return an {@link UrlConnectionHttpClient} */ public static SdkHttpClient create() { return new DefaultBuilder().build(); } /** * Use this method if you want to control the way a {@link HttpURLConnection} is created. * This will ignore SDK defaults like {@link SdkHttpConfigurationOption#CONNECTION_TIMEOUT} * and {@link SdkHttpConfigurationOption#READ_TIMEOUT} * @param connectionFactory a function that, given a {@link URI} will create an {@link HttpURLConnection} * @return an {@link UrlConnectionHttpClient} */ public static SdkHttpClient create(UrlConnectionFactory connectionFactory) { return new UrlConnectionHttpClient(AttributeMap.empty(), connectionFactory); } @Override public ExecutableHttpRequest prepareRequest(HttpExecuteRequest request) { HttpURLConnection connection = createAndConfigureConnection(request); return new RequestCallable(connection, request); } @Override public void close() { // Nothing to close. The connections will be closed by closing the InputStreams. } @Override public String clientName() { return CLIENT_NAME; } private HttpURLConnection createAndConfigureConnection(HttpExecuteRequest request) { SdkHttpRequest sdkHttpRequest = request.httpRequest(); HttpURLConnection connection = connectionFactory.createConnection(sdkHttpRequest.getUri()); sdkHttpRequest.forEachHeader((key, values) -> values.forEach(value -> connection.setRequestProperty(key, value))); // connection.setRequestProperty("Transfer-Encoding", "chunked") does not work, i.e., property does not get set if (sdkHttpRequest.matchingHeaders("Transfer-Encoding").contains("chunked")) { connection.setChunkedStreamingMode(-1); } if (!sdkHttpRequest.firstMatchingHeader(ACCEPT).isPresent()) { // Override Accept header because the default one in JDK does not comply with RFC 7231 // See: https://bugs.openjdk.org/browse/JDK-8163921 connection.setRequestProperty(ACCEPT, "*/*"); } invokeSafely(() -> connection.setRequestMethod(sdkHttpRequest.method().name())); if (request.contentStreamProvider().isPresent()) { connection.setDoOutput(true); } // Disable following redirects since it breaks SDK error handling and matches Apache. // See: https://github.com/aws/aws-sdk-java-v2/issues/975 connection.setInstanceFollowRedirects(false); sdkHttpRequest.firstMatchingHeader(CONTENT_LENGTH).map(Long::parseLong) .ifPresent(connection::setFixedLengthStreamingMode); return connection; } private HttpURLConnection createDefaultConnection(URI uri, SSLSocketFactory socketFactory) { Optional
* TODO: Determine precise root cause of intermittent NPEs, submit JDK bug report if applicable, and consider applying
* this behavior only on unpatched JVM runtime versions.
*/
private static int getResponseCodeSafely(HttpURLConnection connection) throws IOException {
Validate.paramNotNull(connection, "connection");
try {
return connection.getResponseCode();
} catch (NullPointerException e) {
throw new IOException("Unexpected NullPointerException when trying to read response from HttpURLConnection", e);
}
}
private Map
* SdkHttpClient httpClient = UrlConnectionHttpClient.builder()
* .socketTimeout(Duration.ofSeconds(10))
* .connectionTimeout(Duration.ofSeconds(1))
* .build();
*
*/
public interface Builder extends SdkHttpClient.Builder