diff --git a/driver/pom.xml b/driver/pom.xml index 6b68e4c7..27788bfd 100644 --- a/driver/pom.xml +++ b/driver/pom.xml @@ -46,7 +46,7 @@ Copyright (c) 2011, 2022 Oracle and/or its affiliates. All rights reserved. http://docs.oracle.com/javase/8/docs/api false - 4.1.77.Final + 4.1.82.Final 2.12.5 1.70 @@ -175,6 +175,11 @@ netty-codec-http ${netty.version} + + io.netty + netty-codec-http2 + ${netty.version} + io.netty netty-handler diff --git a/driver/src/main/java/oracle/nosql/driver/NoSQLHandleConfig.java b/driver/src/main/java/oracle/nosql/driver/NoSQLHandleConfig.java index da3cc407..9c14c136 100644 --- a/driver/src/main/java/oracle/nosql/driver/NoSQLHandleConfig.java +++ b/driver/src/main/java/oracle/nosql/driver/NoSQLHandleConfig.java @@ -21,6 +21,7 @@ import oracle.nosql.driver.Region.RegionProvider; import oracle.nosql.driver.iam.SignatureProvider; +import oracle.nosql.driver.util.HttpConstants; import io.netty.handler.ssl.SslContext; /** @@ -142,6 +143,13 @@ public class NoSQLHandleConfig implements Cloneable { */ private int maxChunkSize = 0; + /** + * Default http protocols + * + * Default: prefer H2 but fallback to Http1.1 + */ + private List httpProtocols = new ArrayList<>(Arrays.asList(HttpConstants.HTTP_2, HttpConstants.HTTP_1_1)); + /** * A RetryHandler, or null if not configured by the user. */ @@ -553,6 +561,18 @@ public int getDefaultRequestTimeout() { return timeout == 0 ? DEFAULT_TIMEOUT : timeout; } + /** + * Returns the list of Http Protocols. If there is no configured + * protocol, a "default" value of + * List({@link HttpConstants#HTTP_2}, {@link HttpConstants#HTTP_1_1}) + * is used. + * + * @return Http protocol settings + */ + public List getHttpProtocols() { + return httpProtocols; + } + /** * Returns the configured table request timeout value, in milliseconds. * The table request timeout default can be specified independently to allow @@ -631,6 +651,22 @@ public NoSQLHandleConfig setRequestTimeout(int timeout) { return this; } + /** + * Sets the default http protocol(s). The default is {@link HttpConstants#HTTP_2} + * and fall back to {@link HttpConstants#HTTP_1_1} + * + * @param protocols Protocol list + * + * @return this + */ + public NoSQLHandleConfig setHttpProtocols(String ... protocols) { + this.httpProtocols = new ArrayList<>(2); + for (String p : protocols) { + this.httpProtocols.add(p); + } + return this; + } + /** * Sets the default table request timeout. * The table request timeout can be specified independently diff --git a/driver/src/main/java/oracle/nosql/driver/http/Client.java b/driver/src/main/java/oracle/nosql/driver/http/Client.java index c0063982..2c82d43a 100644 --- a/driver/src/main/java/oracle/nosql/driver/http/Client.java +++ b/driver/src/main/java/oracle/nosql/driver/http/Client.java @@ -249,6 +249,7 @@ public Client(Logger logger, sslCtx, config.getSSLHandshakeTimeout(), "NoSQL Driver", + config.getHttpProtocols(), logger); if (httpConfig.getProxyHost() != null) { httpClient.configureProxy(httpConfig); diff --git a/driver/src/main/java/oracle/nosql/driver/http/NoSQLHandleImpl.java b/driver/src/main/java/oracle/nosql/driver/http/NoSQLHandleImpl.java index b5f33c47..17a18476 100644 --- a/driver/src/main/java/oracle/nosql/driver/http/NoSQLHandleImpl.java +++ b/driver/src/main/java/oracle/nosql/driver/http/NoSQLHandleImpl.java @@ -12,6 +12,9 @@ import javax.net.ssl.SSLException; +import io.netty.handler.codec.http2.Http2SecurityUtil; +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; import oracle.nosql.driver.AuthorizationProvider; import oracle.nosql.driver.NoSQLHandle; import oracle.nosql.driver.NoSQLHandleConfig; @@ -46,6 +49,7 @@ import oracle.nosql.driver.ops.TableUsageResult; import oracle.nosql.driver.ops.WriteMultipleRequest; import oracle.nosql.driver.ops.WriteMultipleResult; +import oracle.nosql.driver.util.HttpConstants; import oracle.nosql.driver.values.FieldValue; import oracle.nosql.driver.values.JsonUtils; import oracle.nosql.driver.values.MapValue; @@ -124,6 +128,14 @@ private void configSslContext(NoSQLHandleConfig config) { } builder.sessionTimeout(config.getSSLSessionTimeout()); builder.sessionCacheSize(config.getSSLSessionCacheSize()); + if (config.getHttpProtocols().contains(HttpConstants.HTTP_2)) { + builder.ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE); + } + builder.applicationProtocolConfig( + new ApplicationProtocolConfig(ApplicationProtocolConfig.Protocol.ALPN, + ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, + ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, + config.getHttpProtocols())); config.setSslContext(builder.build()); } catch (SSLException se) { throw new IllegalStateException( @@ -137,6 +149,7 @@ private void configAuthProvider(Logger logger, NoSQLHandleConfig config) { if (ap instanceof StoreAccessTokenProvider) { final StoreAccessTokenProvider stProvider = (StoreAccessTokenProvider) ap; + stProvider.setHttpProtocols(config.getHttpProtocols()); if (stProvider.getLogger() == null) { stProvider.setLogger(logger); } diff --git a/driver/src/main/java/oracle/nosql/driver/httpclient/HttpClient.java b/driver/src/main/java/oracle/nosql/driver/httpclient/HttpClient.java index a23cc4a6..88b32ed9 100644 --- a/driver/src/main/java/oracle/nosql/driver/httpclient/HttpClient.java +++ b/driver/src/main/java/oracle/nosql/driver/httpclient/HttpClient.java @@ -17,6 +17,9 @@ import static oracle.nosql.driver.util.LogUtil.logWarning; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -39,6 +42,7 @@ * from this config needs to be be abstracted to a generic class. */ import oracle.nosql.driver.NoSQLHandleConfig; +import oracle.nosql.driver.util.HttpConstants; /** * Netty HTTP client. Initialization process: @@ -96,6 +100,8 @@ public class HttpClient { private final String host; private final int port; private final String name; + private final List httpProtocols; + private final String httpFallbackProtocol; /* * Amount of time to wait for acquiring a channel before timing @@ -134,12 +140,14 @@ public class HttpClient { * @param handshakeTimeoutMs if not zero, timeout to use for SSL handshake * @param name A name to use in logging messages for this client. * @param logger A logger to use for logging messages. + * @param httpProtocols A list of preferred http protocols (H2 and Http1.1) */ public static HttpClient createMinimalClient(String host, int port, SslContext sslCtx, int handshakeTimeoutMs, String name, + List httpProtocols, Logger logger) { return new HttpClient(host, port, @@ -149,7 +157,7 @@ public static HttpClient createMinimalClient(String host, true, /* minimal client */ DEFAULT_MAX_CONTENT_LENGTH, DEFAULT_MAX_CHUNK_SIZE, - sslCtx, handshakeTimeoutMs, name, logger); + sslCtx, handshakeTimeoutMs, name, httpProtocols, logger); } /** @@ -178,6 +186,7 @@ public static HttpClient createMinimalClient(String host, * @param handshakeTimeoutMs if not zero, timeout to use for SSL handshake * @param name A name to use in logging messages for this client. * @param logger A logger to use for logging messages. + * @param httpProtocols A list of preferred http protocols (H2 and Http1.1) */ public HttpClient(String host, int port, @@ -189,11 +198,12 @@ public HttpClient(String host, SslContext sslCtx, int handshakeTimeoutMs, String name, + List httpProtocols, Logger logger) { this(host, port, numThreads, connectionPoolMinSize, inactivityPeriodSeconds, false /* not minimal */, - maxContentLength, maxChunkSize, sslCtx, handshakeTimeoutMs, name, logger); + maxContentLength, maxChunkSize, sslCtx, handshakeTimeoutMs, name, httpProtocols, logger); } /* @@ -210,6 +220,7 @@ private HttpClient(String host, SslContext sslCtx, int handshakeTimeoutMs, String name, + List httpProtocols, Logger logger) { this.logger = logger; @@ -218,6 +229,15 @@ private HttpClient(String host, this.port = port; this.name = name; + this.httpProtocols = httpProtocols.size() > 0 ? + httpProtocols : + new ArrayList<>(Arrays.asList(HttpConstants.HTTP_2, HttpConstants.HTTP_1_1)); + + // If Http1.1 is in the httpProtocols list, we prefer use it as the fallback + // Else we use the last protocol in the httpProtocols list. + this.httpFallbackProtocol = this.httpProtocols.contains(HttpConstants.HTTP_1_1) ? + HttpConstants.HTTP_1_1 : this.httpProtocols.get(this.httpProtocols.size() - 1); + this.maxContentLength = (maxContentLength == 0 ? DEFAULT_MAX_CONTENT_LENGTH : maxContentLength); this.maxChunkSize = (maxChunkSize == 0 ? @@ -292,6 +312,14 @@ String getName() { return name; } + List getHttpProtocols() { + return httpProtocols; + } + + public String getHttpFallbackProtocol() { + return httpFallbackProtocol; + } + Logger getLogger() { return logger; } diff --git a/driver/src/main/java/oracle/nosql/driver/httpclient/HttpClientChannelPoolHandler.java b/driver/src/main/java/oracle/nosql/driver/httpclient/HttpClientChannelPoolHandler.java index 471cfdc7..91474457 100644 --- a/driver/src/main/java/oracle/nosql/driver/httpclient/HttpClientChannelPoolHandler.java +++ b/driver/src/main/java/oracle/nosql/driver/httpclient/HttpClientChannelPoolHandler.java @@ -10,6 +10,7 @@ import static oracle.nosql.driver.util.LogUtil.logFine; import java.net.InetSocketAddress; + import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLParameters; @@ -21,12 +22,11 @@ import io.netty.channel.EventLoop; import io.netty.channel.pool.ChannelHealthChecker; import io.netty.channel.pool.ChannelPoolHandler; -import io.netty.handler.codec.http.HttpClientCodec; -import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.proxy.HttpProxyHandler; import io.netty.handler.ssl.SslHandler; import io.netty.handler.ssl.SslHandshakeCompletionEvent; import io.netty.util.concurrent.Future; +import oracle.nosql.driver.util.HttpConstants; /** * This is an instance of Netty's ChannelPoolHandler used to initialize @@ -37,10 +37,6 @@ public class HttpClientChannelPoolHandler implements ChannelPoolHandler, ChannelHealthChecker { - private static final String CODEC_HANDLER_NAME = "http-codec"; - private static final String AGG_HANDLER_NAME = "http-aggregator"; - private static final String HTTP_HANDLER_NAME = "http-response-handler"; - private final HttpClient client; /** @@ -53,6 +49,63 @@ public class HttpClientChannelPoolHandler implements ChannelPoolHandler, this.client = client; } + private void configureSSL(Channel ch) { + ChannelPipeline p = ch.pipeline(); + /* Enable hostname verification */ + final SslHandler sslHandler = client.getSslContext().newHandler( + ch.alloc(), client.getHost(), client.getPort()); + final SSLEngine sslEngine = sslHandler.engine(); + final SSLParameters sslParameters = sslEngine.getSSLParameters(); + sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); + sslEngine.setSSLParameters(sslParameters); + sslHandler.setHandshakeTimeoutMillis(client.getHandshakeTimeoutMs()); + + p.addLast(sslHandler); + p.addLast(new ChannelLoggingHandler(client)); + // Handle ALPN protocol negotiation result, and configure the pipeline accordingly + p.addLast(new HttpProtocolNegotiationHandler( + client.getHttpFallbackProtocol(), new HttpClientHandler(client.getLogger()), + client.getMaxChunkSize(), client.getMaxContentLength(), client.getLogger())); + } + + private void configureClearText(Channel ch) { + ChannelPipeline p = ch.pipeline(); + HttpClientHandler handler = new HttpClientHandler(client.getLogger()); + boolean useHttp2 = client.getHttpProtocols().contains(HttpConstants.HTTP_2); + + // Only true when HTTP_2 is the only protocol, as if user set: + // config.setHttpProtocols(HttpConstants.HTTP_2); + if (useHttp2 && + HttpConstants.HTTP_2.equals(client.getHttpFallbackProtocol())) { + // If choose to use H2 and fallback is also H2 + // Then there is no need to upgrade from Http1.1 to H2C + // Directly connects with H2 protocol, so called Http2-prior-knowledge + HttpUtil.configureHttp2(ch.pipeline(), client.getMaxContentLength()); + p.addLast(handler); + return; + } + + // Only true when HTTP_1_1 is the only protocol, as if user set: + // config.setHttpProtocols(HttpConstants.HTTP_1_1); + if (!useHttp2 && + HttpConstants.HTTP_1_1.equals(client.getHttpFallbackProtocol())) { + HttpUtil.configureHttp1(ch.pipeline(), client.getMaxChunkSize(), client.getMaxContentLength()); + p.addLast(handler); + return; + } + + // Only true when both HTTP_2 and HTTP_1_1 are available, the default option: + // config.setHttpProtocols(HttpConstants.HTTP_2, + // HttpConstants.HTTP_1_1) + if (useHttp2 && + HttpConstants.HTTP_1_1.equals(client.getHttpFallbackProtocol())) { + HttpUtil.configureH2C(ch.pipeline(), client.getMaxChunkSize(), client.getMaxContentLength()); + p.addLast(handler); + return; + } + throw new IllegalStateException("unknown protocol: " + client.getHttpProtocols()); + } + /** * Initialize a channel with handlers that: * 1 -- handle and HTTP @@ -67,28 +120,11 @@ public void channelCreated(Channel ch) { logFine(client.getLogger(), "HttpClient " + client.getName() + ", channel created: " + ch + ", acquired channel cnt " + client.getAcquiredChannelCount()); - ChannelPipeline p = ch.pipeline(); if (client.getSslContext() != null) { - /* Enable hostname verification */ - final SslHandler sslHandler = client.getSslContext().newHandler( - ch.alloc(), client.getHost(), client.getPort()); - final SSLEngine sslEngine = sslHandler.engine(); - final SSLParameters sslParameters = sslEngine.getSSLParameters(); - sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); - sslEngine.setSSLParameters(sslParameters); - sslHandler.setHandshakeTimeoutMillis(client.getHandshakeTimeoutMs()); - - p.addLast(sslHandler); - p.addLast(new ChannelLoggingHandler(client)); + configureSSL(ch); + } else { + configureClearText(ch); } - p.addLast(CODEC_HANDLER_NAME, new HttpClientCodec - (4096, // initial line - 8192, // header size - client.getMaxChunkSize())); - p.addLast(AGG_HANDLER_NAME, new HttpObjectAggregator( - client.getMaxContentLength())); - p.addLast(HTTP_HANDLER_NAME, - new HttpClientHandler(client.getLogger())); if (client.getProxyHost() != null) { InetSocketAddress sockAddr = @@ -101,7 +137,7 @@ public void channelCreated(Channel ch) { client.getProxyUsername(), client.getProxyPassword()); - p.addFirst("proxyServer", proxyHandler); + ch.pipeline().addFirst("proxyServer", proxyHandler); } } diff --git a/driver/src/main/java/oracle/nosql/driver/httpclient/HttpProtocolNegotiationHandler.java b/driver/src/main/java/oracle/nosql/driver/httpclient/HttpProtocolNegotiationHandler.java new file mode 100644 index 00000000..2f6690e1 --- /dev/null +++ b/driver/src/main/java/oracle/nosql/driver/httpclient/HttpProtocolNegotiationHandler.java @@ -0,0 +1,127 @@ +/*- + * Copyright (c) 2011, 2022 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +package oracle.nosql.driver.httpclient; + +import static oracle.nosql.driver.util.LogUtil.logFine; + +import java.net.SocketAddress; +import java.util.logging.Logger; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandler; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; +import io.netty.util.internal.RecyclableArrayList; +import oracle.nosql.driver.util.HttpConstants; + +/** + * Handle TLS protocol negotiation result, either Http1.1 or H2 + * + * The channel initialization process: + * 1. Channel acquired from {@link ConnectionPool} after channel is active. + * 2. SSL negotiation started, pipeline is not ready. + * 3. {@link HttpProtocolNegotiationHandler} holds all {@link HttpMessage} while waiting for the negotiation result. + * 4. Negotiation finished, {@link HttpProtocolNegotiationHandler} changes the pipeline according to the protocol selected. + * 5. {@link HttpProtocolNegotiationHandler} removes itself from the pipeline. Writes any buffered {@link HttpMessage} to the channel. + */ +public class HttpProtocolNegotiationHandler extends ApplicationProtocolNegotiationHandler implements ChannelOutboundHandler { + private static final String HTTP_HANDLER_NAME = "http-client-handler"; + + private final Logger logger; + private final RecyclableArrayList bufferedMessages = RecyclableArrayList.newInstance(); + private final HttpClientHandler handler; + private final int maxChunkSize; + private final int maxContentLength; + + public HttpProtocolNegotiationHandler(String fallbackProtocol, HttpClientHandler handler, int maxChunkSize, int maxContentLength, Logger logger) { + super(fallbackProtocol); + + this.logger = logger; + this.handler = handler; + this.maxChunkSize = maxChunkSize; + this.maxContentLength = maxContentLength; + } + + @Override + protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { + if (HttpConstants.HTTP_2.equals(protocol)) { + HttpUtil.configureHttp2(ctx.pipeline(), this.maxContentLength); + } else if (HttpConstants.HTTP_1_1.equals(protocol)) { + HttpUtil.configureHttp1(ctx.pipeline(), this.maxChunkSize, this.maxContentLength); + } else { + throw new IllegalStateException("unknown http protocol: " + protocol); + } + logFine(this.logger, "HTTP protocol selected: " + protocol); + ctx.pipeline().addLast(HTTP_HANDLER_NAME, handler); + } + + /* + * User can write requests right after the channel is active, while protocol + * negotiation is still in progress. At this stage the pipeline is not ready + * to write http requests, so we must hold them here. + */ + @Override + public void write(ChannelHandlerContext ctx, Object o, ChannelPromise channelPromise) throws Exception { + if (o instanceof HttpMessage) { + HttpUtil.Pair p = HttpUtil.Pair.of(o, channelPromise); + this.bufferedMessages.add(p); + return; + } + + // let non-http message to pass, so the HTTP2 preface and settings frame can be sent + ctx.write(o, channelPromise); + } + + /* + * Protocol negotiation finish, handler removed, the pipeline is + * ready to handle http messages. Write previously buffered http messages. + */ + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + super.handlerRemoved(ctx); + HttpUtil.writeBufferedMessages(ctx.channel(), this.bufferedMessages); + } + + @Override + public void bind(ChannelHandlerContext ctx, SocketAddress socketAddress, ChannelPromise channelPromise) { + ctx.bind(socketAddress, channelPromise); + } + + @Override + public void connect(ChannelHandlerContext ctx, SocketAddress socketAddress, SocketAddress socketAddress1, ChannelPromise channelPromise) throws Exception { + ctx.connect(socketAddress, socketAddress1, channelPromise); + } + + @Override + public void disconnect(ChannelHandlerContext ctx, ChannelPromise channelPromise) { + ctx.disconnect(channelPromise); + } + + @Override + public void close(ChannelHandlerContext ctx, ChannelPromise channelPromise) throws Exception { + ctx.close(channelPromise); + } + + @Override + public void deregister(ChannelHandlerContext ctx, ChannelPromise channelPromise) { + ctx.deregister(channelPromise); + } + + @Override + public void read(ChannelHandlerContext ctx) throws Exception { + ctx.read(); + } + + @Override + public void flush(ChannelHandlerContext ctx) { + ctx.flush(); + } + +} + diff --git a/driver/src/main/java/oracle/nosql/driver/httpclient/HttpUtil.java b/driver/src/main/java/oracle/nosql/driver/httpclient/HttpUtil.java new file mode 100644 index 00000000..7b047243 --- /dev/null +++ b/driver/src/main/java/oracle/nosql/driver/httpclient/HttpUtil.java @@ -0,0 +1,290 @@ +/*- + * Copyright (c) 2011, 2022 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +package oracle.nosql.driver.httpclient; + +import static io.netty.handler.logging.LogLevel.DEBUG; +import static io.netty.handler.codec.http.HttpHeaderNames.HOST; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; +import static io.netty.handler.codec.http.HttpMethod.HEAD; +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; +import static io.netty.handler.codec.http.HttpClientUpgradeHandler.UpgradeEvent; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.Objects; + +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpClientUpgradeHandler; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http2.DefaultHttp2Connection; +import io.netty.handler.codec.http2.DelegatingDecompressorFrameListener; +import io.netty.handler.codec.http2.Http2ClientUpgradeCodec; +import io.netty.handler.codec.http2.Http2Connection; +import io.netty.handler.codec.http2.Http2ConnectionHandler; +import io.netty.handler.codec.http2.Http2FrameLogger; +import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandler; +import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder; +import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.internal.RecyclableArrayList; + +public class HttpUtil { + private static final Http2FrameLogger frameLogger = new Http2FrameLogger(DEBUG, HttpUtil.class); + + private static final String CODEC_HANDLER_NAME = "http-codec"; + private static final String AGG_HANDLER_NAME = "http-aggregator"; + + private static Http2ConnectionHandler createHttp2ConnectionHandler(int maxContentLength) { + Http2Connection connection = new DefaultHttp2Connection(false); + HttpToHttp2ConnectionHandler connectionHandler = new HttpToHttp2ConnectionHandlerBuilder() + .frameListener(new DelegatingDecompressorFrameListener( + connection, + new InboundHttp2ToHttpAdapterBuilder(connection) + .maxContentLength(maxContentLength) + .propagateSettings(false) + .build())) + .frameLogger(frameLogger) + .connection(connection) + .build(); + return connectionHandler; + } + + protected static void removeHttpObjectAggregator(ChannelPipeline p) { + p.remove(AGG_HANDLER_NAME); + } + + protected static void configureHttp1(ChannelPipeline p, int maxChunkSize, int maxContentLength) { + p.addLast(CODEC_HANDLER_NAME, + new HttpClientCodec(4096, // initial line + 8192, // header size + maxChunkSize)); // chunksize + p.addLast(AGG_HANDLER_NAME, + new HttpObjectAggregator(maxContentLength)); + } + + protected static void configureHttp2(ChannelPipeline p, int maxContentLength) { + p.addLast(createHttp2ConnectionHandler(maxContentLength)); + } + + protected static void configureH2C(ChannelPipeline p, int maxChunkSize, int maxContentLength) { + HttpClientCodec sourceCodec = new HttpClientCodec(4096, 8192, maxChunkSize); + Http2ClientUpgradeCodec upgradeCodec = new Http2ClientUpgradeCodec(createHttp2ConnectionHandler(maxContentLength)); + HttpClientUpgradeHandler upgradeHandler = new UpgradeHandler(sourceCodec, upgradeCodec, maxContentLength); + + p.addLast(CODEC_HANDLER_NAME, sourceCodec); + p.addLast(upgradeHandler); + p.addLast(AGG_HANDLER_NAME, new HttpObjectAggregator(maxContentLength)); + p.addLast(new UpgradeRequestHandler()); + } + + protected static void writeBufferedMessages(Channel ch, RecyclableArrayList bufferedMessages) { + if (!bufferedMessages.isEmpty()) { + for(int i = 0; i < bufferedMessages.size(); ++i) { + Pair p = (Pair)bufferedMessages.get(i); + ch.write(p.first, p.second); + } + + ch.flush(); + bufferedMessages.clear(); + } + bufferedMessages.recycle(); + } + + private static final class UpgradeHandler extends HttpClientUpgradeHandler { + public UpgradeHandler(SourceCodec sourceCodec, UpgradeCodec upgradeCodec, int maxContentLength) { + super(sourceCodec, upgradeCodec, maxContentLength); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + super.handlerRemoved(ctx); + ctx.fireUserEventTriggered(UpgradeFinishedEvent.INSTANCE); + } + } + + /** + * A handler that triggers the H2C upgrade to HTTP/2 by sending an initial HTTP1.1 request. + * + */ + private static final class UpgradeRequestHandler extends ChannelDuplexHandler { + private final RecyclableArrayList bufferedMessages = RecyclableArrayList.newInstance(); + private UpgradeEvent upgradeResult = null; + + /** + * In channelActive event, we send a probe request "HEAD / Http1.1 upgrade: h2c" to proxy. + */ + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + DefaultFullHttpRequest upgradeRequest = + new DefaultFullHttpRequest(HTTP_1_1, HEAD, "/", Unpooled.EMPTY_BUFFER); + + // Set HOST header as the remote peer may require it. + InetSocketAddress remote = (InetSocketAddress) ctx.channel().remoteAddress(); + String hostString = remote.getHostString(); + if (hostString == null) { + hostString = remote.getAddress().getHostAddress(); + } + upgradeRequest.headers().set(HOST, hostString + ':' + remote.getPort()); + + ctx.writeAndFlush(upgradeRequest); + + ctx.fireChannelActive(); + } + + /* + * Upgrading, we temporarily hold all user requests. + */ + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (msg instanceof HttpMessage) { + Pair p = Pair.of(msg, promise); + this.bufferedMessages.add(p); + return; + } + + ctx.write(msg, promise); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + // HttpClientUpgradeHandler received the first response from proxy + // Based on the response, it triggers UpgradeEvent, either SUCCEFULL or REJECTED. + if (evt instanceof UpgradeEvent) { + // This can also be UpgradeEvent.UPGRADE_ISSUED + // But it will be overwritten when the first response arrive + upgradeResult = (UpgradeEvent) evt; + // If upgrade is SUCCESSFUL, at this point the Http2ConnectionHandler is installed + // We don't need Aggregator anymore, remove it. + if (upgradeResult == UpgradeEvent.UPGRADE_SUCCESSFUL) { + HttpUtil.removeHttpObjectAggregator(ctx.pipeline()); + } + } + if (evt instanceof UpgradeFinishedEvent && + upgradeResult == UpgradeEvent.UPGRADE_SUCCESSFUL) { + // The HttpClientUpgradeHandler is removed from pipeline + // Upgrade is SUCCESSFUL + // The pipeline is now configured for H2 + // Remove this handler and flush the buffered messages + ctx.pipeline().remove(this); + } + ctx.fireUserEventTriggered(evt); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + // When upgrade is rejected (Old version proxy, does not support Http2) + // the proxy sends a "400 Bad Request" response, drop it here. + // Also remove this handler and flush the buffered messages + if (msg instanceof FullHttpResponse && + upgradeResult == UpgradeEvent.UPGRADE_REJECTED) { + FullHttpResponse rep = (FullHttpResponse) msg; + if (BAD_REQUEST.equals(rep.status())) { + // Just drop the first "400" response, remove this handler + ReferenceCountUtil.release(msg); + ctx.pipeline().remove(this); + return; + } + } + ctx.fireChannelRead(msg); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + super.handlerRemoved(ctx); + writeBufferedMessages(ctx.channel(), this.bufferedMessages); + } + + @Override + public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception { + ctx.bind(localAddress, promise); + } + + @Override + public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) throws Exception { + ctx.connect(remoteAddress, localAddress, promise); + } + + @Override + public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { + ctx.disconnect(promise); + } + + @Override + public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { + ctx.close(promise); + } + + @Override + public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { + ctx.deregister(promise); + } + + @Override + public void read(ChannelHandlerContext ctx) throws Exception { + ctx.read(); + } + + @Override + public void flush(ChannelHandlerContext ctx) throws Exception { + ctx.flush(); + } + } + + public static final class UpgradeFinishedEvent { + private static final UpgradeFinishedEvent INSTANCE = new UpgradeFinishedEvent(); + + private UpgradeFinishedEvent() { + } + } + + public static class Pair { + + public final A first; + public final B second; + + public Pair(A fst, B snd) { + this.first = fst; + this.second = snd; + } + + public String toString() { + return "Pair[" + first + "," + second + "]"; + } + + public boolean equals(Object other) { + if (other instanceof Pair) { + Pair pair = (Pair) other; + return Objects.equals(first, pair.first) && + Objects.equals(second, pair.second); + } + return false; + } + + public int hashCode() { + if (first == null) + return (second == null) ? 0 : second.hashCode() + 1; + else if (second == null) + return first.hashCode() + 2; + else + return first.hashCode() * 17 + second.hashCode(); + } + + public static Pair of(A a, B b) { + return new Pair<>(a, b); + } + } +} diff --git a/driver/src/main/java/oracle/nosql/driver/iam/InstancePrincipalsProvider.java b/driver/src/main/java/oracle/nosql/driver/iam/InstancePrincipalsProvider.java index 07d34c2e..45512160 100644 --- a/driver/src/main/java/oracle/nosql/driver/iam/InstancePrincipalsProvider.java +++ b/driver/src/main/java/oracle/nosql/driver/iam/InstancePrincipalsProvider.java @@ -19,6 +19,7 @@ import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; +import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.logging.Logger; @@ -32,6 +33,7 @@ import oracle.nosql.driver.iam.SecurityTokenSupplier.SecurityTokenBasedProvider; import oracle.nosql.driver.iam.SessionKeyPairSupplier.DefaultSessionKeySupplier; import oracle.nosql.driver.iam.SessionKeyPairSupplier.JDKKeyPairSupplier; +import oracle.nosql.driver.util.HttpConstants; import oracle.nosql.driver.util.HttpRequestUtil; import oracle.nosql.driver.util.HttpRequestUtil.HttpResponse; @@ -298,6 +300,7 @@ private void autoDetectEndpointUsingMetadataUrl() { null, 0, "InstanceMDClient", + Arrays.asList(HttpConstants.HTTP_1_1), logger); HttpResponse response = HttpRequestUtil.doGetRequest diff --git a/driver/src/main/java/oracle/nosql/driver/iam/SecurityTokenSupplier.java b/driver/src/main/java/oracle/nosql/driver/iam/SecurityTokenSupplier.java index cc0280c2..8aebc196 100644 --- a/driver/src/main/java/oracle/nosql/driver/iam/SecurityTokenSupplier.java +++ b/driver/src/main/java/oracle/nosql/driver/iam/SecurityTokenSupplier.java @@ -21,6 +21,7 @@ import java.util.Base64; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Logger; @@ -104,13 +105,16 @@ synchronized void prepare(NoSQLHandleConfig config) { federationClient = buildHttpClient( federationURL, config.getSslContext(), - config.getSSLHandshakeTimeout(), logger); + config.getSSLHandshakeTimeout(), + config.getHttpProtocols(), + logger); } } private static HttpClient buildHttpClient(URI endpoint, SslContext sslCtx, int sslHandshakeTimeout, + List httpProtocols, Logger logger) { String scheme = endpoint.getScheme(); if (scheme == null) { @@ -119,8 +123,8 @@ private static HttpClient buildHttpClient(URI endpoint, endpoint.toString()); } if (scheme.equalsIgnoreCase("http")) { - return HttpClient.createMinimalClient(endpoint.getHost(), endpoint.getPort(), - null, 0, "FederationClient", logger); + return HttpClient.createMinimalClient(endpoint.getHost(), endpoint.getPort(), null, + 0, "FederationClient", httpProtocols, logger); } if (sslCtx == null) { @@ -134,7 +138,7 @@ private static HttpClient buildHttpClient(URI endpoint, return HttpClient.createMinimalClient(endpoint.getHost(), 443, sslCtx, sslHandshakeTimeout, - "FederationClient", logger); + "FederationClient", httpProtocols, logger); } private synchronized String refreshAndGetTokenInternal() { @@ -338,8 +342,8 @@ void validate(long minTokenLifetime) { /** * Checks if two public keys are equal - * @param a one public key - * @param b the other one + * @param actual one public key + * @param expect the other one * @return true if the same */ private boolean isEqualPublicKey(RSAPublicKey actual, diff --git a/driver/src/main/java/oracle/nosql/driver/kv/StoreAccessTokenProvider.java b/driver/src/main/java/oracle/nosql/driver/kv/StoreAccessTokenProvider.java index ade9e1b4..19d52c83 100644 --- a/driver/src/main/java/oracle/nosql/driver/kv/StoreAccessTokenProvider.java +++ b/driver/src/main/java/oracle/nosql/driver/kv/StoreAccessTokenProvider.java @@ -13,6 +13,7 @@ import java.net.URL; import java.util.Arrays; import java.util.Base64; +import java.util.List; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.atomic.AtomicReference; @@ -112,6 +113,11 @@ public class StoreAccessTokenProvider implements AuthorizationProvider { */ private boolean autoRenew = true; + /* + * list of preferred http protocols + */ + private List httpProtocols; + /* * Whether this is a secure store token provider. */ @@ -375,6 +381,16 @@ public StoreAccessTokenProvider setLogger(Logger logger) { return this; } + /** + * Sets Http Protocols + * @param httpProtocols list of preferred http protocols + * @return this + */ + public StoreAccessTokenProvider setHttpProtocols(List httpProtocols) { + this.httpProtocols = httpProtocols; + return this; + } + public String getEndpoint() { return endpoint; } @@ -486,6 +502,7 @@ private HttpResponse sendRequest(String authHeader, (isSecure && !disableSSLHook) ? sslContext : null, sslHandshakeTimeoutMs, serviceName, + httpProtocols, logger); return HttpRequestUtil.doGetRequest( client, diff --git a/driver/src/main/java/oracle/nosql/driver/util/HttpConstants.java b/driver/src/main/java/oracle/nosql/driver/util/HttpConstants.java index 906f201d..5f352b04 100644 --- a/driver/src/main/java/oracle/nosql/driver/util/HttpConstants.java +++ b/driver/src/main/java/oracle/nosql/driver/util/HttpConstants.java @@ -14,6 +14,16 @@ */ public class HttpConstants { + /** + * {@code "h2"}: HTTP version 2 + */ + public static final String HTTP_2 = "h2"; + + /** + * {@code "http/1.1"}: HTTP version 1.1 + */ + public static final String HTTP_1_1 = "http/1.1"; + /** * The http header that identifies the client scoped unique request id * associated with each request. The request header is returned by the diff --git a/driver/src/test/java/oracle/nosql/driver/ProxyTestBase.java b/driver/src/test/java/oracle/nosql/driver/ProxyTestBase.java index 0f69b449..0e927c48 100644 --- a/driver/src/test/java/oracle/nosql/driver/ProxyTestBase.java +++ b/driver/src/test/java/oracle/nosql/driver/ProxyTestBase.java @@ -52,6 +52,7 @@ import oracle.nosql.driver.ops.TableResult; import oracle.nosql.driver.ops.WriteMultipleRequest; import oracle.nosql.driver.ops.WriteMultipleResult; +import oracle.nosql.driver.util.HttpConstants; import oracle.nosql.driver.values.ArrayValue; import oracle.nosql.driver.values.MapValue; @@ -464,6 +465,15 @@ protected NoSQLHandle getHandle(NoSQLHandleConfig config) { logger.setLevel(Level.parse(level)); config.setLogger(logger); + boolean useHttp1only = Boolean.getBoolean("test.http1only"); + if (useHttp1only) { + config.setHttpProtocols(HttpConstants.HTTP_1_1); + } + boolean useHttp2only = Boolean.getBoolean("test.http2only"); + if (useHttp2only) { + config.setHttpProtocols(HttpConstants.HTTP_2); + } + /* * Open the handle */ diff --git a/driver/src/test/java/oracle/nosql/driver/httpclient/ConnectionPoolTest.java b/driver/src/test/java/oracle/nosql/driver/httpclient/ConnectionPoolTest.java index e43ffeb1..ec801974 100644 --- a/driver/src/test/java/oracle/nosql/driver/httpclient/ConnectionPoolTest.java +++ b/driver/src/test/java/oracle/nosql/driver/httpclient/ConnectionPoolTest.java @@ -9,6 +9,7 @@ import static org.junit.Assert.assertEquals; +import java.util.Arrays; import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; @@ -24,6 +25,7 @@ import io.netty.handler.ssl.SslContextBuilder; import oracle.nosql.driver.NoSQLHandleConfig; +import oracle.nosql.driver.util.HttpConstants; /** * This test is excluded from the test profiles and must be run standalone. @@ -71,6 +73,7 @@ public void poolTest() throws Exception { null, // sslCtx 0, // ssl handshake timeout "Pool Test", + Arrays.asList(HttpConstants.HTTP_1_1), logger); ConnectionPool pool = client.getConnectionPool(); @@ -170,6 +173,7 @@ public void testCloudTimeout() throws Exception { buildSslContext(), 0, // ssl handshake timeout "Pool Cloud Test", + Arrays.asList(HttpConstants.HTTP_1_1), logger); ConnectionPool pool = client.getConnectionPool();