diff --git a/.changes/next-release/feature-AWSSDKforJavav2-abb9c7e.json b/.changes/next-release/feature-AWSSDKforJavav2-abb9c7e.json
new file mode 100644
index 000000000000..c326f0d5dd16
--- /dev/null
+++ b/.changes/next-release/feature-AWSSDKforJavav2-abb9c7e.json
@@ -0,0 +1,6 @@
+{
+ "type": "feature",
+ "category": "Netty NIO HTTP Client",
+ "contributor": "",
+ "description": "Adds ALPN H2 support for Netty client"
+}
diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java
index 3cbdd6d83f60..502f07cb7c8e 100644
--- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java
+++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java
@@ -258,6 +258,11 @@ public class CustomizationConfig {
*/
private boolean generateEndpointClientTests;
+ /**
+ * Whether to use prior knowledge protocol negotiation for H2
+ */
+ private boolean usePriorKnowledgeForH2;
+
/**
* A mapping from the skipped test's description to the reason why it's being skipped.
*/
@@ -746,6 +751,14 @@ public void setGenerateEndpointClientTests(boolean generateEndpointClientTests)
this.generateEndpointClientTests = generateEndpointClientTests;
}
+ public boolean isUsePriorKnowledgeForH2() {
+ return usePriorKnowledgeForH2;
+ }
+
+ public void setUsePriorKnowledgeForH2(boolean usePriorKnowledgeForH2) {
+ this.usePriorKnowledgeForH2 = usePriorKnowledgeForH2;
+ }
+
public boolean useGlobalEndpoint() {
return useGlobalEndpoint;
}
diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java
index e1a37e35dea9..a28f4eb48ddc 100644
--- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java
+++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java
@@ -74,6 +74,7 @@
import software.amazon.awssdk.core.retry.RetryMode;
import software.amazon.awssdk.core.signer.Signer;
import software.amazon.awssdk.http.Protocol;
+import software.amazon.awssdk.http.ProtocolNegotiation;
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
import software.amazon.awssdk.http.auth.spi.scheme.AuthScheme;
import software.amazon.awssdk.identity.spi.IdentityProvider;
@@ -718,22 +719,25 @@ private MethodSpec beanStyleSetServiceConfigurationMethod() {
private void addServiceHttpConfigIfNeeded(TypeSpec.Builder builder, IntermediateModel model) {
String serviceDefaultFqcn = model.getCustomizationConfig().getServiceSpecificHttpConfig();
boolean supportsH2 = model.getMetadata().supportsH2();
+ boolean usePriorKnowledgeForH2 = model.getCustomizationConfig().isUsePriorKnowledgeForH2();
if (serviceDefaultFqcn != null || supportsH2) {
- builder.addMethod(serviceSpecificHttpConfigMethod(serviceDefaultFqcn, supportsH2));
+ builder.addMethod(serviceSpecificHttpConfigMethod(serviceDefaultFqcn, supportsH2, usePriorKnowledgeForH2));
}
}
- private MethodSpec serviceSpecificHttpConfigMethod(String serviceDefaultFqcn, boolean supportsH2) {
+ private MethodSpec serviceSpecificHttpConfigMethod(String serviceDefaultFqcn, boolean supportsH2,
+ boolean usePriorKnowledgeForH2) {
return MethodSpec.methodBuilder("serviceHttpConfig")
.addAnnotation(Override.class)
.addModifiers(PROTECTED, FINAL)
.returns(AttributeMap.class)
- .addCode(serviceSpecificHttpConfigMethodBody(serviceDefaultFqcn, supportsH2))
+ .addCode(serviceSpecificHttpConfigMethodBody(serviceDefaultFqcn, supportsH2, usePriorKnowledgeForH2))
.build();
}
- private CodeBlock serviceSpecificHttpConfigMethodBody(String serviceDefaultFqcn, boolean supportsH2) {
+ private CodeBlock serviceSpecificHttpConfigMethodBody(String serviceDefaultFqcn, boolean supportsH2,
+ boolean usePriorKnowledgeForH2) {
CodeBlock.Builder builder = CodeBlock.builder();
if (serviceDefaultFqcn != null) {
@@ -745,10 +749,16 @@ private CodeBlock serviceSpecificHttpConfigMethodBody(String serviceDefaultFqcn,
}
if (supportsH2) {
- builder.addStatement("return result.merge(AttributeMap.builder()"
- + ".put($T.PROTOCOL, $T.HTTP2)"
- + ".build())",
- SdkHttpConfigurationOption.class, Protocol.class);
+ builder.add("return result.merge(AttributeMap.builder()"
+ + ".put($T.PROTOCOL, $T.HTTP2)",
+ SdkHttpConfigurationOption.class, Protocol.class);
+
+ if (!usePriorKnowledgeForH2) {
+ builder.add(".put($T.PROTOCOL_NEGOTIATION, $T.ALPN)",
+ SdkHttpConfigurationOption.class, ProtocolNegotiation.class);
+ }
+
+ builder.addStatement(".build())");
} else {
builder.addStatement("return result");
}
diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java
index 9ab222d3b51e..a75232114d83 100644
--- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java
+++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java
@@ -320,6 +320,36 @@ public static IntermediateModel serviceWithNoAuth() {
return new IntermediateModelBuilder(models).build();
}
+ public static IntermediateModel serviceWithH2() {
+ File serviceModel =
+ new File(ClientTestModels.class.getResource("client/c2j/service-with-h2/service-2.json").getFile());
+ File customizationModel =
+ new File(ClientTestModels.class.getResource("client/c2j/service-with-h2/customization.config")
+ .getFile());
+ C2jModels models = C2jModels
+ .builder()
+ .serviceModel(getServiceModel(serviceModel))
+ .customizationConfig(getCustomizationConfig(customizationModel))
+ .build();
+
+ return new IntermediateModelBuilder(models).build();
+ }
+
+ public static IntermediateModel serviceWithH2UsePriorKnowledgeForH2() {
+ File serviceModel =
+ new File(ClientTestModels.class.getResource("client/c2j/service-with-h2-usePriorKnowledgeForH2/service-2.json").getFile());
+ File customizationModel =
+ new File(ClientTestModels.class.getResource("client/c2j/service-with-h2-usePriorKnowledgeForH2/customization.config")
+ .getFile());
+ C2jModels models = C2jModels
+ .builder()
+ .serviceModel(getServiceModel(serviceModel))
+ .customizationConfig(getCustomizationConfig(customizationModel))
+ .build();
+
+ return new IntermediateModelBuilder(models).build();
+ }
+
public static IntermediateModel serviceMiniS3() {
File serviceModel =
new File(ClientTestModels.class.getResource("client/c2j/mini-s3/service-2.json").getFile());
diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassTest.java
index 253eadc0f59f..3250523f6695 100644
--- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassTest.java
+++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassTest.java
@@ -22,6 +22,8 @@
import static software.amazon.awssdk.codegen.poet.ClientTestModels.queryServiceModels;
import static software.amazon.awssdk.codegen.poet.ClientTestModels.queryServiceModelsEndpointAuthParamsWithAllowList;
import static software.amazon.awssdk.codegen.poet.ClientTestModels.restJsonServiceModels;
+import static software.amazon.awssdk.codegen.poet.ClientTestModels.serviceWithH2;
+import static software.amazon.awssdk.codegen.poet.ClientTestModels.serviceWithH2UsePriorKnowledgeForH2;
import static software.amazon.awssdk.codegen.poet.ClientTestModels.serviceWithNoAuth;
import static software.amazon.awssdk.codegen.poet.builder.BuilderClassTestUtils.validateGeneration;
@@ -117,6 +119,16 @@ void syncComposedDefaultClientBuilderClass_sra() {
"test-composed-sync-default-client-builder.java", true);
}
+ @Test
+ void baseClientBuilderClassWithH2() {
+ validateBaseClientBuilderClassGeneration(serviceWithH2(), "test-h2-service-client-builder-class.java");
+ }
+
+ @Test
+ void baseClientBuilderClassWithH2_usePriorKnowledgeForH2() {
+ validateBaseClientBuilderClassGeneration(serviceWithH2UsePriorKnowledgeForH2(), "test-h2-usePriorKnowledgeForH2-service-client-builder-class.java");
+ }
+
private void validateBaseClientBuilderClassGeneration(IntermediateModel model, String expectedClassName) {
validateBaseClientBuilderClassGeneration(model, expectedClassName, false);
}
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-h2-service-client-builder-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-h2-service-client-builder-class.java
new file mode 100644
index 000000000000..82b96d1af267
--- /dev/null
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-h2-service-client-builder-class.java
@@ -0,0 +1,170 @@
+package software.amazon.awssdk.services.h2;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+import software.amazon.awssdk.annotations.Generated;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.auth.signer.Aws4Signer;
+import software.amazon.awssdk.awscore.client.builder.AwsDefaultClientBuilder;
+import software.amazon.awssdk.awscore.client.config.AwsClientOption;
+import software.amazon.awssdk.awscore.endpoint.AwsClientEndpointProvider;
+import software.amazon.awssdk.awscore.retry.AwsRetryStrategy;
+import software.amazon.awssdk.core.SdkPlugin;
+import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
+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.interceptor.ClasspathInterceptorChainFactory;
+import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
+import software.amazon.awssdk.core.retry.RetryMode;
+import software.amazon.awssdk.core.signer.Signer;
+import software.amazon.awssdk.http.Protocol;
+import software.amazon.awssdk.http.ProtocolNegotiation;
+import software.amazon.awssdk.http.SdkHttpConfigurationOption;
+import software.amazon.awssdk.identity.spi.IdentityProvider;
+import software.amazon.awssdk.identity.spi.IdentityProviders;
+import software.amazon.awssdk.regions.ServiceMetadataAdvancedOption;
+import software.amazon.awssdk.retries.api.RetryStrategy;
+import software.amazon.awssdk.services.h2.endpoints.H2EndpointProvider;
+import software.amazon.awssdk.services.h2.endpoints.internal.H2RequestSetEndpointInterceptor;
+import software.amazon.awssdk.services.h2.endpoints.internal.H2ResolveEndpointInterceptor;
+import software.amazon.awssdk.services.h2.internal.H2ServiceClientConfigurationBuilder;
+import software.amazon.awssdk.utils.AttributeMap;
+import software.amazon.awssdk.utils.CollectionUtils;
+import software.amazon.awssdk.utils.Validate;
+
+/**
+ * Internal base class for {@link DefaultH2ClientBuilder} and {@link DefaultH2AsyncClientBuilder}.
+ */
+@Generated("software.amazon.awssdk:codegen")
+@SdkInternalApi
+abstract class DefaultH2BaseClientBuilder, C> extends AwsDefaultClientBuilder {
+ @Override
+ protected final String serviceEndpointPrefix() {
+ return "h2-service";
+ }
+
+ @Override
+ protected final String serviceName() {
+ return "H2";
+ }
+
+ @Override
+ protected final SdkClientConfiguration mergeServiceDefaults(SdkClientConfiguration config) {
+ return config.merge(c -> c.option(SdkClientOption.ENDPOINT_PROVIDER, defaultEndpointProvider())
+ .option(SdkAdvancedClientOption.SIGNER, defaultSigner())
+ .option(SdkClientOption.CRC32_FROM_COMPRESSED_DATA_ENABLED, false));
+ }
+
+ @Override
+ protected final SdkClientConfiguration finalizeServiceConfiguration(SdkClientConfiguration config) {
+ List endpointInterceptors = new ArrayList<>();
+ endpointInterceptors.add(new H2ResolveEndpointInterceptor());
+ endpointInterceptors.add(new H2RequestSetEndpointInterceptor());
+ ClasspathInterceptorChainFactory interceptorFactory = new ClasspathInterceptorChainFactory();
+ List interceptors = interceptorFactory
+ .getInterceptors("software/amazon/awssdk/services/h2/execution.interceptors");
+ List additionalInterceptors = new ArrayList<>();
+ interceptors = CollectionUtils.mergeLists(endpointInterceptors, interceptors);
+ interceptors = CollectionUtils.mergeLists(interceptors, additionalInterceptors);
+ interceptors = CollectionUtils.mergeLists(interceptors, config.option(SdkClientOption.EXECUTION_INTERCEPTORS));
+ SdkClientConfiguration.Builder builder = config.toBuilder();
+ builder.lazyOption(SdkClientOption.IDENTITY_PROVIDERS, c -> {
+ IdentityProviders.Builder result = IdentityProviders.builder();
+ IdentityProvider> credentialsIdentityProvider = c.get(AwsClientOption.CREDENTIALS_IDENTITY_PROVIDER);
+ if (credentialsIdentityProvider != null) {
+ result.putIdentityProvider(credentialsIdentityProvider);
+ }
+ return result.build();
+ });
+ builder.option(SdkClientOption.EXECUTION_INTERCEPTORS, interceptors);
+ builder.lazyOptionIfAbsent(
+ SdkClientOption.CLIENT_ENDPOINT_PROVIDER,
+ c -> AwsClientEndpointProvider
+ .builder()
+ .serviceEndpointOverrideEnvironmentVariable("AWS_ENDPOINT_URL_H2_SERVICE")
+ .serviceEndpointOverrideSystemProperty("aws.endpointUrlH2")
+ .serviceProfileProperty("h2_service")
+ .serviceEndpointPrefix(serviceEndpointPrefix())
+ .defaultProtocol("https")
+ .region(c.get(AwsClientOption.AWS_REGION))
+ .profileFile(c.get(SdkClientOption.PROFILE_FILE_SUPPLIER))
+ .profileName(c.get(SdkClientOption.PROFILE_NAME))
+ .putAdvancedOption(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT,
+ c.get(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT))
+ .dualstackEnabled(c.get(AwsClientOption.DUALSTACK_ENDPOINT_ENABLED))
+ .fipsEnabled(c.get(AwsClientOption.FIPS_ENDPOINT_ENABLED)).build());
+ return builder.build();
+ }
+
+ private Signer defaultSigner() {
+ return Aws4Signer.create();
+ }
+
+ @Override
+ protected final String signingName() {
+ return "h2-service";
+ }
+
+ private H2EndpointProvider defaultEndpointProvider() {
+ return H2EndpointProvider.defaultProvider();
+ }
+
+ @Override
+ protected final AttributeMap serviceHttpConfig() {
+ AttributeMap result = AttributeMap.empty();
+ return result.merge(AttributeMap.builder().put(SdkHttpConfigurationOption.PROTOCOL, Protocol.HTTP2)
+ .put(SdkHttpConfigurationOption.PROTOCOL_NEGOTIATION, ProtocolNegotiation.ALPN).build());
+ }
+
+ @Override
+ protected SdkClientConfiguration invokePlugins(SdkClientConfiguration config) {
+ List internalPlugins = internalPlugins(config);
+ List externalPlugins = plugins();
+ if (internalPlugins.isEmpty() && externalPlugins.isEmpty()) {
+ return config;
+ }
+ List plugins = CollectionUtils.mergeLists(internalPlugins, externalPlugins);
+ SdkClientConfiguration.Builder configuration = config.toBuilder();
+ H2ServiceClientConfigurationBuilder serviceConfigBuilder = new H2ServiceClientConfigurationBuilder(configuration);
+ for (SdkPlugin plugin : plugins) {
+ plugin.configureClient(serviceConfigBuilder);
+ }
+ updateRetryStrategyClientConfiguration(configuration);
+ return configuration.build();
+ }
+
+ private void updateRetryStrategyClientConfiguration(SdkClientConfiguration.Builder configuration) {
+ ClientOverrideConfiguration.Builder builder = configuration.asOverrideConfigurationBuilder();
+ RetryMode retryMode = builder.retryMode();
+ if (retryMode != null) {
+ configuration.option(SdkClientOption.RETRY_STRATEGY, AwsRetryStrategy.forRetryMode(retryMode));
+ } else {
+ Consumer> configurator = builder.retryStrategyConfigurator();
+ if (configurator != null) {
+ RetryStrategy.Builder, ?> defaultBuilder = AwsRetryStrategy.defaultRetryStrategy().toBuilder();
+ configurator.accept(defaultBuilder);
+ configuration.option(SdkClientOption.RETRY_STRATEGY, defaultBuilder.build());
+ } else {
+ RetryStrategy retryStrategy = builder.retryStrategy();
+ if (retryStrategy != null) {
+ configuration.option(SdkClientOption.RETRY_STRATEGY, retryStrategy);
+ }
+ }
+ }
+ configuration.option(SdkClientOption.CONFIGURED_RETRY_MODE, null);
+ configuration.option(SdkClientOption.CONFIGURED_RETRY_STRATEGY, null);
+ configuration.option(SdkClientOption.CONFIGURED_RETRY_CONFIGURATOR, null);
+ }
+
+ private List internalPlugins(SdkClientConfiguration config) {
+ return Collections.emptyList();
+ }
+
+ protected static void validateClientOptions(SdkClientConfiguration c) {
+ Validate.notNull(c.option(SdkAdvancedClientOption.SIGNER),
+ "The 'overrideConfiguration.advancedOption[SIGNER]' must be configured in the client builder.");
+ }
+}
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-h2-usePriorKnowledgeForH2-service-client-builder-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-h2-usePriorKnowledgeForH2-service-client-builder-class.java
new file mode 100644
index 000000000000..b630574fb1d8
--- /dev/null
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-h2-usePriorKnowledgeForH2-service-client-builder-class.java
@@ -0,0 +1,168 @@
+package software.amazon.awssdk.services.h2;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+import software.amazon.awssdk.annotations.Generated;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.auth.signer.Aws4Signer;
+import software.amazon.awssdk.awscore.client.builder.AwsDefaultClientBuilder;
+import software.amazon.awssdk.awscore.client.config.AwsClientOption;
+import software.amazon.awssdk.awscore.endpoint.AwsClientEndpointProvider;
+import software.amazon.awssdk.awscore.retry.AwsRetryStrategy;
+import software.amazon.awssdk.core.SdkPlugin;
+import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
+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.interceptor.ClasspathInterceptorChainFactory;
+import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
+import software.amazon.awssdk.core.retry.RetryMode;
+import software.amazon.awssdk.core.signer.Signer;
+import software.amazon.awssdk.http.Protocol;
+import software.amazon.awssdk.http.SdkHttpConfigurationOption;
+import software.amazon.awssdk.identity.spi.IdentityProvider;
+import software.amazon.awssdk.identity.spi.IdentityProviders;
+import software.amazon.awssdk.regions.ServiceMetadataAdvancedOption;
+import software.amazon.awssdk.retries.api.RetryStrategy;
+import software.amazon.awssdk.services.h2.endpoints.H2EndpointProvider;
+import software.amazon.awssdk.services.h2.endpoints.internal.H2RequestSetEndpointInterceptor;
+import software.amazon.awssdk.services.h2.endpoints.internal.H2ResolveEndpointInterceptor;
+import software.amazon.awssdk.services.h2.internal.H2ServiceClientConfigurationBuilder;
+import software.amazon.awssdk.utils.AttributeMap;
+import software.amazon.awssdk.utils.CollectionUtils;
+import software.amazon.awssdk.utils.Validate;
+
+/**
+ * Internal base class for {@link DefaultH2ClientBuilder} and {@link DefaultH2AsyncClientBuilder}.
+ */
+@Generated("software.amazon.awssdk:codegen")
+@SdkInternalApi
+abstract class DefaultH2BaseClientBuilder, C> extends AwsDefaultClientBuilder {
+ @Override
+ protected final String serviceEndpointPrefix() {
+ return "h2-service";
+ }
+
+ @Override
+ protected final String serviceName() {
+ return "H2";
+ }
+
+ @Override
+ protected final SdkClientConfiguration mergeServiceDefaults(SdkClientConfiguration config) {
+ return config.merge(c -> c.option(SdkClientOption.ENDPOINT_PROVIDER, defaultEndpointProvider())
+ .option(SdkAdvancedClientOption.SIGNER, defaultSigner())
+ .option(SdkClientOption.CRC32_FROM_COMPRESSED_DATA_ENABLED, false));
+ }
+
+ @Override
+ protected final SdkClientConfiguration finalizeServiceConfiguration(SdkClientConfiguration config) {
+ List endpointInterceptors = new ArrayList<>();
+ endpointInterceptors.add(new H2ResolveEndpointInterceptor());
+ endpointInterceptors.add(new H2RequestSetEndpointInterceptor());
+ ClasspathInterceptorChainFactory interceptorFactory = new ClasspathInterceptorChainFactory();
+ List interceptors = interceptorFactory
+ .getInterceptors("software/amazon/awssdk/services/h2/execution.interceptors");
+ List additionalInterceptors = new ArrayList<>();
+ interceptors = CollectionUtils.mergeLists(endpointInterceptors, interceptors);
+ interceptors = CollectionUtils.mergeLists(interceptors, additionalInterceptors);
+ interceptors = CollectionUtils.mergeLists(interceptors, config.option(SdkClientOption.EXECUTION_INTERCEPTORS));
+ SdkClientConfiguration.Builder builder = config.toBuilder();
+ builder.lazyOption(SdkClientOption.IDENTITY_PROVIDERS, c -> {
+ IdentityProviders.Builder result = IdentityProviders.builder();
+ IdentityProvider> credentialsIdentityProvider = c.get(AwsClientOption.CREDENTIALS_IDENTITY_PROVIDER);
+ if (credentialsIdentityProvider != null) {
+ result.putIdentityProvider(credentialsIdentityProvider);
+ }
+ return result.build();
+ });
+ builder.option(SdkClientOption.EXECUTION_INTERCEPTORS, interceptors);
+ builder.lazyOptionIfAbsent(
+ SdkClientOption.CLIENT_ENDPOINT_PROVIDER,
+ c -> AwsClientEndpointProvider
+ .builder()
+ .serviceEndpointOverrideEnvironmentVariable("AWS_ENDPOINT_URL_H2_SERVICE")
+ .serviceEndpointOverrideSystemProperty("aws.endpointUrlH2")
+ .serviceProfileProperty("h2_service")
+ .serviceEndpointPrefix(serviceEndpointPrefix())
+ .defaultProtocol("https")
+ .region(c.get(AwsClientOption.AWS_REGION))
+ .profileFile(c.get(SdkClientOption.PROFILE_FILE_SUPPLIER))
+ .profileName(c.get(SdkClientOption.PROFILE_NAME))
+ .putAdvancedOption(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT,
+ c.get(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT))
+ .dualstackEnabled(c.get(AwsClientOption.DUALSTACK_ENDPOINT_ENABLED))
+ .fipsEnabled(c.get(AwsClientOption.FIPS_ENDPOINT_ENABLED)).build());
+ return builder.build();
+ }
+
+ private Signer defaultSigner() {
+ return Aws4Signer.create();
+ }
+
+ @Override
+ protected final String signingName() {
+ return "h2-service";
+ }
+
+ private H2EndpointProvider defaultEndpointProvider() {
+ return H2EndpointProvider.defaultProvider();
+ }
+
+ @Override
+ protected final AttributeMap serviceHttpConfig() {
+ AttributeMap result = AttributeMap.empty();
+ return result.merge(AttributeMap.builder().put(SdkHttpConfigurationOption.PROTOCOL, Protocol.HTTP2).build());
+ }
+
+ @Override
+ protected SdkClientConfiguration invokePlugins(SdkClientConfiguration config) {
+ List internalPlugins = internalPlugins(config);
+ List externalPlugins = plugins();
+ if (internalPlugins.isEmpty() && externalPlugins.isEmpty()) {
+ return config;
+ }
+ List plugins = CollectionUtils.mergeLists(internalPlugins, externalPlugins);
+ SdkClientConfiguration.Builder configuration = config.toBuilder();
+ H2ServiceClientConfigurationBuilder serviceConfigBuilder = new H2ServiceClientConfigurationBuilder(configuration);
+ for (SdkPlugin plugin : plugins) {
+ plugin.configureClient(serviceConfigBuilder);
+ }
+ updateRetryStrategyClientConfiguration(configuration);
+ return configuration.build();
+ }
+
+ private void updateRetryStrategyClientConfiguration(SdkClientConfiguration.Builder configuration) {
+ ClientOverrideConfiguration.Builder builder = configuration.asOverrideConfigurationBuilder();
+ RetryMode retryMode = builder.retryMode();
+ if (retryMode != null) {
+ configuration.option(SdkClientOption.RETRY_STRATEGY, AwsRetryStrategy.forRetryMode(retryMode));
+ } else {
+ Consumer> configurator = builder.retryStrategyConfigurator();
+ if (configurator != null) {
+ RetryStrategy.Builder, ?> defaultBuilder = AwsRetryStrategy.defaultRetryStrategy().toBuilder();
+ configurator.accept(defaultBuilder);
+ configuration.option(SdkClientOption.RETRY_STRATEGY, defaultBuilder.build());
+ } else {
+ RetryStrategy retryStrategy = builder.retryStrategy();
+ if (retryStrategy != null) {
+ configuration.option(SdkClientOption.RETRY_STRATEGY, retryStrategy);
+ }
+ }
+ }
+ configuration.option(SdkClientOption.CONFIGURED_RETRY_MODE, null);
+ configuration.option(SdkClientOption.CONFIGURED_RETRY_STRATEGY, null);
+ configuration.option(SdkClientOption.CONFIGURED_RETRY_CONFIGURATOR, null);
+ }
+
+ private List internalPlugins(SdkClientConfiguration config) {
+ return Collections.emptyList();
+ }
+
+ protected static void validateClientOptions(SdkClientConfiguration c) {
+ Validate.notNull(c.option(SdkAdvancedClientOption.SIGNER),
+ "The 'overrideConfiguration.advancedOption[SIGNER]' must be configured in the client builder.");
+ }
+}
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/service-with-h2-usePriorKnowledgeForH2/customization.config b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/service-with-h2-usePriorKnowledgeForH2/customization.config
new file mode 100644
index 000000000000..c087abe8d912
--- /dev/null
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/service-with-h2-usePriorKnowledgeForH2/customization.config
@@ -0,0 +1,3 @@
+{
+ "usePriorKnowledgeForH2": true
+}
\ No newline at end of file
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/service-with-h2-usePriorKnowledgeForH2/service-2.json b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/service-with-h2-usePriorKnowledgeForH2/service-2.json
new file mode 100644
index 000000000000..310b3ca0ed1b
--- /dev/null
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/service-with-h2-usePriorKnowledgeForH2/service-2.json
@@ -0,0 +1,37 @@
+{
+ "version":"2.0",
+ "metadata":{
+ "apiVersion":"2016-03-11",
+ "endpointPrefix":"h2-service",
+ "jsonVersion":"1.1",
+ "protocol":"rest-json",
+ "protocolSettings":{"h2":"eventstream"},
+ "serviceAbbreviation":"H2 Service",
+ "serviceFullName":"H2 Test Service",
+ "serviceId":"H2 Service",
+ "signatureVersion":"v4",
+ "targetPrefix":"ProtocolTestsService",
+ "uid":"restjson-2016-03-11"
+ },
+ "operations":{
+ "OneOperation":{
+ "name":"OneOperation",
+ "http":{
+ "method":"POST",
+ "requestUri":"/2016-03-11/oneoperation"
+ },
+ "input":{"shape":"OneShape"}
+ }
+ },
+ "shapes": {
+ "OneShape": {
+ "type": "structure",
+ "members": {
+ "StringMember": {
+ "shape": "String"
+ }
+ }
+ },
+ "String":{"type":"string"}
+ }
+}
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/service-with-h2/customization.config b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/service-with-h2/customization.config
new file mode 100644
index 000000000000..0e0dcd235c49
--- /dev/null
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/service-with-h2/customization.config
@@ -0,0 +1,3 @@
+{
+
+}
\ No newline at end of file
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/service-with-h2/service-2.json b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/service-with-h2/service-2.json
new file mode 100644
index 000000000000..310b3ca0ed1b
--- /dev/null
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/service-with-h2/service-2.json
@@ -0,0 +1,37 @@
+{
+ "version":"2.0",
+ "metadata":{
+ "apiVersion":"2016-03-11",
+ "endpointPrefix":"h2-service",
+ "jsonVersion":"1.1",
+ "protocol":"rest-json",
+ "protocolSettings":{"h2":"eventstream"},
+ "serviceAbbreviation":"H2 Service",
+ "serviceFullName":"H2 Test Service",
+ "serviceId":"H2 Service",
+ "signatureVersion":"v4",
+ "targetPrefix":"ProtocolTestsService",
+ "uid":"restjson-2016-03-11"
+ },
+ "operations":{
+ "OneOperation":{
+ "name":"OneOperation",
+ "http":{
+ "method":"POST",
+ "requestUri":"/2016-03-11/oneoperation"
+ },
+ "input":{"shape":"OneShape"}
+ }
+ },
+ "shapes": {
+ "OneShape": {
+ "type": "structure",
+ "members": {
+ "StringMember": {
+ "shape": "String"
+ }
+ }
+ },
+ "String":{"type":"string"}
+ }
+}
diff --git a/http-client-spi/src/main/java/software/amazon/awssdk/http/ProtocolNegotiation.java b/http-client-spi/src/main/java/software/amazon/awssdk/http/ProtocolNegotiation.java
new file mode 100644
index 000000000000..e25a88f0adaa
--- /dev/null
+++ b/http-client-spi/src/main/java/software/amazon/awssdk/http/ProtocolNegotiation.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+import software.amazon.awssdk.annotations.SdkPublicApi;
+
+/**
+ * The protocol negotiation selection scheme used by the HTTP client to establish connections
+ */
+@SdkPublicApi
+public enum ProtocolNegotiation {
+
+ /**
+ * Uses prior knowledge
+ */
+ ASSUME_PROTOCOL,
+
+ /**
+ * Uses Application Level Protocol Negotiation
+ */
+ ALPN
+}
diff --git a/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpConfigurationOption.java b/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpConfigurationOption.java
index 42074fbe76dd..e9efa8b1814b 100644
--- a/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpConfigurationOption.java
+++ b/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpConfigurationOption.java
@@ -78,6 +78,12 @@ public final class SdkHttpConfigurationOption extends AttributeMap.Key {
public static final SdkHttpConfigurationOption PROTOCOL =
new SdkHttpConfigurationOption<>("Protocol", Protocol.class);
+ /**
+ * HTTP protocol negotiation to use.
+ */
+ public static final SdkHttpConfigurationOption PROTOCOL_NEGOTIATION =
+ new SdkHttpConfigurationOption<>("ProtocolNegotiation", ProtocolNegotiation.class);
+
/**
* Maximum number of requests allowed to wait for a connection.
*/
@@ -148,6 +154,7 @@ public final class SdkHttpConfigurationOption extends AttributeMap.Key {
private static final Boolean DEFAULT_TRUST_ALL_CERTIFICATES = Boolean.FALSE;
private static final Protocol DEFAULT_PROTOCOL = Protocol.HTTP1_1;
+ private static final ProtocolNegotiation DEFAULT_PROTOCOL_NEGOTIATION = ProtocolNegotiation.ASSUME_PROTOCOL;
private static final TlsTrustManagersProvider DEFAULT_TLS_TRUST_MANAGERS_PROVIDER = null;
private static final TlsKeyManagersProvider DEFAULT_TLS_KEY_MANAGERS_PROVIDER = SystemPropertyTlsKeyManagersProvider.create();
@@ -163,6 +170,7 @@ public final class SdkHttpConfigurationOption extends AttributeMap.Key {
.put(MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
.put(MAX_PENDING_CONNECTION_ACQUIRES, DEFAULT_MAX_CONNECTION_ACQUIRES)
.put(PROTOCOL, DEFAULT_PROTOCOL)
+ .put(PROTOCOL_NEGOTIATION, DEFAULT_PROTOCOL_NEGOTIATION)
.put(TRUST_ALL_CERTIFICATES, DEFAULT_TRUST_ALL_CERTIFICATES)
.put(REAP_IDLE_CONNECTIONS, DEFAULT_REAP_IDLE_CONNECTIONS)
.put(TCP_KEEPALIVE, DEFAULT_TCP_KEEPALIVE)
diff --git a/http-clients/netty-nio-client/pom.xml b/http-clients/netty-nio-client/pom.xml
index e40302efea87..affd9965e3ae 100644
--- a/http-clients/netty-nio-client/pom.xml
+++ b/http-clients/netty-nio-client/pom.xml
@@ -177,8 +177,61 @@
rxjava
test
+
+ org.eclipse.jetty
+ jetty-server
+ test
+
+
+ org.eclipse.jetty
+ jetty-servlet
+ test
+
+
+ org.eclipse.jetty.http2
+ http2-server
+ test
+
+
+ org.eclipse.jetty
+ jetty-alpn-server
+ test
+
+
+ org.eclipse.jetty.http2
+ http2-common
+ test
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ test
+
+
+ org.eclipse.jetty
+ jetty-http
+ test
+
+
+ org.eclipse.jetty
+ jetty-util
+ test
+
+
+
+
+ org.eclipse.jetty
+ jetty-bom
+ ${jetty.version}
+ pom
+ import
+
+
+
+
diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClient.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClient.java
index 70f62e1f6e77..ab395b542513 100644
--- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClient.java
+++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClient.java
@@ -19,7 +19,9 @@
import static software.amazon.awssdk.http.nio.netty.internal.NettyConfiguration.EVENTLOOP_SHUTDOWN_FUTURE_TIMEOUT_SECONDS;
import static software.amazon.awssdk.http.nio.netty.internal.NettyConfiguration.EVENTLOOP_SHUTDOWN_QUIET_PERIOD_SECONDS;
import static software.amazon.awssdk.http.nio.netty.internal.NettyConfiguration.EVENTLOOP_SHUTDOWN_TIMEOUT_SECONDS;
+import static software.amazon.awssdk.http.nio.netty.internal.utils.NettyUtils.isAlpnSupported;
import static software.amazon.awssdk.http.nio.netty.internal.utils.NettyUtils.runAndLogError;
+import static software.amazon.awssdk.http.nio.netty.internal.utils.NettyUtils.validateAlpnSupported;
import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;
import io.netty.channel.ChannelOption;
@@ -37,6 +39,7 @@
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.annotations.SdkTestInternalApi;
import software.amazon.awssdk.http.Protocol;
+import software.amazon.awssdk.http.ProtocolNegotiation;
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
import software.amazon.awssdk.http.SdkHttpRequest;
import software.amazon.awssdk.http.SystemPropertyTlsKeyManagersProvider;
@@ -82,10 +85,14 @@ public final class NettyNioAsyncHttpClient implements SdkAsyncHttpClient {
private final SdkEventLoopGroup sdkEventLoopGroup;
private final SdkChannelPoolMap pools;
private final NettyConfiguration configuration;
+ private final ProtocolNegotiation protocolNegotiation;
private NettyNioAsyncHttpClient(DefaultBuilder builder, AttributeMap serviceDefaultsMap) {
this.configuration = new NettyConfiguration(serviceDefaultsMap);
Protocol protocol = serviceDefaultsMap.get(SdkHttpConfigurationOption.PROTOCOL);
+ SslProvider sslProvider = resolveSslProvider(builder);
+ this.protocolNegotiation = resolveProtocolNegotiation(builder.protocolNegotiation, serviceDefaultsMap,
+ protocol, sslProvider);
this.sdkEventLoopGroup = eventLoopGroup(builder);
Http2Configuration http2Configuration = builder.http2Configuration;
@@ -97,11 +104,12 @@ private NettyNioAsyncHttpClient(DefaultBuilder builder, AttributeMap serviceDefa
.sdkChannelOptions(builder.sdkChannelOptions)
.configuration(configuration)
.protocol(protocol)
+ .protocolNegotiation(protocolNegotiation)
.maxStreams(maxStreams)
.initialWindowSize(initialWindowSize)
.healthCheckPingPeriod(resolveHealthCheckPingPeriod(http2Configuration))
.sdkEventLoopGroup(sdkEventLoopGroup)
- .sslProvider(resolveSslProvider(builder))
+ .sslProvider(sslProvider)
.proxyConfiguration(builder.proxyConfiguration)
.useNonBlockingDnsResolver(builder.useNonBlockingDnsResolver)
.build();
@@ -110,19 +118,29 @@ private NettyNioAsyncHttpClient(DefaultBuilder builder, AttributeMap serviceDefa
@SdkTestInternalApi
NettyNioAsyncHttpClient(SdkEventLoopGroup sdkEventLoopGroup,
SdkChannelPoolMap pools,
- NettyConfiguration configuration) {
+ NettyConfiguration configuration,
+ ProtocolNegotiation protocolNegotiation) {
this.sdkEventLoopGroup = sdkEventLoopGroup;
this.pools = pools;
this.configuration = configuration;
+ this.protocolNegotiation = protocolNegotiation;
}
@Override
public CompletableFuture execute(AsyncExecuteRequest request) {
+ failIfAlpnUsedWithHttp(request);
RequestContext ctx = createRequestContext(request);
ctx.metricCollector().reportMetric(HTTP_CLIENT_NAME, clientName()); // TODO: Can't this be done in core?
return new NettyRequestExecutor(ctx).execute();
}
+ private void failIfAlpnUsedWithHttp(AsyncExecuteRequest request) {
+ if (protocolNegotiation == ProtocolNegotiation.ALPN
+ && "http".equals(request.request().protocol())) {
+ throw new UnsupportedOperationException("ALPN can only be used with HTTPS, not HTTP.");
+ }
+ }
+
public static Builder builder() {
return new DefaultBuilder();
}
@@ -162,6 +180,36 @@ private SslProvider resolveSslProvider(DefaultBuilder builder) {
return SslContext.defaultClientProvider();
}
+ private ProtocolNegotiation resolveProtocolNegotiation(ProtocolNegotiation userSetValue, AttributeMap serviceDefaultsMap,
+ Protocol protocol, SslProvider sslProvider) {
+ if (userSetValue == ProtocolNegotiation.ALPN) {
+ // TODO - remove once we implement support for ALPN with HTTP1
+ if (protocol == Protocol.HTTP1_1) {
+ throw new UnsupportedOperationException("ALPN with HTTP/1.1 is not yet supported, use prior knowledge instead "
+ + "with ProtocolNegotiation.ASSUME_PROTOCOL, or use ALPN with H2.");
+ }
+
+ // throw error if not supported and user set ALPN
+ validateAlpnSupported(sslProvider);
+ return ProtocolNegotiation.ALPN;
+ }
+ if (userSetValue == ProtocolNegotiation.ASSUME_PROTOCOL) {
+ return ProtocolNegotiation.ASSUME_PROTOCOL;
+ }
+
+ ProtocolNegotiation protocolNegotiation = serviceDefaultsMap.get(SdkHttpConfigurationOption.PROTOCOL_NEGOTIATION);
+ if (protocolNegotiation == ProtocolNegotiation.ALPN) {
+ if (!isAlpnSupported(sslProvider)) {
+ // fallback to prior knowledge if not supported and SDK defaults to ALPN
+ protocolNegotiation = ProtocolNegotiation.ASSUME_PROTOCOL;
+ log.warn(null, () -> "ALPN is not supported in the current Java version, falling back to prior knowledge for "
+ + "protocol negotiation");
+ }
+ }
+
+ return protocolNegotiation;
+ }
+
private long resolveMaxHttp2Streams(Integer topLevelValue, Http2Configuration http2Configuration) {
if (topLevelValue != null) {
return topLevelValue;
@@ -371,6 +419,25 @@ public interface Builder extends SdkAsyncHttpClient.BuilderDefault values:
+ *
+ * - For services with H2 protocol setting, the default value is {@code ProtocolNegotiation.ALPN},
+ * with the exception of the following services: Kinesis, Transcribe Streaming, Lex Runtime v2, Q Business.
+ * - For all other services, the default value is {@code ProtocolNegotiation.ASSUME_PROTOCOL}, in which case the SDK
+ * will use prior knowledge to establish connections.
+ *
+ * Note: For Java 8, ALPN is only supported in versions 1.8.0_251 and newer.
+ * If on an unsupported Java version and using {@code SslProvider.JDK}:
+ *
+ * - Default SDK setting of ALPN → SDK will fallback to prior knowledge and not use ALPN.
+ * - User explicitly sets value of ALPN → Exception will be thrown.
+ *
+ */
+ Builder protocolNegotiation(ProtocolNegotiation protocolNegotiation);
+
/**
* Configure whether to enable or disable TCP KeepAlive.
* The configuration will be passed to the socket option {@link SocketOptions#SO_KEEPALIVE}.
@@ -503,6 +570,7 @@ private static final class DefaultBuilder implements Builder {
private SslProvider sslProvider;
private ProxyConfiguration proxyConfiguration = ProxyConfiguration.builder().build();
private Boolean useNonBlockingDnsResolver;
+ private ProtocolNegotiation protocolNegotiation;
private DefaultBuilder() {
}
@@ -640,8 +708,14 @@ public Builder protocol(Protocol protocol) {
return this;
}
- public void setProtocol(Protocol protocol) {
- protocol(protocol);
+ @Override
+ public Builder protocolNegotiation(ProtocolNegotiation protocolNegotiation) {
+ this.protocolNegotiation = protocolNegotiation;
+ return this;
+ }
+
+ public void setProtocolNegotiation(ProtocolNegotiation protocolNegotiation) {
+ protocolNegotiation(protocolNegotiation);
}
@Override
diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java
index e2fbd1c1adca..82e3edc764a9 100644
--- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java
+++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java
@@ -39,6 +39,7 @@
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.annotations.SdkTestInternalApi;
import software.amazon.awssdk.http.Protocol;
+import software.amazon.awssdk.http.ProtocolNegotiation;
import software.amazon.awssdk.http.nio.netty.ProxyConfiguration;
import software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup;
import software.amazon.awssdk.http.nio.netty.internal.http2.HttpOrHttp2ChannelPool;
@@ -76,6 +77,7 @@ public void channelCreated(Channel ch) throws Exception {
private final NettyConfiguration configuration;
private final Protocol protocol;
+ private final ProtocolNegotiation protocolNegotiation;
private final long maxStreams;
private final Duration healthCheckPingPeriod;
private final int initialWindowSize;
@@ -88,13 +90,14 @@ public void channelCreated(Channel ch) throws Exception {
private AwaitCloseChannelPoolMap(Builder builder, Function createBootStrapProvider) {
this.configuration = builder.configuration;
this.protocol = builder.protocol;
+ this.protocolNegotiation = builder.protocolNegotiation;
this.maxStreams = builder.maxStreams;
this.healthCheckPingPeriod = builder.healthCheckPingPeriod;
this.initialWindowSize = builder.initialWindowSize;
this.sslProvider = builder.sslProvider;
this.proxyConfiguration = builder.proxyConfiguration;
this.bootstrapProvider = createBootStrapProvider.apply(builder);
- this.sslContextProvider = new SslContextProvider(configuration, protocol, sslProvider);
+ this.sslContextProvider = new SslContextProvider(configuration, protocol, protocolNegotiation, sslProvider);
this.useNonBlockingDnsResolver = builder.useNonBlockingDnsResolver;
}
@@ -126,6 +129,7 @@ protected SimpleChannelPoolAwareChannelPool newPool(URI key) {
AtomicReference channelPoolRef = new AtomicReference<>();
ChannelPipelineInitializer pipelineInitializer = new ChannelPipelineInitializer(protocol,
+ protocolNegotiation,
sslContext,
sslProvider,
maxStreams,
@@ -282,6 +286,7 @@ public static class Builder {
private SdkEventLoopGroup sdkEventLoopGroup;
private NettyConfiguration configuration;
private Protocol protocol;
+ private ProtocolNegotiation protocolNegotiation;
private long maxStreams;
private int initialWindowSize;
private Duration healthCheckPingPeriod;
@@ -312,6 +317,11 @@ public Builder protocol(Protocol protocol) {
return this;
}
+ public Builder protocolNegotiation(ProtocolNegotiation protocolNegotiation) {
+ this.protocolNegotiation = protocolNegotiation;
+ return this;
+ }
+
public Builder maxStreams(long maxStreams) {
this.maxStreams = maxStreams;
return this;
diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ChannelPipelineInitializer.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ChannelPipelineInitializer.java
index 80561c84db49..6401a8ecc584 100644
--- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ChannelPipelineInitializer.java
+++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ChannelPipelineInitializer.java
@@ -26,6 +26,7 @@
import io.netty.buffer.UnpooledByteBufAllocator;
import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
@@ -39,6 +40,8 @@
import io.netty.handler.codec.http2.Http2Settings;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
+import io.netty.handler.ssl.ApplicationProtocolNames;
+import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.ssl.SslProvider;
@@ -48,6 +51,7 @@
import java.util.concurrent.atomic.AtomicReference;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.http.Protocol;
+import software.amazon.awssdk.http.ProtocolNegotiation;
import software.amazon.awssdk.http.nio.netty.internal.http2.Http2GoAwayEventListener;
import software.amazon.awssdk.http.nio.netty.internal.http2.Http2PingHandler;
import software.amazon.awssdk.http.nio.netty.internal.http2.Http2SettingsFrameHandler;
@@ -58,6 +62,7 @@
@SdkInternalApi
public final class ChannelPipelineInitializer extends AbstractChannelPoolHandler {
private final Protocol protocol;
+ private final ProtocolNegotiation protocolNegotiation;
private final SslContext sslCtx;
private final SslProvider sslProvider;
private final long clientMaxStreams;
@@ -68,6 +73,7 @@ public final class ChannelPipelineInitializer extends AbstractChannelPoolHandler
private final URI poolKey;
public ChannelPipelineInitializer(Protocol protocol,
+ ProtocolNegotiation protocolNegotiation,
SslContext sslCtx,
SslProvider sslProvider,
long clientMaxStreams,
@@ -77,6 +83,7 @@ public ChannelPipelineInitializer(Protocol protocol,
NettyConfiguration configuration,
URI poolKey) {
this.protocol = protocol;
+ this.protocolNegotiation = protocolNegotiation;
this.sslCtx = sslCtx;
this.sslProvider = sslProvider;
this.clientMaxStreams = clientMaxStreams;
@@ -107,30 +114,39 @@ public void channelCreated(Channel ch) {
}
}
- if (protocol == Protocol.HTTP2) {
- configureHttp2(ch, pipeline);
- } else {
- configureHttp11(ch, pipeline);
- }
+ configureProtocolHandlers(ch, pipeline, protocol);
+ configurePostProtocolHandlers(pipeline, protocol);
+ }
- if (configuration.reapIdleConnections()) {
- pipeline.addLast(new IdleConnectionReaperHandler(configuration.idleTimeoutMillis()));
+ private void configureProtocolHandlers(Channel ch, ChannelPipeline pipeline, Protocol protocol) {
+ switch (protocolNegotiation) {
+ case ASSUME_PROTOCOL:
+ configureAssumeProtocol(ch, pipeline, protocol);
+ break;
+ case ALPN:
+ configureAlpn(pipeline, protocol);
+ break;
+ default:
+ throw new UnsupportedOperationException("Unsupported ProtocolNegotiation: " + protocolNegotiation);
}
+ }
- if (configuration.connectionTtlMillis() > 0) {
- pipeline.addLast(new OldConnectionReaperHandler(configuration.connectionTtlMillis()));
+ private void configureAlpn(ChannelPipeline pipeline, Protocol protocol) {
+ if (protocol == Protocol.HTTP1_1) {
+ // TODO - remove once we implement support for ALPN with HTTP1
+ throw new UnsupportedOperationException("ALPN with HTTP1 is not yet supported, use prior knowledge instead with "
+ + "ProtocolNegotiation.ASSUME_PROTOCOL, or use ALPN with H2.");
+ } else if (protocol == Protocol.HTTP2) {
+ configureAlpnH2(pipeline);
}
+ }
- pipeline.addLast(FutureCancelHandler.getInstance());
-
- // Only add it for h1 channel because it does not apply to
- // h2 connection channel. It will be attached
- // to stream channels when they are created.
+ private void configureAssumeProtocol(Channel ch, ChannelPipeline pipeline, Protocol protocol) {
if (protocol == Protocol.HTTP1_1) {
- pipeline.addLast(UnusedChannelExceptionHandler.getInstance());
+ configureHttp11(ch, pipeline);
+ } else if (protocol == Protocol.HTTP2) {
+ configureHttp2(ch, pipeline);
}
-
- pipeline.addLast(new LoggingHandler(LogLevel.DEBUG));
}
private void configureHttp2(Channel ch, ChannelPipeline pipeline) {
@@ -167,11 +183,45 @@ private void configureHttp11(Channel ch, ChannelPipeline pipeline) {
ch.attr(PROTOCOL_FUTURE).get().complete(Protocol.HTTP1_1);
}
+ private void configureAlpnH2(ChannelPipeline pipeline) {
+ pipeline.addLast(new ApplicationProtocolNegotiationHandler("") {
+ @Override
+ protected void configurePipeline(ChannelHandlerContext ctx, String protocol) {
+ if (protocol.equals(ApplicationProtocolNames.HTTP_2)) {
+ configureHttp2(ctx.channel(), ctx.pipeline());
+ } else {
+ ctx.channel().attr(PROTOCOL_FUTURE).get()
+ .completeExceptionally(new UnsupportedOperationException("The server does not support ALPN with H2"));
+ ctx.close();
+ }
+ }
+ });
+ }
+
+ private void configurePostProtocolHandlers(ChannelPipeline pipeline, Protocol protocol) {
+ if (configuration.reapIdleConnections()) {
+ pipeline.addLast(new IdleConnectionReaperHandler(configuration.idleTimeoutMillis()));
+ }
+
+ if (configuration.connectionTtlMillis() > 0) {
+ pipeline.addLast(new OldConnectionReaperHandler(configuration.connectionTtlMillis()));
+ }
+
+ pipeline.addLast(FutureCancelHandler.getInstance());
+
+ // Only add it for h1 channel because it does not apply to
+ // h2 connection channel. It will be attached
+ // to stream channels when they are created.
+ if (protocol == Protocol.HTTP1_1) {
+ pipeline.addLast(UnusedChannelExceptionHandler.getInstance());
+ }
+
+ pipeline.addLast(new LoggingHandler(LogLevel.DEBUG));
+ }
+
private static class NoOpChannelInitializer extends ChannelInitializer {
@Override
protected void initChannel(Channel ch) {
}
}
}
-
-
diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/SslContextProvider.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/SslContextProvider.java
index 2947c6dedae8..b7ed00353941 100644
--- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/SslContextProvider.java
+++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/SslContextProvider.java
@@ -16,6 +16,8 @@
package software.amazon.awssdk.http.nio.netty.internal;
import io.netty.handler.codec.http2.Http2SecurityUtil;
+import io.netty.handler.ssl.ApplicationProtocolConfig;
+import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslProvider;
@@ -28,6 +30,7 @@
import javax.net.ssl.TrustManagerFactory;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.http.Protocol;
+import software.amazon.awssdk.http.ProtocolNegotiation;
import software.amazon.awssdk.http.SystemPropertyTlsKeyManagersProvider;
import software.amazon.awssdk.http.TlsTrustManagersProvider;
import software.amazon.awssdk.http.nio.netty.internal.utils.NettyClientLogger;
@@ -37,12 +40,15 @@
public final class SslContextProvider {
private static final NettyClientLogger log = NettyClientLogger.getLogger(SslContextProvider.class);
private final Protocol protocol;
+ private final ProtocolNegotiation protocolNegotiation;
private final SslProvider sslProvider;
private final TrustManagerFactory trustManagerFactory;
private final KeyManagerFactory keyManagerFactory;
- public SslContextProvider(NettyConfiguration configuration, Protocol protocol, SslProvider sslProvider) {
+ public SslContextProvider(NettyConfiguration configuration, Protocol protocol, ProtocolNegotiation protocolNegotiation,
+ SslProvider sslProvider) {
this.protocol = protocol;
+ this.protocolNegotiation = protocolNegotiation;
this.sslProvider = sslProvider;
this.trustManagerFactory = getTrustManager(configuration);
this.keyManagerFactory = getKeyManager(configuration);
@@ -50,17 +56,48 @@ public SslContextProvider(NettyConfiguration configuration, Protocol protocol, S
public SslContext sslContext() {
try {
- return SslContextBuilder.forClient()
- .sslProvider(sslProvider)
- .ciphers(getCiphers(), SupportedCipherSuiteFilter.INSTANCE)
- .trustManager(trustManagerFactory)
- .keyManager(keyManagerFactory)
- .build();
+ SslContextBuilder builder = SslContextBuilder.forClient()
+ .sslProvider(sslProvider)
+ .ciphers(getCiphers(), SupportedCipherSuiteFilter.INSTANCE)
+ .trustManager(trustManagerFactory)
+ .keyManager(keyManagerFactory);
+
+ addAlpnConfigIfEnabled(builder);
+
+ return builder.build();
} catch (SSLException e) {
throw new RuntimeException(e);
}
}
+ private SslContextBuilder addAlpnConfigIfEnabled(SslContextBuilder builder) {
+ if (protocolNegotiation != ProtocolNegotiation.ALPN) {
+ return builder;
+ }
+
+ ApplicationProtocolConfig.SelectorFailureBehavior selectorFailureBehavior;
+ ApplicationProtocolConfig.SelectedListenerFailureBehavior selectedListenerFailureBehavior;
+
+ if (sslProvider == SslProvider.OPENSSL || sslProvider == SslProvider.OPENSSL_REFCNT) {
+ // OpenSSL does not support FATAL_ALERT
+ selectorFailureBehavior = ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE;
+ selectedListenerFailureBehavior = ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT;
+ } else {
+ selectorFailureBehavior = ApplicationProtocolConfig.SelectorFailureBehavior.FATAL_ALERT;
+ selectedListenerFailureBehavior = ApplicationProtocolConfig.SelectedListenerFailureBehavior.FATAL_ALERT;
+ }
+
+ return builder.applicationProtocolConfig(
+ new ApplicationProtocolConfig(ApplicationProtocolConfig.Protocol.ALPN,
+ selectorFailureBehavior,
+ selectedListenerFailureBehavior,
+ resolveNettyProtocol(protocol)));
+ }
+
+ private String resolveNettyProtocol(Protocol protocol) {
+ return protocol == Protocol.HTTP2 ? ApplicationProtocolNames.HTTP_2 : ApplicationProtocolNames.HTTP_1_1;
+ }
+
/**
* HTTP/2: per Rfc7540, there is a blocked list of cipher suites for HTTP/2, so setting
* the recommended cipher suites directly here
diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/utils/NettyUtils.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/utils/NettyUtils.java
index 827e2f41c3ac..6a9d0eedb682 100644
--- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/utils/NettyUtils.java
+++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/utils/NettyUtils.java
@@ -22,6 +22,7 @@
import io.netty.channel.EventLoop;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
+import io.netty.handler.ssl.SslProvider;
import io.netty.handler.timeout.ReadTimeoutException;
import io.netty.handler.timeout.WriteTimeoutException;
import io.netty.util.AttributeKey;
@@ -31,7 +32,13 @@
import io.netty.util.concurrent.Promise;
import io.netty.util.concurrent.SucceededFuture;
import java.io.IOException;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
import java.nio.channels.ClosedChannelException;
+import java.security.AccessController;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@@ -39,11 +46,13 @@
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
+import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.http.nio.netty.internal.ChannelDiagnostics;
import software.amazon.awssdk.utils.FunctionalUtils;
+import software.amazon.awssdk.utils.Lazy;
import software.amazon.awssdk.utils.Logger;
@SdkInternalApi
@@ -61,6 +70,8 @@ public final class NettyUtils {
+ "read or written in a timely manner.";
private static final Logger log = Logger.loggerFor(NettyUtils.class);
+ private static final Lazy ALPN_SUPPORTED = new Lazy<>(NettyUtils::checkAlpnSupport);
+
private NettyUtils() {
}
@@ -388,4 +399,49 @@ public static void runAndLogError(NettyClientLogger log, String errorMsg, Functi
log.error(null, () -> errorMsg, e);
}
}
+
+ // ALPN supported backported in u251
+ // https://bugs.openjdk.org/browse/JDK-8242894
+ public static boolean isAlpnSupported(SslProvider sslProvider) {
+ if (sslProvider != SslProvider.JDK) {
+ return true;
+ }
+
+ return ALPN_SUPPORTED.getValue();
+ }
+
+ private static boolean checkAlpnSupport() {
+ try {
+ SSLContext context = SSLContext.getInstance("TLS");
+ context.init(null, null, null);
+ SSLEngine engine = context.createSSLEngine();
+ MethodHandles.Lookup lookup = MethodHandles.lookup();
+
+ MethodHandle getApplicationProtocol = AccessController.doPrivileged(
+ (PrivilegedExceptionAction) () ->
+ lookup.findVirtual(SSLEngine.class, "getApplicationProtocol", MethodType.methodType(String.class)));
+
+ getApplicationProtocol.invoke(engine);
+ return true;
+ } catch (PrivilegedActionException e) {
+ log.debug(() -> "ALPN not supported: SSLEngine.getApplicationProtocol() method not found: " + e);
+ return false;
+ } catch (Throwable t) {
+ log.debug(() -> "ALPN support check failed: " + t);
+ return false;
+ }
+ }
+
+ public static String getJavaVersion() {
+ // CHECKSTYLE:OFF
+ return System.getProperty("java.version");
+ // CHECKSTYLE:ON
+ }
+
+ public static void validateAlpnSupported(SslProvider sslProvider) {
+ if (!isAlpnSupported(sslProvider)) {
+ throw new UnsupportedOperationException("ALPN is not supported in the current Java Version: " + getJavaVersion()
+ + "Use SslProvider.OPENSSL or ProtocolNegotiation.ASSUME_PROTOCOL.");
+ }
+ }
}
diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/BaseMockServer.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/BaseMockServer.java
new file mode 100644
index 000000000000..f7dff4a55e99
--- /dev/null
+++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/BaseMockServer.java
@@ -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.nio.netty;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.URI;
+
+public class BaseMockServer {
+
+ protected int httpPort;
+ protected int httpsPort;
+
+ public BaseMockServer() throws IOException {
+ httpPort = getUnusedPort();
+ httpsPort = getUnusedPort();
+ }
+
+ public URI getHttpUri() {
+ return URI.create(String.format("http://localhost:%s", httpPort));
+ }
+
+ public URI getHttpsUri() {
+ return URI.create(String.format("https://localhost:%s", httpsPort));
+ }
+
+ public static int getUnusedPort() throws IOException {
+ try (ServerSocket socket = new ServerSocket(0)) {
+ socket.setReuseAddress(true);
+ return socket.getLocalPort();
+ }
+ }
+}
diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/MockH2Server.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/MockH2Server.java
new file mode 100644
index 000000000000..4c34c3bc55de
--- /dev/null
+++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/MockH2Server.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.nio.netty;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http2.HTTP2Cipher;
+import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
+import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.SecureRequestCustomizer;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+
+/**
+ * Local h2 server used to stub fixed response.
+ *
+ * See:
+ * https://git.eclipse.org/c/jetty/org.eclipse.jetty.project
+ * .git/tree/examples/embedded/src/main/java/org/eclipse/jetty/embedded/Http2Server.java
+ */
+public class MockH2Server extends BaseMockServer {
+ private final Server server;
+
+ public MockH2Server(boolean usingAlpn) throws IOException {
+ server = new Server();
+ ServerConnector connector = new ServerConnector(server);
+ connector.setPort(httpPort);
+
+ // HTTP Configuration
+ HttpConfiguration httpConfiguration = new HttpConfiguration();
+ httpConfiguration.setSecureScheme("https");
+ httpConfiguration.setSecurePort(httpsPort);
+ httpConfiguration.setSendXPoweredBy(true);
+ httpConfiguration.setSendServerVersion(true);
+
+ // HTTP Connector
+ ServerConnector http = new ServerConnector(server,
+ new HttpConnectionFactory(httpConfiguration),
+ new HTTP2CServerConnectionFactory(httpConfiguration));
+ http.setPort(httpPort);
+ server.addConnector(http);
+
+
+ // HTTPS Configuration
+ HttpConfiguration https = new HttpConfiguration();
+ https.addCustomizer(new SecureRequestCustomizer());
+
+ // SSL Context Factory for HTTPS and HTTP/2
+ SslContextFactory sslContextFactory = new SslContextFactory();
+ sslContextFactory.setTrustAll(true);
+ sslContextFactory.setValidateCerts(false);
+ sslContextFactory.setNeedClientAuth(false);
+ sslContextFactory.setWantClientAuth(false);
+ sslContextFactory.setValidatePeerCerts(false);
+ sslContextFactory.setCipherComparator(HTTP2Cipher.COMPARATOR);
+ sslContextFactory.setKeyStorePassword("password");
+ sslContextFactory.setKeyStorePath("src/test/resources/software.amazon.awssdk.http.nio.netty/mock-keystore.jks");
+
+
+ // HTTP/2 Connection Factory
+ HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(https);
+
+ // SSL Connection Factory
+ SslConnectionFactory ssl;
+ ServerConnector http2Connector;
+
+ if (usingAlpn) {
+ ssl = new SslConnectionFactory(sslContextFactory, "alpn");
+ ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory("h2");
+ // HTTP/2 Connector
+ http2Connector = new ServerConnector(server, ssl, alpn, h2, new HttpConnectionFactory(https));
+ } else {
+ ssl = new SslConnectionFactory(sslContextFactory, "h2");
+ http2Connector = new ServerConnector(server, ssl, h2, new HttpConnectionFactory(https));
+ }
+
+ http2Connector.setPort(httpsPort);
+ server.addConnector(http2Connector);
+
+ ServletContextHandler context = new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS);
+ context.addServlet(new ServletHolder(new AlwaysSuccessServlet()), "/*");
+ server.setHandler(context);
+ }
+
+ public void start() throws Exception {
+ server.start();
+ }
+
+ public void stop() throws Exception {
+ server.stop();
+ }
+
+ static class AlwaysSuccessServlet extends HttpServlet {
+
+ public static final String JSON_BODY = "{\"StringMember\":\"foo\",\"IntegerMember\":123}";
+
+ @Override
+ public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
+ response.setStatus(HttpStatus.OK_200);
+ response.setContentType("application/json");
+ response.setContentLength(JSON_BODY.getBytes(StandardCharsets.UTF_8).length);
+ response.getOutputStream().print(JSON_BODY);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyClientAlpnTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyClientAlpnTest.java
new file mode 100644
index 000000000000..9ea125f6a195
--- /dev/null
+++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyClientAlpnTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.nio.netty;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static software.amazon.awssdk.http.SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES;
+import static software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClientTestUtils.createProvider;
+import static software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClientTestUtils.createRequest;
+
+import io.netty.handler.ssl.SslProvider;
+import java.net.URI;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIf;
+import software.amazon.awssdk.http.Protocol;
+import software.amazon.awssdk.http.ProtocolNegotiation;
+import software.amazon.awssdk.http.SdkHttpRequest;
+import software.amazon.awssdk.http.async.AsyncExecuteRequest;
+import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
+import software.amazon.awssdk.http.nio.netty.internal.utils.NettyUtils;
+import software.amazon.awssdk.utils.AttributeMap;
+
+public class NettyClientAlpnTest {
+
+ private static MockH2Server mockServer;
+ private static SdkAsyncHttpClient sdkHttpClient;
+
+ @AfterEach
+ public void reset() throws Exception {
+ sdkHttpClient.close();
+ mockServer.stop();
+ }
+
+ private static void initServer(boolean useAlpn) throws Exception {
+ mockServer = new MockH2Server(useAlpn);
+ mockServer.start();
+ }
+
+ @Test
+ @EnabledIf("alpnSupported")
+ public void alpnClientJdkProvider_serverWithAlpnSupport_requestSucceeds() throws Exception {
+ initClient(ProtocolNegotiation.ALPN, SslProvider.JDK);
+ initServer(true);
+ makeHttpsRequest();
+ }
+
+ @Test
+ public void alpnClientOpenSslProvider_serverWithAlpnSupport_requestSucceeds() throws Exception {
+ initClient(ProtocolNegotiation.ALPN, SslProvider.OPENSSL);
+ initServer(true);
+ makeHttpsRequest();
+ }
+
+ @Test
+ @EnabledIf("alpnSupported")
+ public void alpnClient_serverWithoutAlpnSupport_throwsException() throws Exception {
+ initClient(ProtocolNegotiation.ALPN, SslProvider.JDK);
+ initServer(false);
+ ExecutionException e = assertThrows(ExecutionException.class, this::makeHttpsRequest);
+ assertThat(e).hasCauseInstanceOf(UnsupportedOperationException.class);
+ assertThat(e.getMessage()).contains("The server does not support ALPN with H2");
+ }
+
+ @Test
+ @EnabledIf("alpnSupported")
+ public void priorKnowledgeClient_serverWithAlpnSupport_requestSucceeds() throws Exception {
+ initClient(ProtocolNegotiation.ASSUME_PROTOCOL, SslProvider.JDK);
+ initServer(true);
+ makeHttpsRequest();
+ }
+
+ @Test
+ public void priorKnowledgeClient_serverWithoutAlpnSupport_requestSucceeds() throws Exception {
+ initClient(ProtocolNegotiation.ASSUME_PROTOCOL, SslProvider.JDK);
+ initServer(false);
+ makeHttpsRequest();
+ }
+
+ @Test
+ @EnabledIf("alpnSupported")
+ public void alpnClient_httpRequest_throwsException() throws Exception {
+ initClient(ProtocolNegotiation.ALPN, SslProvider.JDK);
+ initServer(true);
+ UnsupportedOperationException e = assertThrows(UnsupportedOperationException.class, this::makeHttpRequest);
+ assertThat(e.getMessage()).isEqualTo("ALPN can only be used with HTTPS, not HTTP.");
+ }
+
+ private void makeHttpsRequest() throws Exception {
+ makeSimpleRequest(mockServer.getHttpsUri());
+ }
+
+ private void makeHttpRequest() throws Exception {
+ makeSimpleRequest(mockServer.getHttpUri());
+ }
+
+ private void makeSimpleRequest(URI uri) throws Exception {
+ SdkHttpRequest request = createRequest(uri);
+ RecordingResponseHandler recorder = new RecordingResponseHandler();
+ sdkHttpClient.execute(AsyncExecuteRequest.builder().request(request).requestContentPublisher(createProvider("")).responseHandler(recorder).build());
+ recorder.completeFuture.get(5, TimeUnit.SECONDS);
+ }
+
+ private static void initClient(ProtocolNegotiation protocolNegotiation, SslProvider sslProvider) {
+ sdkHttpClient = NettyNioAsyncHttpClient.builder()
+ .sslProvider(sslProvider)
+ .protocol(Protocol.HTTP2)
+ .protocolNegotiation(protocolNegotiation)
+ .buildWithDefaults(AttributeMap.builder().put(TRUST_ALL_CERTIFICATES, true)
+ .build());
+ }
+
+ private static boolean alpnSupported(){
+ return NettyUtils.isAlpnSupported(SslProvider.JDK);
+ }
+}
diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClientWireMockTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClientWireMockTest.java
index 2212e78ce4df..e6bf2585ad0f 100644
--- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClientWireMockTest.java
+++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClientWireMockTest.java
@@ -81,6 +81,7 @@
import org.mockito.stubbing.Answer;
import software.amazon.awssdk.http.HttpMetric;
import software.amazon.awssdk.http.HttpTestUtils;
+import software.amazon.awssdk.http.ProtocolNegotiation;
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
import software.amazon.awssdk.http.SdkHttpFullRequest;
import software.amazon.awssdk.http.SdkHttpMethod;
@@ -310,7 +311,7 @@ protected SdkChannelPool newPool(URI key) {
NettyConfiguration nettyConfiguration = new NettyConfiguration(AttributeMap.empty());
SdkAsyncHttpClient customerClient =
- new NettyNioAsyncHttpClient(eventLoopGroup, sdkChannelPoolMap, nettyConfiguration);
+ new NettyNioAsyncHttpClient(eventLoopGroup, sdkChannelPoolMap, nettyConfiguration, ProtocolNegotiation.ASSUME_PROTOCOL);
customerClient.close();
assertThat(eventLoopGroup.eventLoopGroup().isShuttingDown()).isTrue();
diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMapTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMapTest.java
index 236b88479827..a1d4b9781f35 100644
--- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMapTest.java
+++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMapTest.java
@@ -43,6 +43,7 @@
import org.junit.Test;
import org.mockito.Mockito;
import software.amazon.awssdk.http.Protocol;
+import software.amazon.awssdk.http.ProtocolNegotiation;
import software.amazon.awssdk.http.TlsKeyManagersProvider;
import software.amazon.awssdk.http.nio.netty.ProxyConfiguration;
import software.amazon.awssdk.http.nio.netty.RecordingNetworkTrafficListener;
@@ -275,6 +276,7 @@ public void acquireChannel_autoReadDisabled() {
.sdkEventLoopGroup(SdkEventLoopGroup.builder().build())
.configuration(new NettyConfiguration(GLOBAL_HTTP_DEFAULTS))
.protocol(Protocol.HTTP1_1)
+ .protocolNegotiation(ProtocolNegotiation.ASSUME_PROTOCOL)
.maxStreams(100)
.sslProvider(SslProvider.OPENSSL)
.build();
@@ -293,6 +295,7 @@ public void releaseChannel_autoReadEnabled() {
.sdkEventLoopGroup(SdkEventLoopGroup.builder().build())
.configuration(new NettyConfiguration(GLOBAL_HTTP_DEFAULTS))
.protocol(Protocol.HTTP1_1)
+ .protocolNegotiation(ProtocolNegotiation.ASSUME_PROTOCOL)
.maxStreams(100)
.sslProvider(SslProvider.OPENSSL)
.build();
diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ChannelPipelineInitializerTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ChannelPipelineInitializerTest.java
index aa5f60329b3d..4b1aeb85073a 100644
--- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ChannelPipelineInitializerTest.java
+++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ChannelPipelineInitializerTest.java
@@ -17,59 +17,126 @@
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
import static software.amazon.awssdk.http.SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS;
import io.netty.buffer.UnpooledByteBufAllocator;
import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOption;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.channel.pool.ChannelPool;
-import io.netty.handler.codec.http2.Http2SecurityUtil;
-import io.netty.handler.ssl.SslContext;
-import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.codec.http2.Http2MultiplexHandler;
+import io.netty.handler.ssl.ApplicationProtocolNames;
+import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
+import io.netty.handler.ssl.SslHandler;
+import io.netty.handler.ssl.SslHandshakeCompletionEvent;
import io.netty.handler.ssl.SslProvider;
-import io.netty.handler.ssl.SupportedCipherSuiteFilter;
import java.net.URI;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicReference;
-import javax.net.ssl.SSLException;
+
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIf;
import software.amazon.awssdk.http.Protocol;
+import software.amazon.awssdk.http.ProtocolNegotiation;
+import software.amazon.awssdk.http.nio.netty.internal.utils.NettyUtils;
public class ChannelPipelineInitializerTest {
- private ChannelPipelineInitializer pipelineInitializer;
+ private final URI TARGET_URI = URI.create("https://some-awesome-service-1234.amazonaws.com:8080");
+ private static final SslProvider SSL_PROVIDER = SslProvider.JDK;
- private URI targetUri;
+ @Test
+ public void channelConfigOptionCheck() {
+ ChannelPipelineInitializer pipelineInitializer = createChannelPipelineInitializer(Protocol.HTTP1_1, ProtocolNegotiation.ASSUME_PROTOCOL);
+ Channel channel = new EmbeddedChannel();
+ pipelineInitializer.channelCreated(channel);
+
+ assertThat(channel.config().getOption(ChannelOption.ALLOCATOR), is(UnpooledByteBufAllocator.DEFAULT));
+ }
@Test
- public void channelConfigOptionCheck() throws SSLException {
- targetUri = URI.create("https://some-awesome-service-1234.amazonaws.com:8080");
+ @EnabledIf("alpnSupported")
+ public void h2AlpnEnabled_shouldUseAlpn() {
+ ChannelPipelineInitializer pipelineInitializer = createChannelPipelineInitializer(Protocol.HTTP2, ProtocolNegotiation.ALPN);
+ Channel channel = new EmbeddedChannel();
+ pipelineInitializer.channelCreated(channel);
- SslContext sslContext = SslContextBuilder.forClient()
- .sslProvider(SslProvider.JDK)
- .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
- .build();
+ assertNotNull(channel.pipeline().get(ApplicationProtocolNegotiationHandler.class));
+ }
- AtomicReference channelPoolRef = new AtomicReference<>();
+ @Test
+ @EnabledIf("alpnSupported")
+ public void h2AlpnEnabled_serverSupportsAlpn_shouldCreateH2Handler() throws Exception {
+ ChannelPipelineInitializer pipelineInitializer = createChannelPipelineInitializer(Protocol.HTTP2, ProtocolNegotiation.ALPN);
+ Channel channel = new EmbeddedChannel();
+ pipelineInitializer.channelCreated(channel);
- NettyConfiguration nettyConfiguration = new NettyConfiguration(GLOBAL_HTTP_DEFAULTS);
+ simulateServerAlpnSuccess(channel, ApplicationProtocolNames.HTTP_2);
- pipelineInitializer = new ChannelPipelineInitializer(Protocol.HTTP1_1,
- sslContext,
- SslProvider.JDK,
- 100,
- 1024,
- Duration.ZERO,
- channelPoolRef,
- nettyConfiguration,
- targetUri);
+ assertNotNull(channel.pipeline().get(Http2MultiplexHandler.class));
+ }
+ @Test
+ @EnabledIf("alpnSupported")
+ public void h2AlpnEnabled_serverDoesNotSupportAlpn_shouldNotFallbackToH2() throws Exception {
+ ChannelPipelineInitializer pipelineInitializer = createChannelPipelineInitializer(Protocol.HTTP2, ProtocolNegotiation.ALPN);
Channel channel = new EmbeddedChannel();
-
pipelineInitializer.channelCreated(channel);
- assertThat(channel.config().getOption(ChannelOption.ALLOCATOR), is(UnpooledByteBufAllocator.DEFAULT));
+ simulateServerAlpnUnsupported(channel);
+
+ assertNull(channel.pipeline().get(Http2MultiplexHandler.class));
+ }
+
+ private void simulateServerAlpnSuccess(Channel channel, String protocol) throws Exception {
+ SslHandler mockSslHandler = mock(SslHandler.class);
+ when(mockSslHandler.applicationProtocol()).thenReturn(protocol);
+ channel.pipeline().replace(SslHandler.class, "MockSslHandler", mockSslHandler);
+
+ assertNotNull(channel.pipeline().get(ApplicationProtocolNegotiationHandler.class));
+
+ ChannelHandlerContext ctx = channel.pipeline().context(ApplicationProtocolNegotiationHandler.class);
+ channel.pipeline().get(ApplicationProtocolNegotiationHandler.class).userEventTriggered(ctx, SslHandshakeCompletionEvent.SUCCESS);
+ }
+
+ private void simulateServerAlpnUnsupported(Channel channel) throws Exception {
+ SslHandler mockSslHandler = mock(SslHandler.class);
+ when(mockSslHandler.applicationProtocol()).thenReturn(null);
+ channel.pipeline().replace(SslHandler.class, "MockSslHandler", mockSslHandler);
+
+ assertNotNull(channel.pipeline().get(ApplicationProtocolNegotiationHandler.class));
+
+ ChannelHandlerContext ctx = channel.pipeline().context(ApplicationProtocolNegotiationHandler.class);
+ channel.pipeline().get(ApplicationProtocolNegotiationHandler.class).userEventTriggered(ctx, SslHandshakeCompletionEvent.SUCCESS);
+ }
+
+ private ChannelPipelineInitializer createChannelPipelineInitializer(Protocol protocol, ProtocolNegotiation protocolNegotiation) {
+ AtomicReference channelPoolRef = new AtomicReference<>();
+ NettyConfiguration nettyConfiguration = new NettyConfiguration(GLOBAL_HTTP_DEFAULTS);
+ SslContextProvider sslContextProvider = new SslContextProvider(nettyConfiguration,
+ protocol,
+ protocolNegotiation,
+ SSL_PROVIDER);
+
+ return new ChannelPipelineInitializer(protocol,
+ protocolNegotiation,
+ sslContextProvider.sslContext(),
+ SSL_PROVIDER,
+ 100,
+ 1024,
+ Duration.ZERO,
+ channelPoolRef,
+ nettyConfiguration,
+ TARGET_URI);
+
+ }
+ private static boolean alpnSupported(){
+ return NettyUtils.isAlpnSupported(SSL_PROVIDER);
}
}
\ No newline at end of file
diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/SslContextProviderTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/SslContextProviderTest.java
index ee279eabdb07..b8e05419b4b1 100644
--- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/SslContextProviderTest.java
+++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/SslContextProviderTest.java
@@ -22,14 +22,18 @@
import static software.amazon.awssdk.http.SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES;
import io.netty.handler.codec.http2.Http2SecurityUtil;
+import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.SslProvider;
import javax.net.ssl.TrustManager;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIf;
import org.mockito.Mockito;
import software.amazon.awssdk.http.Protocol;
+import software.amazon.awssdk.http.ProtocolNegotiation;
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
import software.amazon.awssdk.http.TlsKeyManagersProvider;
import software.amazon.awssdk.http.TlsTrustManagersProvider;
+import software.amazon.awssdk.http.nio.netty.internal.utils.NettyUtils;
import software.amazon.awssdk.utils.AttributeMap;
public class SslContextProviderTest {
@@ -38,6 +42,7 @@ public class SslContextProviderTest {
public void sslContext_h2WithJdk_h2CiphersShouldBeUsed() {
SslContextProvider sslContextProvider = new SslContextProvider(new NettyConfiguration(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS),
Protocol.HTTP2,
+ ProtocolNegotiation.ASSUME_PROTOCOL,
SslProvider.JDK);
assertThat(sslContextProvider.sslContext().cipherSuites()).isSubsetOf(Http2SecurityUtil.CIPHERS);
@@ -47,6 +52,7 @@ public void sslContext_h2WithJdk_h2CiphersShouldBeUsed() {
public void sslContext_h2WithOpenSsl_h2CiphersShouldBeUsed() {
SslContextProvider sslContextProvider = new SslContextProvider(new NettyConfiguration(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS),
Protocol.HTTP2,
+ ProtocolNegotiation.ASSUME_PROTOCOL,
SslProvider.OPENSSL);
assertThat(sslContextProvider.sslContext().cipherSuites()).isSubsetOf(Http2SecurityUtil.CIPHERS);
@@ -56,6 +62,7 @@ public void sslContext_h2WithOpenSsl_h2CiphersShouldBeUsed() {
public void sslContext_h1_defaultCipherShouldBeUsed() {
SslContextProvider sslContextProvider = new SslContextProvider(new NettyConfiguration(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS),
Protocol.HTTP1_1,
+ ProtocolNegotiation.ASSUME_PROTOCOL,
SslProvider.JDK);
assertThat(sslContextProvider.sslContext().cipherSuites()).isNotIn(Http2SecurityUtil.CIPHERS);
@@ -69,6 +76,7 @@ public void customizedKeyManagerPresent_shouldUseCustomized() {
.put(TLS_KEY_MANAGERS_PROVIDER, mockProvider)
.build()),
Protocol.HTTP1_1,
+ ProtocolNegotiation.ASSUME_PROTOCOL,
SslProvider.JDK);
sslContextProvider.sslContext();
@@ -85,6 +93,7 @@ public void customizedTrustManagerPresent_shouldUseCustomized() {
.put(TLS_TRUST_MANAGERS_PROVIDER, mockProvider)
.build()),
Protocol.HTTP1_1,
+ ProtocolNegotiation.ASSUME_PROTOCOL,
SslProvider.JDK);
sslContextProvider.sslContext();
@@ -100,6 +109,7 @@ public void TlsTrustManagerAndTrustAllCertificates_shouldThrowException() {
mockProvider)
.build()),
Protocol.HTTP1_1,
+ ProtocolNegotiation.ASSUME_PROTOCOL,
SslProvider.JDK)).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("A TlsTrustManagerProvider can't"
+ " be provided if "
@@ -107,4 +117,33 @@ public void TlsTrustManagerAndTrustAllCertificates_shouldThrowException() {
+ "set");
}
-}
+
+ @Test
+ @EnabledIf("alpnSupported")
+ public void protocolH2AlpnEnabled_jdkProvider_shouldUseAlpn() {
+ SslContextProvider sslContextProvider = new SslContextProvider(new NettyConfiguration(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS),
+ Protocol.HTTP2,
+ ProtocolNegotiation.ALPN,
+ SslProvider.JDK);
+
+ assertThat(sslContextProvider.sslContext().applicationProtocolNegotiator()).isNotNull();
+ assertThat(sslContextProvider.sslContext().applicationProtocolNegotiator().protocols()).contains(ApplicationProtocolNames.HTTP_2);
+ assertThat(sslContextProvider.sslContext().applicationProtocolNegotiator().protocols()).doesNotContain(ApplicationProtocolNames.HTTP_1_1);
+ }
+
+ @Test
+ public void protocolH2AlpnEnabled_openSslProvider_shouldUseAlpn() {
+ SslContextProvider sslContextProvider = new SslContextProvider(new NettyConfiguration(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS),
+ Protocol.HTTP2,
+ ProtocolNegotiation.ALPN,
+ SslProvider.OPENSSL);
+
+ assertThat(sslContextProvider.sslContext().applicationProtocolNegotiator()).isNotNull();
+ assertThat(sslContextProvider.sslContext().applicationProtocolNegotiator().protocols()).contains(ApplicationProtocolNames.HTTP_2);
+ assertThat(sslContextProvider.sslContext().applicationProtocolNegotiator().protocols()).doesNotContain(ApplicationProtocolNames.HTTP_1_1);
+ }
+
+ private static boolean alpnSupported(){
+ return NettyUtils.isAlpnSupported(SslProvider.JDK);
+ }
+}
\ No newline at end of file
diff --git a/http-clients/netty-nio-client/src/test/resources/software.amazon.awssdk.http.nio.netty/mock-keystore.jks b/http-clients/netty-nio-client/src/test/resources/software.amazon.awssdk.http.nio.netty/mock-keystore.jks
new file mode 100644
index 000000000000..8570460ae70d
Binary files /dev/null and b/http-clients/netty-nio-client/src/test/resources/software.amazon.awssdk.http.nio.netty/mock-keystore.jks differ
diff --git a/services/kinesis/src/it/java/software/amazon/awssdk/services/kinesis/AbstractTestCase.java b/services/kinesis/src/it/java/software/amazon/awssdk/services/kinesis/AbstractTestCase.java
index 85a23f5ab49b..854352cdf2e7 100644
--- a/services/kinesis/src/it/java/software/amazon/awssdk/services/kinesis/AbstractTestCase.java
+++ b/services/kinesis/src/it/java/software/amazon/awssdk/services/kinesis/AbstractTestCase.java
@@ -20,28 +20,48 @@
import java.io.IOException;
import java.net.URI;
import java.util.Properties;
-import org.junit.BeforeClass;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
import software.amazon.awssdk.awscore.util.AwsHostNameUtils;
+import software.amazon.awssdk.http.Protocol;
+import software.amazon.awssdk.http.ProtocolNegotiation;
+import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.testutils.service.AwsTestBase;
public class AbstractTestCase extends AwsTestBase {
protected static KinesisClient client;
protected static KinesisAsyncClient asyncClient;
+ protected static KinesisAsyncClient asyncClientAlpn;
- @BeforeClass
+ @BeforeAll
public static void init() throws IOException {
setUpCredentials();
KinesisClientBuilder builder = KinesisClient.builder().credentialsProvider(CREDENTIALS_PROVIDER_CHAIN);
setEndpoint(builder);
client = builder.build();
- asyncClient = KinesisAsyncClient.builder().credentialsProvider(CREDENTIALS_PROVIDER_CHAIN).build();
+ asyncClient = KinesisAsyncClient.builder()
+ .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN)
+ .build();
+ asyncClientAlpn = KinesisAsyncClient.builder()
+ .httpClient(NettyNioAsyncHttpClient.builder()
+ .protocol(Protocol.HTTP2)
+ .protocolNegotiation(ProtocolNegotiation.ALPN)
+ .build())
+ .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN)
+ .build();
+ }
+
+ @AfterAll
+ public static void cleanUp() {
+ client.close();
+ asyncClient.close();
}
private static void setEndpoint(KinesisClientBuilder builder) throws IOException {
File endpointOverrides = new File(
- new File(System.getProperty("user.home")),
- ".aws/awsEndpointOverrides.properties"
+ new File(System.getProperty("user.home")),
+ ".aws/awsEndpointOverrides.properties"
);
if (endpointOverrides.exists()) {
diff --git a/services/kinesis/src/it/java/software/amazon/awssdk/services/kinesis/KinesisIntegrationTests.java b/services/kinesis/src/it/java/software/amazon/awssdk/services/kinesis/KinesisIntegrationTests.java
index 6f9d4bf4ac2a..c2d65bee16a8 100644
--- a/services/kinesis/src/it/java/software/amazon/awssdk/services/kinesis/KinesisIntegrationTests.java
+++ b/services/kinesis/src/it/java/software/amazon/awssdk/services/kinesis/KinesisIntegrationTests.java
@@ -15,16 +15,14 @@
package software.amazon.awssdk.services.kinesis;
-import static org.junit.Assert.assertThat;
-import static org.junit.Assert.assertTrue;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
import java.math.BigInteger;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
-import org.hamcrest.Matchers;
-import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.core.exception.SdkServiceException;
import software.amazon.awssdk.services.kinesis.model.CreateStreamRequest;
@@ -53,7 +51,7 @@ public class KinesisIntegrationTests extends AbstractTestCase {
public void testDescribeBogusStream() {
try {
client.describeStream(DescribeStreamRequest.builder().streamName("bogus-stream-name").build());
- Assert.fail("Expected ResourceNotFoundException");
+ fail("Expected ResourceNotFoundException");
} catch (ResourceNotFoundException exception) {
// Ignored or expected.
}
@@ -63,7 +61,7 @@ public void testDescribeBogusStream() {
public void testDeleteBogusStream() {
try {
client.deleteStream(DeleteStreamRequest.builder().streamName("bogus-stream-name").build());
- Assert.fail("Expected ResourceNotFoundException");
+ fail("Expected ResourceNotFoundException");
} catch (ResourceNotFoundException exception) {
// Ignored or expected.
}
@@ -77,7 +75,7 @@ public void testGetIteratorForBogusStream() {
.shardId("bogus-shard-id")
.shardIteratorType(ShardIteratorType.LATEST)
.build());
- Assert.fail("Expected ResourceNotFoundException");
+ fail("Expected ResourceNotFoundException");
} catch (ResourceNotFoundException exception) {
// Ignored or expected.
}
@@ -87,7 +85,7 @@ public void testGetIteratorForBogusStream() {
public void testGetFromNullIterator() {
try {
client.getRecords(GetRecordsRequest.builder().build());
- Assert.fail("Expected InvalidArgumentException");
+ fail("Expected InvalidArgumentException");
} catch (SdkServiceException exception) {
// Ignored or expected.
}
@@ -97,7 +95,7 @@ public void testGetFromNullIterator() {
public void testGetFromBogusIterator() {
try {
client.getRecords(GetRecordsRequest.builder().shardIterator("bogusmonkeys").build());
- Assert.fail("Expected InvalidArgumentException");
+ fail("Expected InvalidArgumentException");
} catch (InvalidArgumentException exception) {
// Ignored or expected.
}
@@ -118,7 +116,7 @@ public void testCreatePutGetDelete() throws Exception {
List shards = waitForStream(streamName);
Thread.sleep(1000);
- Assert.assertEquals(1, shards.size());
+ assertThat(shards.size()).isEqualTo(1);
Shard shard = shards.get(0);
putRecord(streamName, "See No Evil");
@@ -133,20 +131,20 @@ public void testCreatePutGetDelete() throws Exception {
}
}
- private void testGets(final String streamName, final Shard shard) throws InterruptedException {
+ private void testGets(String streamName, Shard shard) {
// Wait for the shard to be in an active state
// Get an iterator for the first shard.
GetShardIteratorResponse iteratorResult = client.getShardIterator(
- GetShardIteratorRequest.builder()
- .streamName(streamName)
- .shardId(shard.shardId())
- .shardIteratorType(ShardIteratorType.AT_SEQUENCE_NUMBER)
- .startingSequenceNumber(shard.sequenceNumberRange().startingSequenceNumber())
- .build());
- Assert.assertNotNull(iteratorResult);
+ GetShardIteratorRequest.builder()
+ .streamName(streamName)
+ .shardId(shard.shardId())
+ .shardIteratorType(ShardIteratorType.AT_SEQUENCE_NUMBER)
+ .startingSequenceNumber(shard.sequenceNumberRange().startingSequenceNumber())
+ .build());
+ assertThat(iteratorResult).isNotNull();
String iterator = iteratorResult.shardIterator();
- Assert.assertNotNull(iterator);
+ assertThat(iterator).isNotNull();
GetRecordsResponse result = getOneRecord(iterator);
validateRecord(result.records().get(0), "See No Evil");
@@ -157,7 +155,7 @@ private void testGets(final String streamName, final Shard shard) throws Interru
result = client.getRecords(GetRecordsRequest.builder()
.shardIterator(result.nextShardIterator())
.build());
- assertTrue(result.records().isEmpty());
+ assertThat(result.records()).isEmpty();
}
private GetRecordsResponse getOneRecord(String iterator) {
@@ -170,23 +168,23 @@ private GetRecordsResponse getOneRecord(String iterator) {
while (true) {
tries += 1;
if (tries > 100) {
- Assert.fail("Failed to read any records after 100 seconds");
+ fail("Failed to read any records after 100 seconds");
}
result = client.getRecords(GetRecordsRequest.builder()
.shardIterator(iterator)
.limit(1)
.build());
- Assert.assertNotNull(result);
- Assert.assertNotNull(result.records());
- Assert.assertNotNull(result.nextShardIterator());
+ assertThat(result).isNotNull();
+ assertThat(result.records()).isNotNull();
+ assertThat(result.nextShardIterator()).isNotNull();
records = result.records();
if (records.size() > 0) {
long arrivalTime = records.get(0).approximateArrivalTimestamp().toEpochMilli();
Long delta = Math.abs(Instant.now().minusMillis(arrivalTime).toEpochMilli());
// Assert that the arrival date is within 5 minutes of the current date to make sure it unmarshalled correctly.
- assertThat(delta, Matchers.lessThan(60 * 5000L));
+ assertThat(delta).isLessThan(60 * 5000L);
break;
}
@@ -201,58 +199,54 @@ private GetRecordsResponse getOneRecord(String iterator) {
return result;
}
- private void validateRecord(final Record record, String data) {
- Assert.assertNotNull(record);
+ private void validateRecord(Record record, String data) {
+ assertThat(record).isNotNull();
+ assertThat(record.sequenceNumber()).isNotNull();
- Assert.assertNotNull(record.sequenceNumber());
new BigInteger(record.sequenceNumber());
String value = record.data() == null ? null : record.data().asUtf8String();
- Assert.assertEquals(data, value);
+ assertThat(value).isEqualTo(data);
- Assert.assertNotNull(record.partitionKey());
+ assertThat(record.partitionKey()).isNotNull();
// The timestamp should be relatively recent
- Assert.assertTrue(Duration.between(record.approximateArrivalTimestamp(), Instant.now()).toMinutes() < 5);
+ assertThat(Duration.between(record.approximateArrivalTimestamp(), Instant.now()).toMinutes()).isLessThan(5);
}
- private PutRecordResponse putRecord(final String streamName,
- final String data) {
+ private PutRecordResponse putRecord(String streamName, String data) {
PutRecordResponse result = client.putRecord(
- PutRecordRequest.builder()
- .streamName(streamName)
- .partitionKey("foobar")
- .data(SdkBytes.fromUtf8String(data))
- .build());
- Assert.assertNotNull(result);
-
- Assert.assertNotNull(result.shardId());
- Assert.assertNotNull(result.sequenceNumber());
+ PutRecordRequest.builder()
+ .streamName(streamName)
+ .partitionKey("foobar")
+ .data(SdkBytes.fromUtf8String(data))
+ .build());
+ assertThat(result).isNotNull();
+ assertThat(result.shardId()).isNotNull();
+ assertThat(result.sequenceNumber()).isNotNull();
return result;
}
- private List waitForStream(final String streamName)
- throws InterruptedException {
+ private List waitForStream(String streamName) throws InterruptedException {
while (true) {
DescribeStreamResponse result = client.describeStream(DescribeStreamRequest.builder().streamName(streamName).build());
- Assert.assertNotNull(result);
+ assertThat(result).isNotNull();
StreamDescription description = result.streamDescription();
- Assert.assertNotNull(description);
-
- Assert.assertEquals(streamName, description.streamName());
- Assert.assertNotNull(description.streamARN());
- Assert.assertFalse(description.hasMoreShards());
+ assertThat(description).isNotNull();
+ assertThat(description.streamName()).isEqualTo(streamName);
+ assertThat(description.streamARN()).isNotNull();
+ assertThat(description.hasMoreShards()).isFalse();
StreamStatus status = description.streamStatus();
- Assert.assertNotNull(status);
+ assertThat(status).isNotNull();
if (status == StreamStatus.ACTIVE) {
List shards = description.shards();
@@ -264,49 +258,44 @@ private List waitForStream(final String streamName)
if (!(status == StreamStatus.CREATING
|| status == StreamStatus.UPDATING)) {
- Assert.fail("Unexpected status '" + status + "'");
+ fail("Unexpected status '" + status + "'");
}
Thread.sleep(1000);
}
}
- private void validateShards(final List shards) {
- Assert.assertNotNull(shards);
- Assert.assertFalse(shards.isEmpty());
+ private void validateShards(List shards) {
+ assertThat(shards).isNotNull().isNotEmpty();
for (Shard shard : shards) {
- Assert.assertNotNull(shard);
- Assert.assertNotNull(shard.shardId());
+ assertThat(shard).isNotNull();
+ assertThat(shard.shardId()).isNotNull();
validateHashKeyRange(shard.hashKeyRange());
validateSQNRange(shard.sequenceNumberRange());
}
-
}
- private void validateHashKeyRange(final HashKeyRange range) {
-
- Assert.assertNotNull(range);
- Assert.assertNotNull(range.startingHashKey());
- Assert.assertNotNull(range.endingHashKey());
+ private void validateHashKeyRange(HashKeyRange range) {
+ assertThat(range).isNotNull();
+ assertThat(range.startingHashKey()).isNotNull();
+ assertThat(range.endingHashKey()).isNotNull();
BigInteger start = new BigInteger(range.startingHashKey());
BigInteger end = new BigInteger(range.endingHashKey());
- Assert.assertTrue(start.compareTo(end) <= 0);
+ assertThat(start).isLessThanOrEqualTo(end);
}
- private void validateSQNRange(final SequenceNumberRange range) {
- Assert.assertNotNull(range);
- Assert.assertNotNull(range.startingSequenceNumber());
+ private void validateSQNRange(SequenceNumberRange range) {
+ assertThat(range).isNotNull();
+ assertThat(range.startingSequenceNumber()).isNotNull();
BigInteger start = new BigInteger(range.startingSequenceNumber());
if (range.endingSequenceNumber() != null) {
BigInteger end = new BigInteger(range.endingSequenceNumber());
-
- Assert.assertTrue(start.compareTo(end) <= 0);
+ assertThat(start).isLessThanOrEqualTo(end);
}
}
-
}
diff --git a/services/kinesis/src/it/java/software/amazon/awssdk/services/kinesis/KinesisResponseMetadataIntegrationTest.java b/services/kinesis/src/it/java/software/amazon/awssdk/services/kinesis/KinesisResponseMetadataIntegrationTest.java
index 7f835f6980eb..68aa8d999f07 100644
--- a/services/kinesis/src/it/java/software/amazon/awssdk/services/kinesis/KinesisResponseMetadataIntegrationTest.java
+++ b/services/kinesis/src/it/java/software/amazon/awssdk/services/kinesis/KinesisResponseMetadataIntegrationTest.java
@@ -17,7 +17,7 @@
import static org.assertj.core.api.Assertions.assertThat;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import software.amazon.awssdk.services.kinesis.model.DescribeLimitsResponse;
import software.amazon.awssdk.services.kinesis.model.KinesisResponse;
@@ -35,6 +35,12 @@ public void async_shouldContainResponseMetadata() {
verifyResponseMetadata(response);
}
+ @Test
+ public void asyncAlpn_shouldContainResponseMetadata() {
+ DescribeLimitsResponse response = asyncClientAlpn.describeLimits().join();
+ verifyResponseMetadata(response);
+ }
+
private void verifyResponseMetadata(KinesisResponse response) {
assertThat(response.responseMetadata().requestId()).isNotEqualTo("UNKNOWN");
assertThat(response.responseMetadata().extendedRequestId()).isNotEqualTo("UNKNOWN");
diff --git a/services/kinesis/src/it/java/software/amazon/awssdk/services/kinesis/SubscribeToShardIntegrationTest.java b/services/kinesis/src/it/java/software/amazon/awssdk/services/kinesis/SubscribeToShardIntegrationTest.java
index d3171bf3744d..f0775794f588 100644
--- a/services/kinesis/src/it/java/software/amazon/awssdk/services/kinesis/SubscribeToShardIntegrationTest.java
+++ b/services/kinesis/src/it/java/software/amazon/awssdk/services/kinesis/SubscribeToShardIntegrationTest.java
@@ -30,9 +30,9 @@
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.apache.commons.lang3.RandomUtils;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import software.amazon.awssdk.core.SdkBytes;
@@ -54,13 +54,13 @@
public class SubscribeToShardIntegrationTest extends AbstractTestCase {
public static final int WAIT_TIME_FOR_SUBSCRIPTION_COMPLETION = 300;
- private String streamName;
+ private static String streamName;
private static final String CONSUMER_NAME = "subscribe-to-shard-consumer";
private static String consumerArn;
private static String shardId;
- @Before
- public void setup() throws InterruptedException {
+ @BeforeAll
+ public static void setup() throws InterruptedException {
streamName = "subscribe-to-shard-integ-test-" + System.currentTimeMillis();
asyncClient.createStream(r -> r.streamName(streamName)
@@ -70,18 +70,18 @@ public void setup() throws InterruptedException {
.streamDescription()
.streamARN();
- this.shardId = asyncClient.listShards(r -> r.streamName(streamName))
- .join()
- .shards().get(0).shardId();
- this.consumerArn = asyncClient.registerStreamConsumer(r -> r.streamARN(streamARN)
- .consumerName(CONSUMER_NAME)).join()
- .consumer()
- .consumerARN();
+ shardId = asyncClient.listShards(r -> r.streamName(streamName))
+ .join()
+ .shards().get(0).shardId();
+ consumerArn = asyncClient.registerStreamConsumer(r -> r.streamARN(streamARN)
+ .consumerName(CONSUMER_NAME)).join()
+ .consumer()
+ .consumerARN();
waitForConsumerToBeActive();
}
- @After
- public void tearDown() {
+ @AfterAll
+ public static void tearDown() {
asyncClient.deleteStream(r -> r.streamName(streamName)
.enforceConsumerDeletion(true)).join();
}
@@ -96,22 +96,22 @@ public void subscribeToShard_smallWindow_doesNotTimeOutReads() {
}
KinesisAsyncClient smallWindowAsyncClient = KinesisAsyncClient.builder()
- .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN)
- .httpClientBuilder(NettyNioAsyncHttpClient.builder()
- .http2Configuration(Http2Configuration.builder()
- .initialWindowSize(16384)
- .build()))
- .build();
+ .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN)
+ .httpClientBuilder(NettyNioAsyncHttpClient.builder()
+ .http2Configuration(Http2Configuration.builder()
+ .initialWindowSize(16384)
+ .build()))
+ .build();
try {
smallWindowAsyncClient.subscribeToShard(r -> r.consumerARN(consumerArn)
- .shardId(shardId)
- .startingPosition(s -> s.type(ShardIteratorType.TRIM_HORIZON)),
- SubscribeToShardResponseHandler.builder()
- .onEventStream(es -> Flowable.fromPublisher(es).forEach(e -> {}))
- .onResponse(this::verifyHttpMetadata)
- .build())
- .join();
+ .shardId(shardId)
+ .startingPosition(s -> s.type(ShardIteratorType.TRIM_HORIZON)),
+ SubscribeToShardResponseHandler.builder()
+ .onEventStream(es -> Flowable.fromPublisher(es).forEach(e -> {}))
+ .onResponse(this::verifyHttpMetadata)
+ .build())
+ .join();
} finally {
smallWindowAsyncClient.close();
@@ -153,51 +153,51 @@ public void limitedSubscription_callCompleteMethodOfSubs_whenLimitsReached() {
AtomicBoolean errorOccurred = new AtomicBoolean(false);
List events = new ArrayList<>();
asyncClient.subscribeToShard(r -> r.consumerARN(consumerArn)
- .shardId(shardId)
- .startingPosition(s -> s.type(ShardIteratorType.LATEST)),
- new SubscribeToShardResponseHandler() {
- @Override
- public void responseReceived(SubscribeToShardResponse response) {
- verifyHttpMetadata(response);
- }
-
- @Override
- public void onEventStream(SdkPublisher publisher) {
- publisher.limit(3).subscribe(new Subscriber() {
- private Subscription subscription;
- @Override
- public void onSubscribe(Subscription subscription) {
- this.subscription = subscription;
- subscription.request(10);
- }
-
- @Override
- public void onNext(SubscribeToShardEventStream subscribeToShardEventStream) {
- events.add(subscribeToShardEventStream);
- }
-
- @Override
- public void onError(Throwable throwable) {
- errorOccurred.set(true);
- }
-
- @Override
- public void onComplete() {
- onCompleteSubsMethodsCalled.set(true);
- }
- });
- }
-
- @Override
- public void exceptionOccurred(Throwable throwable) {
- errorOccurred.set(true);
- }
-
- @Override
- public void complete() {
- completeMethodOfHandlerCalled.set(true);
- }
- }).join();
+ .shardId(shardId)
+ .startingPosition(s -> s.type(ShardIteratorType.LATEST)),
+ new SubscribeToShardResponseHandler() {
+ @Override
+ public void responseReceived(SubscribeToShardResponse response) {
+ verifyHttpMetadata(response);
+ }
+
+ @Override
+ public void onEventStream(SdkPublisher publisher) {
+ publisher.limit(3).subscribe(new Subscriber() {
+ private Subscription subscription;
+ @Override
+ public void onSubscribe(Subscription subscription) {
+ this.subscription = subscription;
+ subscription.request(10);
+ }
+
+ @Override
+ public void onNext(SubscribeToShardEventStream subscribeToShardEventStream) {
+ events.add(subscribeToShardEventStream);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ errorOccurred.set(true);
+ }
+
+ @Override
+ public void onComplete() {
+ onCompleteSubsMethodsCalled.set(true);
+ }
+ });
+ }
+
+ @Override
+ public void exceptionOccurred(Throwable throwable) {
+ errorOccurred.set(true);
+ }
+
+ @Override
+ public void complete() {
+ completeMethodOfHandlerCalled.set(true);
+ }
+ }).join();
try {
Thread.sleep(WAIT_TIME_FOR_SUBSCRIPTION_COMPLETION);
@@ -279,13 +279,14 @@ public void complete() {
assertThat(events.size()).isEqualTo(1);
}
+
private static void waitForConsumerToBeActive() {
Waiter.run(() -> asyncClient.describeStreamConsumer(r -> r.consumerARN(consumerArn)).join())
.until(b -> b.consumerDescription().consumerStatus().equals(ConsumerStatus.ACTIVE))
.orFailAfter(Duration.ofMinutes(5));
}
- private void waitForStreamToBeActive() {
+ private static void waitForStreamToBeActive() {
Waiter.run(() -> asyncClient.describeStream(r -> r.streamName(streamName)).join())
.until(b -> b.streamDescription().streamStatus().equals(StreamStatus.ACTIVE))
.orFailAfter(Duration.ofMinutes(5));
diff --git a/services/kinesis/src/main/resources/codegen-resources/customization.config b/services/kinesis/src/main/resources/codegen-resources/customization.config
index 44a50c7cc7c9..93d23c80b74b 100644
--- a/services/kinesis/src/main/resources/codegen-resources/customization.config
+++ b/services/kinesis/src/main/resources/codegen-resources/customization.config
@@ -52,5 +52,6 @@
"Invalid ARN: Kinesis ARNs only support stream arn types": "Test is broken for client tests, need operationInputs.",
"RegionMismatch: client region should be used for endpoint region": "Test is broken for client tests, need operationInputs."
},
+ "usePriorKnowledgeForH2": true,
"enableFastUnmarshaller": false
}
diff --git a/services/lexruntimev2/src/main/resources/codegen-resources/customization.config b/services/lexruntimev2/src/main/resources/codegen-resources/customization.config
index 73f3bd4a1db7..36be8b0e4981 100644
--- a/services/lexruntimev2/src/main/resources/codegen-resources/customization.config
+++ b/services/lexruntimev2/src/main/resources/codegen-resources/customization.config
@@ -3,5 +3,6 @@
"contentType": "application/json"
},
"enableGenerateCompiledEndpointRules": true,
+ "usePriorKnowledgeForH2": true,
"enableFastUnmarshaller": false
}
diff --git a/services/qbusiness/src/main/resources/codegen-resources/customization.config b/services/qbusiness/src/main/resources/codegen-resources/customization.config
index f010e660efef..432ab1dfd4e7 100644
--- a/services/qbusiness/src/main/resources/codegen-resources/customization.config
+++ b/services/qbusiness/src/main/resources/codegen-resources/customization.config
@@ -1,4 +1,5 @@
{
"enableGenerateCompiledEndpointRules": true,
+ "usePriorKnowledgeForH2": true,
"enableFastUnmarshaller": false
}
diff --git a/services/transcribestreaming/src/it/java/software/amazon/awssdk/services/transcribestreaming/TranscribeStreamingIntegrationTest.java b/services/transcribestreaming/src/it/java/software/amazon/awssdk/services/transcribestreaming/TranscribeStreamingIntegrationTest.java
index 54fb9a67d4af..c5555458f174 100644
--- a/services/transcribestreaming/src/it/java/software/amazon/awssdk/services/transcribestreaming/TranscribeStreamingIntegrationTest.java
+++ b/services/transcribestreaming/src/it/java/software/amazon/awssdk/services/transcribestreaming/TranscribeStreamingIntegrationTest.java
@@ -15,7 +15,6 @@
package software.amazon.awssdk.services.transcribestreaming;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static software.amazon.awssdk.http.Header.CONTENT_TYPE;
@@ -27,9 +26,9 @@
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import org.junit.BeforeClass;
-import org.junit.Test;
+import java.util.stream.Stream;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
@@ -41,6 +40,9 @@
import software.amazon.awssdk.core.internal.util.Mimetype;
import software.amazon.awssdk.core.metrics.CoreMetric;
import software.amazon.awssdk.http.HttpMetric;
+import software.amazon.awssdk.http.Protocol;
+import software.amazon.awssdk.http.ProtocolNegotiation;
+import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
import software.amazon.awssdk.metrics.MetricCollection;
import software.amazon.awssdk.metrics.MetricPublisher;
import software.amazon.awssdk.regions.Region;
@@ -60,43 +62,61 @@
*/
public class TranscribeStreamingIntegrationTest {
private static final Logger log = Logger.loggerFor(TranscribeStreamingIntegrationTest.class);
+ private TranscribeStreamingAsyncClient client;
+ private MetricPublisher mockPublisher;
- private static TranscribeStreamingAsyncClient client;
-
- private static MetricPublisher mockPublisher;
-
- @BeforeClass
- public static void setup() {
- mockPublisher = mock(MetricPublisher.class);
- client = TranscribeStreamingAsyncClient.builder()
- .region(Region.US_EAST_1)
- .overrideConfiguration(b -> b.addExecutionInterceptor(new VerifyHeaderInterceptor())
- .addMetricPublisher(mockPublisher))
- .credentialsProvider(getCredentials())
- .build();
+ private static Stream protocolNegotiations() {
+ return Stream.of(ProtocolNegotiation.ASSUME_PROTOCOL, ProtocolNegotiation.ALPN);
}
- @Test
- public void testFileWith16kRate() throws InterruptedException {
- CompletableFuture result = client.startStreamTranscription(getRequest(16_000),
- new AudioStreamPublisher(
- getInputStream("silence_16kHz_s16le.wav")),
- TestResponseHandlers.responseHandlerBuilder_Classic());
+ @ParameterizedTest
+ @MethodSource("protocolNegotiations")
+ public void testFileWith16kRate(ProtocolNegotiation protocolNegotiation) throws Exception {
+ initClient(protocolNegotiation);
+
+ CompletableFuture result = client.startStreamTranscription(
+ getRequest(16_000),
+ new AudioStreamPublisher(getInputStream("silence_16kHz_s16le.wav")),
+ TestResponseHandlers.responseHandlerBuilder_Classic());
result.join();
verifyMetrics();
}
- @Test
- public void testFileWith8kRate() throws ExecutionException, InterruptedException {
- CompletableFuture result = client.startStreamTranscription(getRequest(8_000),
- new AudioStreamPublisher(
- getInputStream("silence_8kHz_s16le.wav")),
- TestResponseHandlers.responseHandlerBuilder_Consumer());
+ @ParameterizedTest
+ @MethodSource("protocolNegotiations")
+ public void testFileWith8kRate(ProtocolNegotiation protocolNegotiation) throws Exception {
+ initClient(protocolNegotiation);
+
+ CompletableFuture result = client.startStreamTranscription(
+ getRequest(8_000),
+ new AudioStreamPublisher(getInputStream("silence_8kHz_s16le.wav")),
+ TestResponseHandlers.responseHandlerBuilder_Consumer());
result.get();
}
+ private void initClient(ProtocolNegotiation protocolNegotiation) {
+ if (client != null) {
+ client.close();
+ }
+ if (mockPublisher != null) {
+ mockPublisher.close();
+ }
+
+ mockPublisher = mock(MetricPublisher.class);
+ client = TranscribeStreamingAsyncClient.builder()
+ .region(Region.US_EAST_1)
+ .overrideConfiguration(b -> b.addExecutionInterceptor(new VerifyHeaderInterceptor())
+ .addMetricPublisher(mockPublisher))
+ .credentialsProvider(getCredentials())
+ .httpClient(NettyNioAsyncHttpClient.builder()
+ .protocol(Protocol.HTTP2)
+ .protocolNegotiation(protocolNegotiation)
+ .build())
+ .build();
+ }
+
private static AwsCredentialsProvider getCredentials() {
return DefaultCredentialsProvider.create();
}
@@ -112,15 +132,14 @@ private StartStreamTranscriptionRequest getRequest(Integer mediaSampleRateHertz)
private InputStream getInputStream(String audioFileName) {
try {
File inputFile = new File(getClass().getClassLoader().getResource(audioFileName).getFile());
- assertTrue(inputFile.exists());
- InputStream audioStream = new FileInputStream(inputFile);
- return audioStream;
+ assertThat(inputFile).exists();
+ return new FileInputStream(inputFile);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
- private class AudioStreamPublisher implements Publisher {
+ private static final class AudioStreamPublisher implements Publisher {
private final InputStream inputStream;
private AudioStreamPublisher(InputStream inputStream) {
@@ -170,5 +189,4 @@ private void verifyMetrics() throws InterruptedException {
assertThat(attemptCollection.metricValues(CoreMetric.SERVICE_CALL_DURATION).get(0))
.isGreaterThanOrEqualTo(Duration.ofMillis(100));
}
-
}
diff --git a/services/transcribestreaming/src/main/resources/codegen-resources/customization.config b/services/transcribestreaming/src/main/resources/codegen-resources/customization.config
index 00aeca44496a..bb7b25c942a4 100644
--- a/services/transcribestreaming/src/main/resources/codegen-resources/customization.config
+++ b/services/transcribestreaming/src/main/resources/codegen-resources/customization.config
@@ -6,6 +6,7 @@
"TranscriptResultStream": ["TranscriptEvent"],
"AudioStream": ["AudioEvent"],
"MedicalTranscriptResultStream": ["TranscriptEvent"]
- },
- "enableFastUnmarshaller": false
+ },
+ "usePriorKnowledgeForH2": true,
+ "enableFastUnmarshaller": false
}
diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apicall/httpclient/async/NettyHttpClientAlpnBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apicall/httpclient/async/NettyHttpClientAlpnBenchmark.java
new file mode 100644
index 000000000000..0972b9cd3db6
--- /dev/null
+++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apicall/httpclient/async/NettyHttpClientAlpnBenchmark.java
@@ -0,0 +1,101 @@
+/*
+ * 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.benchmark.apicall.httpclient.async;
+
+import static software.amazon.awssdk.benchmark.utils.BenchmarkConstant.DEFAULT_JDK_SSL_PROVIDER;
+import static software.amazon.awssdk.benchmark.utils.BenchmarkConstant.OPEN_SSL_PROVIDER;
+import static software.amazon.awssdk.benchmark.utils.BenchmarkUtils.getSslProvider;
+import static software.amazon.awssdk.benchmark.utils.BenchmarkUtils.trustAllTlsAttributeMapBuilder;
+
+import io.netty.handler.ssl.SslProvider;
+import java.util.Collection;
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.results.RunResult;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+import software.amazon.awssdk.benchmark.utils.MockH2Server;
+import software.amazon.awssdk.http.Protocol;
+import software.amazon.awssdk.http.ProtocolNegotiation;
+import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
+import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonAsyncClient;
+
+/**
+ * Using netty client with ALPN to test against local http2 server with ALPN support.
+ */
+@State(Scope.Benchmark)
+@Warmup(iterations = 3, time = 15, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS)
+@Fork(2) // To reduce difference between each run
+@BenchmarkMode(Mode.Throughput)
+public class NettyHttpClientAlpnBenchmark extends BaseNettyBenchmark {
+
+ private MockH2Server mockServer;
+ private SdkAsyncHttpClient sdkHttpClient;
+
+ @Param({DEFAULT_JDK_SSL_PROVIDER, OPEN_SSL_PROVIDER})
+ private String sslProviderValue;
+
+ @Setup(Level.Trial)
+ public void setup() throws Exception {
+ boolean usingAlpn = true;
+ mockServer = new MockH2Server(usingAlpn);
+ mockServer.start();
+
+ SslProvider sslProvider = getSslProvider(sslProviderValue);
+
+ sdkHttpClient = NettyNioAsyncHttpClient.builder()
+ .sslProvider(sslProvider)
+ .protocol(Protocol.HTTP2)
+ .protocolNegotiation(ProtocolNegotiation.ALPN)
+ .buildWithDefaults(trustAllTlsAttributeMapBuilder().build());
+ client = ProtocolRestJsonAsyncClient.builder()
+ .endpointOverride(mockServer.getHttpsUri())
+ .httpClient(sdkHttpClient)
+ .region(Region.US_EAST_1)
+ .build();
+
+ // Making sure the request actually succeeds
+ client.allTypes().join();
+ }
+
+ @TearDown(Level.Trial)
+ public void tearDown() throws Exception {
+ mockServer.stop();
+ sdkHttpClient.close();
+ client.close();
+ }
+
+ public static void main(String... args) throws Exception {
+ Options opt = new OptionsBuilder()
+ .include(NettyHttpClientH2Benchmark.class.getSimpleName())
+ .build();
+ Collection run = new Runner(opt).run();
+ }
+}
\ No newline at end of file
diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apicall/httpclient/async/NettyHttpClientH2Benchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apicall/httpclient/async/NettyHttpClientH2Benchmark.java
index 97c42bde324f..ffbd537da1aa 100644
--- a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apicall/httpclient/async/NettyHttpClientH2Benchmark.java
+++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apicall/httpclient/async/NettyHttpClientH2Benchmark.java
@@ -19,7 +19,6 @@
import static software.amazon.awssdk.benchmark.utils.BenchmarkConstant.OPEN_SSL_PROVIDER;
import static software.amazon.awssdk.benchmark.utils.BenchmarkUtils.getSslProvider;
import static software.amazon.awssdk.benchmark.utils.BenchmarkUtils.trustAllTlsAttributeMapBuilder;
-import static software.amazon.awssdk.http.SdkHttpConfigurationOption.PROTOCOL;
import io.netty.handler.ssl.SslProvider;
import java.util.Collection;
@@ -43,6 +42,7 @@
import software.amazon.awssdk.http.Protocol;
import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
+import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonAsyncClient;
/**
@@ -63,19 +63,20 @@ public class NettyHttpClientH2Benchmark extends BaseNettyBenchmark {
@Setup(Level.Trial)
public void setup() throws Exception {
- mockServer = new MockH2Server(false);
+ boolean usingAlpn = false;
+ mockServer = new MockH2Server(usingAlpn);
mockServer.start();
SslProvider sslProvider = getSslProvider(sslProviderValue);
sdkHttpClient = NettyNioAsyncHttpClient.builder()
.sslProvider(sslProvider)
- .buildWithDefaults(trustAllTlsAttributeMapBuilder()
- .put(PROTOCOL, Protocol.HTTP2)
- .build());
+ .protocol(Protocol.HTTP2)
+ .buildWithDefaults(trustAllTlsAttributeMapBuilder().build());
client = ProtocolRestJsonAsyncClient.builder()
.endpointOverride(mockServer.getHttpsUri())
.httpClient(sdkHttpClient)
+ .region(Region.US_EAST_1)
.build();
// Making sure the request actually succeeds
diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/utils/MockH2Server.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/utils/MockH2Server.java
index 5b1f20817940..4a770193ff1e 100644
--- a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/utils/MockH2Server.java
+++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/utils/MockH2Server.java
@@ -81,15 +81,16 @@ public MockH2Server(boolean usingAlpn) throws IOException {
HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(https);
// SSL Connection Factory
- SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, "h2");
+ SslConnectionFactory ssl;
ServerConnector http2Connector;
if (usingAlpn) {
- ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory();
- alpn.setDefaultProtocol("h2");
+ ssl = new SslConnectionFactory(sslContextFactory, "alpn");
+ ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory("h2");
// HTTP/2 Connector
http2Connector = new ServerConnector(server, ssl, alpn, h2, new HttpConnectionFactory(https));
} else {
+ ssl = new SslConnectionFactory(sslContextFactory, "h2");
http2Connector = new ServerConnector(server, ssl, h2, new HttpConnectionFactory(https));
}
diff --git a/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/transcribestreaming/TranscribeStreamingStabilityTest.java b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/transcribestreaming/TranscribeStreamingStabilityTest.java
index 988be1ab18c5..0c98711d332a 100644
--- a/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/transcribestreaming/TranscribeStreamingStabilityTest.java
+++ b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/transcribestreaming/TranscribeStreamingStabilityTest.java
@@ -24,6 +24,8 @@
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import software.amazon.awssdk.core.async.SdkPublisher;
+import software.amazon.awssdk.http.Protocol;
+import software.amazon.awssdk.http.ProtocolNegotiation;
import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
import software.amazon.awssdk.services.transcribestreaming.TranscribeStreamingAsyncClient;
import software.amazon.awssdk.services.transcribestreaming.model.AudioStream;
@@ -45,17 +47,14 @@ public class TranscribeStreamingStabilityTest extends AwsTestBase {
private static final Logger log = Logger.loggerFor(TranscribeStreamingStabilityTest.class.getSimpleName());
public static final int CONCURRENCY = 2;
public static final int TOTAL_RUNS = 1;
- private static TranscribeStreamingAsyncClient transcribeStreamingClient;
+ private static TranscribeStreamingAsyncClient asyncClient;
+ private static TranscribeStreamingAsyncClient asyncClientAlpn;
private static InputStream audioFileInputStream;
@BeforeAll
public static void setup() {
- transcribeStreamingClient = TranscribeStreamingAsyncClient.builder()
- .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN)
- .httpClientBuilder(NettyNioAsyncHttpClient.builder()
- .connectionAcquisitionTimeout(Duration.ofSeconds(30))
- .maxConcurrency(CONCURRENCY))
- .build();
+ asyncClient = initClient(ProtocolNegotiation.ASSUME_PROTOCOL);
+ asyncClientAlpn = initClient(ProtocolNegotiation.ALPN);
audioFileInputStream = getInputStream();
@@ -64,19 +63,47 @@ public static void setup() {
}
}
+ private static TranscribeStreamingAsyncClient initClient(ProtocolNegotiation protocolNegotiation) {
+ return TranscribeStreamingAsyncClient.builder()
+ .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN)
+ .httpClientBuilder(NettyNioAsyncHttpClient.builder()
+ .connectionAcquisitionTimeout(Duration.ofSeconds(30))
+ .maxConcurrency(CONCURRENCY)
+ .protocol(Protocol.HTTP2)
+ .protocolNegotiation(protocolNegotiation))
+ .build();
+ }
+
@AfterAll
public static void tearDown() {
- transcribeStreamingClient.close();
+ asyncClient.close();
+ asyncClientAlpn.close();
}
@RetryableTest(maxRetries = 3, retryableException = StabilityTestsRetryableException.class)
public void startTranscription() {
IntFunction> futureIntFunction = i ->
- transcribeStreamingClient.startStreamTranscription(b -> b.mediaSampleRateHertz(8_000)
- .languageCode(LanguageCode.EN_US)
- .mediaEncoding(MediaEncoding.PCM),
- new AudioStreamPublisher(),
- new TestStartStreamTranscriptionResponseHandler());
+ asyncClient.startStreamTranscription(b -> b.mediaSampleRateHertz(8_000)
+ .languageCode(LanguageCode.EN_US)
+ .mediaEncoding(MediaEncoding.PCM),
+ new AudioStreamPublisher(),
+ new TestStartStreamTranscriptionResponseHandler());
+ StabilityTestRunner.newRunner()
+ .futureFactory(futureIntFunction)
+ .totalRuns(TOTAL_RUNS)
+ .requestCountPerRun(CONCURRENCY)
+ .testName("TranscribeStreamingStabilityTest.startTranscription")
+ .run();
+ }
+
+ @RetryableTest(maxRetries = 3, retryableException = StabilityTestsRetryableException.class)
+ public void startTranscription_alpnEnabled() {
+ IntFunction> futureIntFunction = i ->
+ asyncClientAlpn.startStreamTranscription(b -> b.mediaSampleRateHertz(8_000)
+ .languageCode(LanguageCode.EN_US)
+ .mediaEncoding(MediaEncoding.PCM),
+ new AudioStreamPublisher(),
+ new TestStartStreamTranscriptionResponseHandler());
StabilityTestRunner.newRunner()
.futureFactory(futureIntFunction)
.totalRuns(TOTAL_RUNS)
@@ -89,7 +116,7 @@ private static InputStream getInputStream() {
return TranscribeStreamingStabilityTest.class.getResourceAsStream("silence_8kHz.wav");
}
- private class AudioStreamPublisher implements Publisher {
+ private static class AudioStreamPublisher implements Publisher {
@Override
public void subscribe(Subscriber super AudioStream> s) {