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