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..b3306a16057a --- /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 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/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java index 4ae9f93c1955..bc400cbdc7ad 100644 --- a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java @@ -18,6 +18,7 @@ import static software.amazon.awssdk.crtcore.CrtConfigurationUtils.resolveHttpMonitoringOptions; import static software.amazon.awssdk.crtcore.CrtConfigurationUtils.resolveProxy; import static software.amazon.awssdk.http.SdkHttpConfigurationOption.PROTOCOL; +import static software.amazon.awssdk.http.SdkHttpConfigurationOption.PROTOCOL_NEGOTIATION; import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.buildSocketOptions; import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.resolveCipherPreference; import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely; @@ -37,6 +38,7 @@ import software.amazon.awssdk.crt.io.TlsContext; import software.amazon.awssdk.crt.io.TlsContextOptions; 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.crt.internal.AwsCrtClientBuilderBase; @@ -73,6 +75,11 @@ abstract class AwsCrtHttpClientBase implements SdkAutoCloseable { + "NettyNioAsyncHttpClient instead."); } + if (config.get(PROTOCOL_NEGOTIATION) == ProtocolNegotiation.ALPN) { + throw new UnsupportedOperationException("LPN is not supported in AwsCrtHttpClient yet. Use " + + "NettyNioAsyncHttpClient instead."); + } + try (ClientBootstrap clientBootstrap = new ClientBootstrap(null, null); SocketOptions clientSocketOptions = buildSocketOptions(builder.getTcpKeepAliveConfiguration(), config.get(SdkHttpConfigurationOption.CONNECTION_TIMEOUT)); 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..c0009ed4d766 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; @@ -86,6 +89,8 @@ public final class NettyNioAsyncHttpClient implements SdkAsyncHttpClient { private NettyNioAsyncHttpClient(DefaultBuilder builder, AttributeMap serviceDefaultsMap) { this.configuration = new NettyConfiguration(serviceDefaultsMap); Protocol protocol = serviceDefaultsMap.get(SdkHttpConfigurationOption.PROTOCOL); + ProtocolNegotiation protocolNegotiation = resolveProtocolNegotiation(builder.protocolNegotiation, serviceDefaultsMap, + protocol); this.sdkEventLoopGroup = eventLoopGroup(builder); Http2Configuration http2Configuration = builder.http2Configuration; @@ -97,6 +102,7 @@ private NettyNioAsyncHttpClient(DefaultBuilder builder, AttributeMap serviceDefa .sdkChannelOptions(builder.sdkChannelOptions) .configuration(configuration) .protocol(protocol) + .protocolNegotiation(protocolNegotiation) .maxStreams(maxStreams) .initialWindowSize(initialWindowSize) .healthCheckPingPeriod(resolveHealthCheckPingPeriod(http2Configuration)) @@ -162,6 +168,36 @@ private SslProvider resolveSslProvider(DefaultBuilder builder) { return SslContext.defaultClientProvider(); } + private ProtocolNegotiation resolveProtocolNegotiation(ProtocolNegotiation userSetValue, AttributeMap serviceDefaultsMap, + Protocol protocol) { + if (userSetValue == ProtocolNegotiation.ALPN) { + // TODO - remove once we implement support for ALPN with HTTP1 + if (protocol == Protocol.HTTP1_1) { + throw new UnsupportedOperationException("ALPN with HTTP1 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(); + 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()) { + // 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 +407,20 @@ public interface Builder extends SdkAsyncHttpClient.Builder + * Default value is true for services with H2 protocol setting, with the exception of the following services: + * Kinesis, TranscribeStreaming, Lex Runtime v2, Q Business + *

+ * Note: For Java 8, ALPN is only supported in versions 1.8.0_251 and newer. + * If on unsupported Java version: + * 1) Default SDK setting of true -> SDK will fallback to prior knowledge and not use ALPN + * 2) User explicitly sets value of true -> Exception will be thrown + */ + Builder protocolNegotiation(ProtocolNegotiation protocolNegotiation); + /** * Configure whether to enable or disable TCP KeepAlive. * The configuration will be passed to the socket option {@link SocketOptions#SO_KEEPALIVE}. @@ -503,6 +553,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 +691,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..598772147179 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,26 @@ public void channelCreated(Channel ch) { } } - if (protocol == Protocol.HTTP2) { - configureHttp2(ch, pipeline); - } else { - configureHttp11(ch, pipeline); - } - - if (configuration.reapIdleConnections()) { - pipeline.addLast(new IdleConnectionReaperHandler(configuration.idleTimeoutMillis())); - } - - if (configuration.connectionTtlMillis() > 0) { - pipeline.addLast(new OldConnectionReaperHandler(configuration.connectionTtlMillis())); - } - - pipeline.addLast(FutureCancelHandler.getInstance()); + configureProtocolHandlers(ch, pipeline, protocol); + configurePostProtocolHandlers(pipeline, protocol); + } - // 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()); + private void configureProtocolHandlers(Channel ch, ChannelPipeline pipeline, Protocol protocol) { + if (protocolNegotiation == ProtocolNegotiation.ASSUME_PROTOCOL) { + if (protocol == Protocol.HTTP1_1) { + configureHttp11(ch, pipeline); + } else if (protocol == Protocol.HTTP2) { + configureHttp2(ch, pipeline); + } + } else if (protocolNegotiation == ProtocolNegotiation.ALPN) { + if (protocol == Protocol.HTTP1_1) { + // TODO - verify functionality once we implement ALPN with H1 + // we will never reach here since we throw error in NettyNioAsyncHttpClient for ALPN + H1 + configureAlpnH1(pipeline); + } else if (protocol == Protocol.HTTP2) { + configureAlpnH2(pipeline); + } } - - pipeline.addLast(new LoggingHandler(LogLevel.DEBUG)); } private void configureHttp2(Channel ch, ChannelPipeline pipeline) { @@ -167,11 +170,52 @@ 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 (ApplicationProtocolNames.HTTP_2.equals(protocol)) { + configureHttp2(ctx.channel(), ctx.pipeline()); + } + } + }); + } + + private void configureAlpnH1(ChannelPipeline pipeline) { + pipeline.addLast(new ApplicationProtocolNegotiationHandler("") { + @Override + protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { + if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { + configureHttp11(ctx.channel(), ctx.pipeline()); + } + } + }); + } + + 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..3177eeee50da 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,30 @@ 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); + + if (protocolNegotiation == ProtocolNegotiation.ALPN) { + builder.applicationProtocolConfig( + new ApplicationProtocolConfig(ApplicationProtocolConfig.Protocol.ALPN, + ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, + ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, + resolveNettyProtocol(protocol))); + } + + return builder.build(); } catch (SSLException e) { throw new RuntimeException(e); } } + 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..680d2555696f 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 @@ -388,4 +388,35 @@ 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() { + String javaVersion = getJavaVersion(); + String[] versionComponents = javaVersion.split("_"); + if (versionComponents.length == 2) { + try { + int buildNumber = Integer.parseInt(versionComponents[1].split("-")[0]); + if (javaVersion.startsWith("1.8.0") && buildNumber < 251) { + return false; + } + } catch (NumberFormatException e) { + log.error(() -> "Invalid Java version format: " + javaVersion); + throw e; + } + } + return true; + } + + public static String getJavaVersion() { + // CHECKSTYLE:OFF + return System.getProperty("java.version"); + // CHECKSTYLE:ON + } + + public static void validateAlpnSupported() { + if (!isAlpnSupported()) { + throw new UnsupportedOperationException("ALPN is not supported in the current Java Version: " + getJavaVersion()); + } + } }