Skip to content

Broke up UrlConnectionHttpClient into multiple classes. Hopefully this is an improvement? #3054

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* 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.internal;

import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;
import static software.amazon.awssdk.utils.NumericUtils.saturatedCast;

import java.net.HttpURLConnection;
import java.net.URI;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
import software.amazon.awssdk.http.TlsKeyManagersProvider;
import software.amazon.awssdk.http.urlconnection.UrlConnectionFactory;
import software.amazon.awssdk.utils.AttributeMap;
import software.amazon.awssdk.utils.Logger;
import software.amazon.awssdk.utils.Validate;

/**
* The default implementation of {@link UrlConnectionFactory}.
*/
@SdkInternalApi
public class DefaultUrlConnectionFactory implements UrlConnectionFactory {
private static final Logger log = UrlConnectionLogger.LOG;

private final SSLSocketFactory socketFactory;
private final AttributeMap options;

private DefaultUrlConnectionFactory(AttributeMap options) {
this.options = options;

// 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.
this.socketFactory = getSslContext().getSocketFactory();

}

public static UrlConnectionFactory get(AttributeMap options, UrlConnectionFactory configuredFactory) {
if (configuredFactory != null) {
return configuredFactory;
}

return new DefaultUrlConnectionFactory(options);
}

@Override
public HttpURLConnection createConnection(URI uri) {
return createDefaultConnection(uri, socketFactory);
}

private SSLContext getSslContext() {
Validate.isTrue(options.get(SdkHttpConfigurationOption.TLS_TRUST_MANAGERS_PROVIDER) == null ||
!options.get(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES),
"A TlsTrustManagerProvider can't be provided if TrustAllCertificates is also set");

TrustManager[] trustManagers = null;
if (options.get(SdkHttpConfigurationOption.TLS_TRUST_MANAGERS_PROVIDER) != null) {
trustManagers = options.get(SdkHttpConfigurationOption.TLS_TRUST_MANAGERS_PROVIDER).trustManagers();
}

if (options.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 = new TrustManager[] { TrustAllManager.INSTANCE };
}

TlsKeyManagersProvider provider = options.get(SdkHttpConfigurationOption.TLS_KEY_MANAGERS_PROVIDER);
KeyManager[] keyManagers = provider.keyManagers();

try {
SSLContext context = SSLContext.getInstance("TLS");
context.init(keyManagers, trustManagers, null);
return context;
} catch (NoSuchAlgorithmException | KeyManagementException ex) {
throw new RuntimeException(ex.getMessage(), ex);
}
}

private HttpURLConnection createDefaultConnection(URI uri, SSLSocketFactory socketFactory) {
HttpURLConnection connection = invokeSafely(() -> (HttpURLConnection) uri.toURL().openConnection());

if (connection instanceof HttpsURLConnection) {
HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;

if (options.get(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES)) {
httpsConnection.setHostnameVerifier(NoOpHostNameVerifier.INSTANCE);
}
httpsConnection.setSSLSocketFactory(socketFactory);
}

connection.setConnectTimeout(saturatedCast(options.get(SdkHttpConfigurationOption.CONNECTION_TIMEOUT).toMillis()));
connection.setReadTimeout(saturatedCast(options.get(SdkHttpConfigurationOption.READ_TIMEOUT).toMillis()));

return connection;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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.internal;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;
import software.amazon.awssdk.annotations.SdkInternalApi;

@SdkInternalApi
public class NoOpHostNameVerifier implements HostnameVerifier {
public static final NoOpHostNameVerifier INSTANCE = new NoOpHostNameVerifier();

@Override
public boolean verify(String s, SSLSession sslSession) {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -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.
*/

package software.amazon.awssdk.http.urlconnection.internal;

import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.utils.Logger;

/**
* Insecure trust manager to trust all certs. Should only be used for testing.
*/
@SdkInternalApi
public class TrustAllManager implements X509TrustManager {
public static final TrustAllManager INSTANCE = new TrustAllManager();

private static final Logger log = UrlConnectionLogger.LOG;

@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) {
log.debug(() -> "Accepting a client certificate: " + x509Certificates[0].getSubjectDN());
}

@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) {
log.debug(() -> "Accepting a server certificate: " + x509Certificates[0].getSubjectDN());
}

@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* 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.internal;

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 java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.net.HttpURLConnection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import software.amazon.awssdk.annotations.SdkInternalApi;
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.SdkHttpResponse;
import software.amazon.awssdk.http.urlconnection.UrlConnectionFactory;
import software.amazon.awssdk.http.urlconnection.internal.connection.DefaultHttpConnection;
import software.amazon.awssdk.http.urlconnection.internal.connection.Expect100BugHttpConnection;
import software.amazon.awssdk.http.urlconnection.internal.connection.HttpConnection;
import software.amazon.awssdk.http.urlconnection.internal.connection.ResponseCodeBugHttpConnection;
import software.amazon.awssdk.utils.IoUtils;

/**
* Executes a {@link HttpExecuteRequest} against a {@link HttpURLConnection}
*/
@SdkInternalApi
public class UrlConnectionExecutableHttpRequest implements ExecutableHttpRequest {
private final HttpConnection connection;
private final HttpExecuteRequest request;

public UrlConnectionExecutableHttpRequest(UrlConnectionFactory connectionFactory,
HttpExecuteRequest executeRequest) {
this.request = executeRequest;

HttpURLConnection httpUrlConnection = connectionFactory.createConnection(executeRequest.httpRequest().getUri());
initializeConnection(httpUrlConnection);

HttpConnection connection = new DefaultHttpConnection(httpUrlConnection);
connection = new ResponseCodeBugHttpConnection(connection);
connection = new Expect100BugHttpConnection(connection, executeRequest.httpRequest());
this.connection = connection;
Comment on lines +62 to +65
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very elegant usage of wrapping!

}

private void initializeConnection(HttpURLConnection connection) {
// 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);

request.httpRequest()
.headers()
.forEach((key, values) -> values.forEach(value -> connection.setRequestProperty(key, value)));

invokeSafely(() -> connection.setRequestMethod(request.httpRequest().method().name()));

if (request.contentStreamProvider().isPresent()) {
connection.setDoOutput(true);
}

request.httpRequest().firstMatchingHeader(CONTENT_LENGTH).map(Long::parseLong)
.ifPresent(connection::setFixedLengthStreamingMode);
}

@Override
public HttpExecuteResponse call() throws IOException {
try {
return doCall();
} catch (UncheckedIOException e) {
throw e.getCause();
}
}

private HttpExecuteResponse doCall() throws IOException {
connection.connect();

Optional<ContentStreamProvider> requestContent = request.contentStreamProvider();

if (requestContent.isPresent()) {
OutputStream outputStream = connection.getRequestStream();
if (outputStream != null) {
IoUtils.copy(requestContent.get().newStream(), outputStream);
}
}

int responseCode = connection.getResponseCode();
boolean isErrorResponse = HttpStatusFamily.of(responseCode).isOneOf(CLIENT_ERROR, SERVER_ERROR);
InputStream responseContent = isErrorResponse ? connection.getResponseErrorStream()
: connection.getResponseStream();
AbortableInputStream responseBody = responseContent != null ? AbortableInputStream.create(responseContent)
: null;

return HttpExecuteResponse.builder()
.response(SdkHttpResponse.builder()
.statusCode(responseCode)
.statusText(connection.getResponseMessage())
.headers(responseHeaders())
.build())
.responseBody(responseBody)
.build();
}

private Map<String, List<String>> responseHeaders() {
return connection.getResponseHeaders()
.entrySet().stream()
.filter(e -> e.getKey() != null)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

@Override
public void abort() {
connection.disconnect();
}
}
Original file line number Diff line number Diff line change
@@ -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.urlconnection.internal;

import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
import software.amazon.awssdk.utils.Logger;

/**
* A logger used by classes in the URL connection HTTP client package.
*/
@SdkInternalApi
public class UrlConnectionLogger {
public static final Logger LOG = Logger.loggerFor(UrlConnectionHttpClient.class);
Comment on lines +22 to +27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed? Is there any harm in exposing the underlying class name that's logging, even if it's an internal class? It somewhat unifies logs from a user perspective but it also makes things a little bit more difficult to trace.


private UrlConnectionLogger() {
}
}
Loading