diff --git a/.changes/next-release/bugfix-AWSCRTbasedS3Client-7a3c1e2.json b/.changes/next-release/bugfix-AWSCRTbasedS3Client-7a3c1e2.json new file mode 100644 index 000000000000..78233be09295 --- /dev/null +++ b/.changes/next-release/bugfix-AWSCRTbasedS3Client-7a3c1e2.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "AWS CRT-based S3 Client", + "contributor": "MingWangSong", + "description": "Fixed an issue where the CRT-based S3 client always established proxy connections through a `CONNECT` tunnel, even when `endpointOverride` used the plaintext `http://` scheme. Proxies that only support forwarding mode rejected these requests. The proxy connection type is now pinned to forwarding for plaintext endpoints, matching the behavior of `ApacheHttpClient`. See [#7320](https://github.com/aws/aws-sdk-java-v2/issues/7320)." +} diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/S3NativeClientConfiguration.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/S3NativeClientConfiguration.java index b3c0742a272c..47b428699dcc 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/S3NativeClientConfiguration.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/S3NativeClientConfiguration.java @@ -47,6 +47,7 @@ public class S3NativeClientConfiguration implements SdkAutoCloseable { static final long DEFAULT_PART_SIZE_IN_BYTES = 8L * 1024 * 1024; private static final Logger log = Logger.loggerFor(S3NativeClientConfiguration.class); private static final long DEFAULT_TARGET_THROUGHPUT_IN_GBPS = 10; + private static final String HTTP_SCHEME = "http"; private final String signingRegion; private final StandardRetryOptions standardRetryOptions; @@ -106,6 +107,7 @@ public S3NativeClientConfiguration(Builder builder) { if (builder.httpConfiguration != null) { this.proxyOptions = resolveProxy(builder.httpConfiguration.proxyConfiguration(), tlsContext).orElse(null); + applyPlaintextEndpointProxyConnectionType(this.proxyOptions, this.endpointOverride); this.connectionTimeout = builder.httpConfiguration.connectionTimeout(); this.httpMonitoringOptions = resolveHttpMonitoringOptions(builder.httpConfiguration.healthConfiguration()).orElse(null); @@ -135,6 +137,24 @@ private long resolveThresholdInBytes(Builder builder) { return this.partSizeInBytes == null ? DEFAULT_PART_SIZE_IN_BYTES : this.partSizeInBytes; } + /** + * Pins the proxy connection type to forwarding when the endpoint is plaintext. + * + *

The CRT derives the proxy connection type from whether TLS options were supplied for the main connection, + * not from the scheme actually in use. Because this client is always constructed with a {@link TlsContext}, a + * plaintext {@code http://} endpoint would otherwise be reached through a {@code CONNECT} tunnel, which + * forwarding-only proxies reject. This mirrors the scheme check that the CRT's own + * {@code HttpClientConnectionManager} performs before handing a TLS context to the native layer. + */ + private static void applyPlaintextEndpointProxyConnectionType(HttpProxyOptions proxyOptions, URI endpointOverride) { + if (proxyOptions == null || endpointOverride == null) { + return; + } + if (HTTP_SCHEME.equalsIgnoreCase(endpointOverride.getScheme())) { + proxyOptions.setConnectionType(HttpProxyOptions.HttpProxyConnectionType.Forwarding); + } + } + private static Boolean resolveUseEnvironmentVariableValues(Builder builder) { if (builder != null && builder.httpConfiguration != null && builder.httpConfiguration.proxyConfiguration() != null) { return builder.httpConfiguration.proxyConfiguration().isUseEnvironmentVariableValues(); diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crt/S3NativeClientConfigurationTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crt/S3NativeClientConfigurationTest.java index 0fdb91c0e6a5..5662d6b8237c 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crt/S3NativeClientConfigurationTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crt/S3NativeClientConfigurationTest.java @@ -17,6 +17,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.net.URI; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -24,6 +25,8 @@ import org.junit.jupiter.params.provider.MethodSource; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.crt.http.HttpProxyOptions; +import software.amazon.awssdk.services.s3.crt.S3CrtProxyConfiguration; import software.amazon.awssdk.services.s3.crt.S3CrtHttpConfiguration; import software.amazon.awssdk.testutils.LogCaptor; @@ -61,7 +64,49 @@ private static Stream noWarningConfigurations() { ); } + @ParameterizedTest + @MethodSource("proxyConnectionTypeCases") + void build_proxyConnectionType_followsEndpointScheme(URI endpointOverride, + HttpProxyOptions.HttpProxyConnectionType expectedType) { + S3CrtHttpConfiguration httpConfig = + S3CrtHttpConfiguration.builder() + .proxyConfiguration(S3CrtProxyConfiguration.builder() + .scheme("http") + .host("localhost") + .port(8888) + .build()) + .build(); + + try (S3NativeClientConfiguration config = buildConfig(httpConfig, endpointOverride)) { + assertThat(config.proxyOptions().getConnectionType()).isEqualTo(expectedType); + } + } + + private static Stream proxyConnectionTypeCases() { + return Stream.of( + Arguments.of(URI.create("http://localhost:9000"), + HttpProxyOptions.HttpProxyConnectionType.Forwarding), + Arguments.of(URI.create("HTTP://localhost:9000"), + HttpProxyOptions.HttpProxyConnectionType.Forwarding), + Arguments.of(URI.create("https://s3.us-east-1.amazonaws.com"), + HttpProxyOptions.HttpProxyConnectionType.Legacy), + Arguments.of(null, HttpProxyOptions.HttpProxyConnectionType.Legacy) + ); + } + + @Test + void build_whenPlaintextEndpointAndNoProxy_shouldNotSetProxyOptions() { + try (S3NativeClientConfiguration config = buildConfig(S3CrtHttpConfiguration.builder().build(), + URI.create("http://localhost:9000"))) { + assertThat(config.proxyOptions()).isNull(); + } + } + private S3NativeClientConfiguration buildConfig(S3CrtHttpConfiguration httpConfig) { + return buildConfig(httpConfig, null); + } + + private S3NativeClientConfiguration buildConfig(S3CrtHttpConfiguration httpConfig, URI endpointOverride) { S3NativeClientConfiguration.Builder builder = S3NativeClientConfiguration.builder() .signingRegion("us-east-1") @@ -71,6 +116,9 @@ private S3NativeClientConfiguration buildConfig(S3CrtHttpConfiguration httpConfi if (httpConfig != null) { builder.httpConfiguration(httpConfig); } + if (endpointOverride != null) { + builder.endpointOverride(endpointOverride); + } return builder.build(); } }