From 289e5b68d42ce2a7a1f5b8ebb1111df16c624c83 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Thu, 29 Oct 2026 20:49:56 +0000 Subject: [PATCH] Resolve host prefix injection setting from env/config ## Motivation The `DISABLE_HOST_PREFIX_INJECTION` advanced option could only be set in code, and the AWS client builder set it to false whenever the caller left it unset. The SDK read no system property, environment variable or profile setting for it. A user who started the JVM with `-Daws.disableHostPrefixInjection=true` still had requests sent to the prefixed host, such as `data-servicediscovery.us-east-1.amazonaws.com`. ## Changes 1. Clients now resolve the option at build time when the caller has not set it in code, from the `aws.disableHostPrefixInjection` system property, then the `AWS_DISABLE_HOST_PREFIX_INJECTION` environment variable, then the `disable_host_prefix_injection` profile property, and then false. 2. The AWS client builder no longer forces the option to false, which blocked this resolution. 3. A value set in code through `overrideConfiguration` still takes precedence over all three sources. 4. The option's javadoc lists the sources and their order, and a changelog entry announces the feature. --- .../feature-AWSSDKforJavav2-eeeef1c.json | 6 + .github/workflows/pull-request-build.yml | 1 + .../builder/AwsDefaultClientBuilder.java | 2 - .../builder/DefaultAwsClientBuilderTest.java | 29 +++ .../awssdk/profiles/ProfileProperty.java | 6 + .../amazon/awssdk/core/SdkSystemSetting.java | 6 + .../builder/SdkDefaultClientBuilder.java | 15 +- .../config/SdkAdvancedClientOption.java | 8 + .../builder/DefaultClientBuilderTest.java | 114 +++++++++++ .../HostPrefixInjectionConfigurationTest.java | 192 ++++++++++++++++++ 10 files changed, 376 insertions(+), 3 deletions(-) create mode 100644 .changes/next-release/feature-AWSSDKforJavav2-eeeef1c.json create mode 100644 test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/HostPrefixInjectionConfigurationTest.java diff --git a/.changes/next-release/feature-AWSSDKforJavav2-eeeef1c.json b/.changes/next-release/feature-AWSSDKforJavav2-eeeef1c.json new file mode 100644 index 000000000000..36834ed8fe7b --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-eeeef1c.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Add support for resolving the disable host prefix injection setting from the aws.disableHostPrefixInjection system property, the AWS_DISABLE_HOST_PREFIX_INJECTION environment variable, and the disable_host_prefix_injection profile property." +} diff --git a/.github/workflows/pull-request-build.yml b/.github/workflows/pull-request-build.yml index 55066cb4bdb6..b936c6eb8c0c 100644 --- a/.github/workflows/pull-request-build.yml +++ b/.github/workflows/pull-request-build.yml @@ -2,6 +2,7 @@ name: Build SDK on: merge_group: pull_request: + types: [opened, synchronize, reopened, ready_for_review] concurrency: group: start-pull-request-build-${{ github.ref }} diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java index 325585dc6d7b..ae10ffcff95b 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java @@ -48,7 +48,6 @@ import software.amazon.awssdk.awscore.retry.AwsRetryStrategy; import software.amazon.awssdk.core.ClientEndpointProvider; import software.amazon.awssdk.core.client.builder.SdkDefaultClientBuilder; -import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; import software.amazon.awssdk.core.client.config.SdkClientOption; import software.amazon.awssdk.core.http.EnableDefaultSocketTimeout2026Resolver; @@ -146,7 +145,6 @@ protected AwsDefaultClientBuilder() { protected final SdkClientConfiguration mergeChildDefaults(SdkClientConfiguration configuration) { SdkClientConfiguration config = mergeServiceDefaults(configuration); config = config.merge(c -> c.option(AwsAdvancedClientOption.ENABLE_DEFAULT_REGION_DETECTION, true) - .option(SdkAdvancedClientOption.DISABLE_HOST_PREFIX_INJECTION, false) .option(AwsClientOption.SERVICE_SIGNING_NAME, signingName()) .option(SdkClientOption.SERVICE_NAME, serviceName()) .option(AwsClientOption.ENDPOINT_PREFIX, serviceEndpointPrefix())); diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/DefaultAwsClientBuilderTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/DefaultAwsClientBuilderTest.java index c76b5bb36823..34cbfd68de03 100644 --- a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/DefaultAwsClientBuilderTest.java +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/DefaultAwsClientBuilderTest.java @@ -25,6 +25,7 @@ import static software.amazon.awssdk.awscore.client.config.AwsAdvancedClientOption.ENABLE_DEFAULT_REGION_DETECTION; import static software.amazon.awssdk.awscore.client.config.AwsClientOption.SERVICE_SIGNING_NAME; import static software.amazon.awssdk.awscore.client.config.AwsClientOption.SIGNING_REGION; +import static software.amazon.awssdk.core.client.config.SdkAdvancedClientOption.DISABLE_HOST_PREFIX_INJECTION; import static software.amazon.awssdk.core.client.config.SdkAdvancedClientOption.SIGNER; import java.beans.BeanInfo; @@ -37,6 +38,7 @@ import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -53,6 +55,7 @@ import software.amazon.awssdk.core.client.config.SdkClientConfiguration; import software.amazon.awssdk.core.client.config.SdkClientOption; import software.amazon.awssdk.core.ClientEndpointProvider; +import software.amazon.awssdk.core.SdkSystemSetting; import software.amazon.awssdk.core.internal.retry.SdkDefaultRetryStrategy; import software.amazon.awssdk.core.signer.Signer; import software.amazon.awssdk.http.SdkHttpClient; @@ -99,6 +102,32 @@ public class DefaultAwsClientBuilderTest { public void setup() { when(defaultHttpClientBuilder.buildWithDefaults(any())).thenReturn(mock(SdkHttpClient.class)); when(defaultAsyncHttpClientFactory.buildWithDefaults(any())).thenReturn(mock(SdkAsyncHttpClient.class)); + System.clearProperty(SdkSystemSetting.AWS_DISABLE_HOST_PREFIX_INJECTION.property()); + } + + @AfterEach + public void cleanup() { + System.clearProperty(SdkSystemSetting.AWS_DISABLE_HOST_PREFIX_INJECTION.property()); + } + + @Test + public void disableHostPrefixInjection_nothingSet_defaultsToFalse() { + TestClient client = testClientBuilder().region(Region.US_WEST_1).build(); + assertThat(client.clientConfiguration.option(DISABLE_HOST_PREFIX_INJECTION)).isFalse(); + } + + @Test + public void disableHostPrefixInjection_systemPropertyTrue_resolvesTrue() { + System.setProperty(SdkSystemSetting.AWS_DISABLE_HOST_PREFIX_INJECTION.property(), "true"); + TestClient client = testClientBuilder().region(Region.US_WEST_1).build(); + assertThat(client.clientConfiguration.option(DISABLE_HOST_PREFIX_INJECTION)).isTrue(); + } + + @Test + public void disableHostPrefixInjection_asyncClient_systemPropertyTrue_resolvesTrue() { + System.setProperty(SdkSystemSetting.AWS_DISABLE_HOST_PREFIX_INJECTION.property(), "true"); + TestAsyncClient client = testAsyncClientBuilder().region(Region.US_WEST_1).build(); + assertThat(client.clientConfiguration.option(DISABLE_HOST_PREFIX_INJECTION)).isTrue(); } @Test diff --git a/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java index b3243ba6c29b..2a345136861c 100644 --- a/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java +++ b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java @@ -177,6 +177,12 @@ public final class ProfileProperty { */ public static final String DISABLE_REQUEST_COMPRESSION = "disable_request_compression"; + /** + * Whether the SDK should skip prepending the modeled hostPrefix to the endpoint host. The default value is false, i.e., + * the hostPrefix is prepended. + */ + public static final String DISABLE_HOST_PREFIX_INJECTION = "disable_host_prefix_injection"; + /** * The minimum compression size in bytes, inclusive, for a request to be compressed. The default value is 10_240. * The value must be non-negative and no greater than 10_485_760. diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java index 0da073035e64..6033f6d348fa 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java @@ -230,6 +230,12 @@ public enum SdkSystemSetting implements SystemSetting { */ AWS_DISABLE_REQUEST_COMPRESSION("aws.disableRequestCompression", null), + /** + * Whether the SDK should skip prepending the modeled hostPrefix to the endpoint host. The default value is false, i.e., + * the hostPrefix is prepended. + */ + AWS_DISABLE_HOST_PREFIX_INJECTION("aws.disableHostPrefixInjection", null), + /** * Defines the minimum compression size in bytes, inclusive, for a request to be compressed. The default value is 10_240. * The value must be non-negative and no greater than 10_485_760. diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java index 34f165dd1767..ff41a0370b2a 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java @@ -18,6 +18,7 @@ import static software.amazon.awssdk.core.ClientType.ASYNC; import static software.amazon.awssdk.core.ClientType.SYNC; import static software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption.FUTURE_COMPLETION_EXECUTOR; +import static software.amazon.awssdk.core.client.config.SdkAdvancedClientOption.DISABLE_HOST_PREFIX_INJECTION; import static software.amazon.awssdk.core.client.config.SdkAdvancedClientOption.USER_AGENT_PREFIX; import static software.amazon.awssdk.core.client.config.SdkAdvancedClientOption.USER_AGENT_SUFFIX; import static software.amazon.awssdk.core.client.config.SdkClientOption.ADDITIONAL_HTTP_HEADERS; @@ -360,7 +361,8 @@ private SdkClientConfiguration finalizeConfiguration(SdkClientConfiguration conf .option(EXECUTION_INTERCEPTORS, resolveExecutionInterceptors(config)) .lazyOption(CLIENT_USER_AGENT, this::resolveClientUserAgent) .lazyOption(COMPRESSION_CONFIGURATION, this::resolveCompressionConfiguration) - .lazyOptionIfAbsent(IDENTITY_PROVIDERS, c -> IdentityProviders.builder().build()); + .lazyOptionIfAbsent(IDENTITY_PROVIDERS, c -> IdentityProviders.builder().build()) + .lazyOptionIfAbsent(DISABLE_HOST_PREFIX_INJECTION, this::resolveDisableHostPrefixInjection); builder.computeOptionIfAbsent(HTTP_CLIENT_CONFIG_TYPE, () -> BusinessMetricFeatureId.HTTP_CLIENT_AUTO); return builder.build(); } @@ -390,6 +392,17 @@ private Boolean resolveCompressionEnabled(LazyValueSource config, CompressionCon .orElse(true); } + private Boolean resolveDisableHostPrefixInjection(LazyValueSource config) { + Supplier> profileFileConfiguration = + () -> config.get(PROFILE_FILE_SUPPLIER).get() + .profile(config.get(PROFILE_NAME)) + .flatMap(p -> p.booleanProperty(ProfileProperty.DISABLE_HOST_PREFIX_INJECTION)); + + return OptionalUtils.firstPresent(SdkSystemSetting.AWS_DISABLE_HOST_PREFIX_INJECTION.getBooleanValue(), + profileFileConfiguration) + .orElse(false); + } + private Integer resolveMinCompressionThreshold(LazyValueSource config, CompressionConfiguration compressionConfig) { Supplier> systemSettingConfiguration = SdkSystemSetting.AWS_REQUEST_MIN_COMPRESSION_SIZE_BYTES::getIntegerValue; diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkAdvancedClientOption.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkAdvancedClientOption.java index 28924e9aea2d..697e6a3f973e 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkAdvancedClientOption.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkAdvancedClientOption.java @@ -84,6 +84,14 @@ public class SdkAdvancedClientOption extends ClientOption { * the endpoint host that the API request is sent to. * * Customers can set this value to True to disable the behavior. + * + *

If this option is not set, the SDK resolves it from the following sources, in order: + *

    + *
  1. The {@code aws.disableHostPrefixInjection} system property
  2. + *
  3. The {@code AWS_DISABLE_HOST_PREFIX_INJECTION} environment variable
  4. + *
  5. The {@code disable_host_prefix_injection} property in the configured profile
  6. + *
+ * If none of them is set, the default is false. */ public static final SdkAdvancedClientOption DISABLE_HOST_PREFIX_INJECTION = new SdkAdvancedClientOption<>(Boolean.class); diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java index cfefe8254cec..bca63997bf28 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java @@ -63,6 +63,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.function.Supplier; import org.assertj.core.api.Assertions; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -72,6 +73,7 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import software.amazon.awssdk.core.ClientEndpointProvider; +import software.amazon.awssdk.core.SdkSystemSetting; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; @@ -89,6 +91,7 @@ import software.amazon.awssdk.metrics.MetricCollection; import software.amazon.awssdk.metrics.MetricPublisher; import software.amazon.awssdk.profiles.ProfileFile; +import software.amazon.awssdk.testutils.EnvironmentVariableHelper; import software.amazon.awssdk.utils.AttributeMap; import software.amazon.awssdk.utils.StringInputStream; @@ -111,6 +114,9 @@ public class DefaultClientBuilderTest { @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); + @Rule + public EnvironmentVariableHelper environmentVariableHelper = new EnvironmentVariableHelper(); + @Mock private SdkHttpClient.Builder defaultHttpClientFactory; @@ -121,6 +127,12 @@ public class DefaultClientBuilderTest { public void setup() { when(defaultHttpClientFactory.buildWithDefaults(any())).thenReturn(mock(SdkHttpClient.class)); when(defaultAsyncHttpClientFactory.buildWithDefaults(any())).thenReturn(mock(SdkAsyncHttpClient.class)); + System.clearProperty(SdkSystemSetting.AWS_DISABLE_HOST_PREFIX_INJECTION.property()); + } + + @After + public void cleanup() { + System.clearProperty(SdkSystemSetting.AWS_DISABLE_HOST_PREFIX_INJECTION.property()); } @Test @@ -520,6 +532,108 @@ public void defaultProfileFileSupplier_isStaticOrHasIdentityCaching() { assertThat(secondGet).isSameAs(firstGet); } + @Test + public void disableHostPrefixInjection_nothingSet_defaultsToFalse() { + assertThat(disableHostPrefixInjection(null, null)).isFalse(); + } + + @Test + public void disableHostPrefixInjection_asyncClient_nothingSet_defaultsToFalse() { + SdkClientConfiguration config = testAsyncClientBuilder() + .overrideConfiguration(o -> o.defaultProfileFile(profileFile("")) + .defaultProfileName("p")) + .build().clientConfiguration; + assertThat(config.option(DISABLE_HOST_PREFIX_INJECTION)).isFalse(); + } + + @Test + public void disableHostPrefixInjection_systemPropertyTrue_resolvesTrue() { + System.setProperty(SdkSystemSetting.AWS_DISABLE_HOST_PREFIX_INJECTION.property(), "TRUE"); + assertThat(disableHostPrefixInjection(null, null)).isTrue(); + } + + @Test + public void disableHostPrefixInjection_asyncClient_systemPropertyTrue_resolvesTrue() { + System.setProperty(SdkSystemSetting.AWS_DISABLE_HOST_PREFIX_INJECTION.property(), "true"); + SdkClientConfiguration config = testAsyncClientBuilder().build().clientConfiguration; + assertThat(config.option(DISABLE_HOST_PREFIX_INJECTION)).isTrue(); + } + + @Test + public void disableHostPrefixInjection_envVarTrue_resolvesTrue() { + environmentVariableHelper.set(SdkSystemSetting.AWS_DISABLE_HOST_PREFIX_INJECTION, "true"); + assertThat(disableHostPrefixInjection(null, null)).isTrue(); + } + + @Test + public void disableHostPrefixInjection_profileTrue_resolvesTrue() { + assertThat(disableHostPrefixInjection(null, "[profile p]\ndisable_host_prefix_injection = True")).isTrue(); + } + + @Test + public void disableHostPrefixInjection_profileTrueInOtherProfile_isIgnored() { + assertThat(disableHostPrefixInjection(null, "[profile other]\ndisable_host_prefix_injection = true")).isFalse(); + } + + @Test + public void disableHostPrefixInjection_explicitFalse_beatsSystemProperty() { + System.setProperty(SdkSystemSetting.AWS_DISABLE_HOST_PREFIX_INJECTION.property(), "true"); + assertThat(disableHostPrefixInjection(Boolean.FALSE, null)).isFalse(); + } + + @Test + public void disableHostPrefixInjection_explicitTrue_beatsSystemProperty() { + System.setProperty(SdkSystemSetting.AWS_DISABLE_HOST_PREFIX_INJECTION.property(), "false"); + assertThat(disableHostPrefixInjection(Boolean.TRUE, null)).isTrue(); + } + + @Test + public void disableHostPrefixInjection_systemPropertyFalse_beatsEnvVar() { + System.setProperty(SdkSystemSetting.AWS_DISABLE_HOST_PREFIX_INJECTION.property(), "false"); + environmentVariableHelper.set(SdkSystemSetting.AWS_DISABLE_HOST_PREFIX_INJECTION, "true"); + assertThat(disableHostPrefixInjection(null, null)).isFalse(); + } + + @Test + public void disableHostPrefixInjection_envVarFalse_beatsProfile() { + environmentVariableHelper.set(SdkSystemSetting.AWS_DISABLE_HOST_PREFIX_INJECTION, "false"); + assertThat(disableHostPrefixInjection(null, "[profile p]\ndisable_host_prefix_injection = true")).isFalse(); + } + + @Test + public void disableHostPrefixInjection_invalidSystemProperty_throwsOnBuild() { + System.setProperty(SdkSystemSetting.AWS_DISABLE_HOST_PREFIX_INJECTION.property(), "yes"); + assertThatThrownBy(() -> disableHostPrefixInjection(null, null)).isInstanceOf(IllegalStateException.class); + } + + @Test + public void disableHostPrefixInjection_invalidProfileValue_throwsOnBuild() { + assertThatThrownBy(() -> disableHostPrefixInjection(null, "[profile p]\ndisable_host_prefix_injection = yes")) + .isInstanceOf(IllegalStateException.class); + } + + private Boolean disableHostPrefixInjection(Boolean explicitValue, String profileContent) { + ClientOverrideConfiguration.Builder overrideConfig = + ClientOverrideConfiguration.builder() + .putAdvancedOption(SIGNER, TEST_SIGNER) + .defaultProfileFile(profileFile(profileContent == null ? "" : profileContent)) + .defaultProfileName("p"); + if (explicitValue != null) { + overrideConfig.putAdvancedOption(DISABLE_HOST_PREFIX_INJECTION, explicitValue); + } + return new TestClientBuilder().overrideConfiguration(overrideConfig.build()) + .build() + .clientConfiguration + .option(DISABLE_HOST_PREFIX_INJECTION); + } + + private static ProfileFile profileFile(String content) { + return ProfileFile.builder() + .content(new StringInputStream(content)) + .type(ProfileFile.Type.CONFIGURATION) + .build(); + } + private SdkDefaultClientBuilder testClientBuilder() { ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/HostPrefixInjectionConfigurationTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/HostPrefixInjectionConfigurationTest.java new file mode 100644 index 000000000000..42bef94e4fed --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/HostPrefixInjectionConfigurationTest.java @@ -0,0 +1,192 @@ +/* + * 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.services; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely; + +import java.net.URI; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.profiles.ProfileFile; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonAsyncClient; +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonClient; +import software.amazon.awssdk.testutils.EnvironmentVariableHelper; +import software.amazon.awssdk.testutils.service.http.MockAsyncHttpClient; +import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient; +import software.amazon.awssdk.utils.IoUtils; +import software.amazon.awssdk.utils.StringInputStream; + +/** + * Verifies that the disable host prefix injection setting is resolved from the system property, the environment variable + * and the profile file, and that a generated client then skips the modeled hostPrefix. + */ +public class HostPrefixInjectionConfigurationTest { + private static final String PROPERTY = SdkSystemSetting.AWS_DISABLE_HOST_PREFIX_INJECTION.property(); + + private final EnvironmentVariableHelper environmentVariableHelper = new EnvironmentVariableHelper(); + private MockSyncHttpClient mockHttpClient; + private MockAsyncHttpClient mockAsyncHttpClient; + private AtomicReference capturedBody; + + @BeforeEach + public void setup() { + System.clearProperty(PROPERTY); + mockHttpClient = new MockSyncHttpClient(); + mockAsyncHttpClient = new MockAsyncHttpClient(); + mockHttpClient.stubNextResponse(okResponse()); + mockAsyncHttpClient.stubNextResponse(okResponse()); + capturedBody = new AtomicReference<>(); + } + + @AfterEach + public void cleanup() { + System.clearProperty(PROPERTY); + environmentVariableHelper.reset(); + mockAsyncHttpClient.close(); + } + + @Test + public void nothingSet_hostPrefixIsApplied() { + syncClient(emptyProfileOverride()).operationWithHostPrefix(b -> b.stringMember("123")); + assertThat(mockHttpClient.getLastRequest().getUri().getHost()).isEqualTo("123-foo.localhost"); + + asyncClient(emptyProfileOverride()).operationWithHostPrefix(b -> b.stringMember("123")).join(); + assertThat(mockAsyncHttpClient.getLastRequest().getUri().getHost()).isEqualTo("123-foo.localhost"); + } + + @Test + public void systemPropertyTrue_sync_hostPrefixIsNotApplied_memberStillSerialized() { + System.setProperty(PROPERTY, "true"); + syncClient(emptyProfileOverride()).operationWithHostPrefix(b -> b.stringMember("123")); + assertThat(mockHttpClient.getLastRequest().getUri().getHost()).isEqualTo("localhost"); + assertThat(capturedBody.get()).contains("\"StringMember\":\"123\""); + } + + @Test + public void systemPropertyTrue_async_hostPrefixIsNotApplied() { + System.setProperty(PROPERTY, "true"); + asyncClient(emptyProfileOverride()).operationWithHostPrefix(b -> b.stringMember("123")).join(); + assertThat(mockAsyncHttpClient.getLastRequest().getUri().getHost()).isEqualTo("localhost"); + } + + @Test + public void environmentVariableTrue_hostPrefixIsNotApplied() { + environmentVariableHelper.set(SdkSystemSetting.AWS_DISABLE_HOST_PREFIX_INJECTION, "true"); + syncClient(emptyProfileOverride()).operationWithHostPrefix(b -> b.stringMember("123")); + assertThat(mockHttpClient.getLastRequest().getUri().getHost()).isEqualTo("localhost"); + + asyncClient(emptyProfileOverride()).operationWithHostPrefix(b -> b.stringMember("123")).join(); + assertThat(mockAsyncHttpClient.getLastRequest().getUri().getHost()).isEqualTo("localhost"); + } + + @Test + public void profileTrue_hostPrefixIsNotApplied() { + ClientOverrideConfiguration override = + profileOverride("[profile test]\ndisable_host_prefix_injection = true"); + syncClient(override).operationWithHostPrefix(b -> b.stringMember("123")); + assertThat(mockHttpClient.getLastRequest().getUri().getHost()).isEqualTo("localhost"); + assertThat(capturedBody.get()).contains("\"StringMember\":\"123\""); + + asyncClient(override).operationWithHostPrefix(b -> b.stringMember("123")).join(); + assertThat(mockAsyncHttpClient.getLastRequest().getUri().getHost()).isEqualTo("localhost"); + } + + @Test + public void explicitFalse_beatsSystemProperty_hostPrefixIsApplied() { + System.setProperty(PROPERTY, "true"); + ClientOverrideConfiguration override = + emptyProfileOverride().toBuilder() + .putAdvancedOption(SdkAdvancedClientOption.DISABLE_HOST_PREFIX_INJECTION, false) + .build(); + syncClient(override).operationWithHostPrefix(b -> b.stringMember("123")); + assertThat(mockHttpClient.getLastRequest().getUri().getHost()).isEqualTo("123-foo.localhost"); + + asyncClient(override).operationWithHostPrefix(b -> b.stringMember("123")).join(); + assertThat(mockAsyncHttpClient.getLastRequest().getUri().getHost()).isEqualTo("123-foo.localhost"); + } + + private ProtocolRestJsonClient syncClient(ClientOverrideConfiguration override) { + ClientOverrideConfiguration withCapture = override.toBuilder() + .addExecutionInterceptor(new BodyCapturingInterceptor()) + .build(); + return ProtocolRestJsonClient.builder() + .credentialsProvider(credentials()) + .region(Region.US_EAST_1) + .endpointOverride(URI.create("http://localhost")) + .httpClient(mockHttpClient) + .overrideConfiguration(withCapture) + .build(); + } + + private ProtocolRestJsonAsyncClient asyncClient(ClientOverrideConfiguration override) { + return ProtocolRestJsonAsyncClient.builder() + .credentialsProvider(credentials()) + .region(Region.US_EAST_1) + .endpointOverride(URI.create("http://localhost")) + .httpClient(mockAsyncHttpClient) + .overrideConfiguration(override) + .build(); + } + + private static ClientOverrideConfiguration emptyProfileOverride() { + return profileOverride(""); + } + + private static ClientOverrideConfiguration profileOverride(String content) { + ProfileFile profileFile = ProfileFile.builder() + .content(new StringInputStream(content)) + .type(ProfileFile.Type.CONFIGURATION) + .build(); + return ClientOverrideConfiguration.builder() + .defaultProfileFileSupplier(() -> profileFile) + .defaultProfileName("test") + .build(); + } + + private static StaticCredentialsProvider credentials() { + return StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid")); + } + + private static HttpExecuteResponse okResponse() { + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(""))) + .build(); + } + + private class BodyCapturingInterceptor implements ExecutionInterceptor { + @Override + public void beforeTransmission(Context.BeforeTransmission context, ExecutionAttributes executionAttributes) { + context.requestBody().ifPresent(body -> capturedBody.set( + invokeSafely(() -> IoUtils.toUtf8String(body.contentStreamProvider().newStream())))); + } + } +}