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:

+ *
    + *
  1. 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.
  2. + *
  3. For all other services, the default value is {@code ProtocolNegotiation.ASSUME_PROTOCOL}, in which case the SDK + * will use prior knowledge to establish connections.
  4. + *
+ * 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}: + *
    + *
  1. Default SDK setting of ALPN → SDK will fallback to prior knowledge and not use ALPN.
  2. + *
  3. User explicitly sets value of ALPN → Exception will be thrown.
  4. + *
+ */ + 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 s) {