Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/http2 #32

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
7 changes: 6 additions & 1 deletion driver/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
<copyright>Copyright (c) 2011, 2022 Oracle and/or its affiliates. All rights reserved.</copyright>
<java.apidoc>http://docs.oracle.com/javase/8/docs/api</java.apidoc>
<maven.deploy.skip>false</maven.deploy.skip>
<netty.version>4.1.77.Final</netty.version>
<netty.version>4.1.82.Final</netty.version>
<jackson.version>2.12.5</jackson.version>
<bouncy.version>1.70</bouncy.version>
<!-- by default, skip tests; tests require a profile -->
Expand Down Expand Up @@ -175,6 +175,11 @@
<artifactId>netty-codec-http</artifactId>
<version>${netty.version}</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec-http2</artifactId>
<version>${netty.version}</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-handler</artifactId>
Expand Down
33 changes: 33 additions & 0 deletions driver/src/main/java/oracle/nosql/driver/NoSQLHandleConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import oracle.nosql.driver.Region.RegionProvider;
import oracle.nosql.driver.iam.SignatureProvider;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.SslContext;

/**
Expand Down Expand Up @@ -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<String> httpProtocols = new ArrayList<>(Arrays.asList(ApplicationProtocolNames.HTTP_2, ApplicationProtocolNames.HTTP_1_1));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we may need to use ApplicationProtocolNames when talking to Netty there should be no netty class or constant that is part of our own public API. We need to provide our own constants to represent http/2 and 1.1 and map them internally (probably not in this class) to netty when needed


/**
* A RetryHandler, or null if not configured by the user.
*/
Expand Down Expand Up @@ -553,6 +561,23 @@ public int getDefaultRequestTimeout() {
return timeout == 0 ? DEFAULT_TIMEOUT : timeout;
}

/**
*
* @return Http protocol settings
Copy link
Member

@gmfeinberg gmfeinberg Oct 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs an actual explanation of what's being returned

*/
public List<String> getHttpProtocols() {
return httpProtocols;
}

/**
* Check if "h2" is in the protocols list
*
* @return true if "h2" is in the protocols list
*/
public boolean useHttp2() {
Copy link
Member

@gmfeinberg gmfeinberg Oct 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking that the list would be ordered by preference. While http1.1, http2 doesn't necessarily make sense that would be the logic. Also it doesn't seem that this method should be available to the public. Minimally it should be hidden. Or, just remove it (preferable) and do the logic in the lower level code

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The list is order irrelevant.
In the ALPN case, if we provide both H2 and H1.1 (no matter what order), the server always pick H2 over H1.1 given the server supports H2.
In the clear text case, when we provide both H2 and H1.1 to the config, it's not like we try H2 first for 30 seconds timeout then fall back to H1.1, instead we directly use H2C protocol, so the order of the protocols is not relevant either.
For the visibility, sure I will remove this method, and do the logic in the lower level code.

return this.httpProtocols.contains(ApplicationProtocolNames.HTTP_2);
}

/**
* Returns the configured table request timeout value, in milliseconds.
* The table request timeout default can be specified independently to allow
Expand Down Expand Up @@ -631,6 +656,14 @@ public NoSQLHandleConfig setRequestTimeout(int timeout) {
return this;
}

public NoSQLHandleConfig setHttpProtocols(String ... protocols) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs javadoc comment

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
Expand Down
1 change: 1 addition & 0 deletions driver/src/main/java/oracle/nosql/driver/http/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ public Client(Logger logger,
sslCtx,
config.getSSLHandshakeTimeout(),
"NoSQL Driver",
config.getHttpProtocols(),
logger);
if (httpConfig.getProxyHost() != null) {
httpClient.configureProxy(httpConfig);
Expand Down
13 changes: 13 additions & 0 deletions driver/src/main/java/oracle/nosql/driver/http/NoSQLHandleImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@

import javax.net.ssl.SSLException;

import io.netty.handler.codec.http2.Http2SecurityUtil;
import io.netty.handler.ssl.ApplicationProtocolConfig;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.SupportedCipherSuiteFilter;
import oracle.nosql.driver.AuthorizationProvider;
import oracle.nosql.driver.NoSQLHandle;
import oracle.nosql.driver.NoSQLHandleConfig;
Expand Down Expand Up @@ -124,6 +128,14 @@ private void configSslContext(NoSQLHandleConfig config) {
}
builder.sessionTimeout(config.getSSLSessionTimeout());
builder.sessionCacheSize(config.getSSLSessionCacheSize());
if (config.useHttp2()) {
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(
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,6 +34,7 @@
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.SslContext;
import io.netty.util.AttributeKey;
import io.netty.util.concurrent.Future;
Expand Down Expand Up @@ -96,6 +100,8 @@ public class HttpClient {
private final String host;
private final int port;
private final String name;
private final List<String> httpProtocols;
private final String httpFallbackProtocol;

/*
* Amount of time to wait for acquiring a channel before timing
Expand Down Expand Up @@ -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<String> httpProtocols,
Logger logger) {
return new HttpClient(host,
port,
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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,
Expand All @@ -189,11 +198,12 @@ public HttpClient(String host,
SslContext sslCtx,
int handshakeTimeoutMs,
String name,
List<String> 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);
}

/*
Expand All @@ -210,6 +220,7 @@ private HttpClient(String host,
SslContext sslCtx,
int handshakeTimeoutMs,
String name,
List<String> httpProtocols,
Logger logger) {

this.logger = logger;
Expand All @@ -218,6 +229,15 @@ private HttpClient(String host,
this.port = port;
this.name = name;

this.httpProtocols = httpProtocols.size() > 0 ?
httpProtocols :
new ArrayList<>(Arrays.asList(ApplicationProtocolNames.HTTP_2, ApplicationProtocolNames.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(ApplicationProtocolNames.HTTP_1_1) ?
ApplicationProtocolNames.HTTP_1_1 : this.httpProtocols.get(this.httpProtocols.size() - 1);

this.maxContentLength = (maxContentLength == 0 ?
DEFAULT_MAX_CONTENT_LENGTH : maxContentLength);
this.maxChunkSize = (maxChunkSize == 0 ?
Expand Down Expand Up @@ -292,6 +312,14 @@ String getName() {
return name;
}

List<String> getHttpProtocols() {
return httpProtocols;
}

public String getHttpFallbackProtocol() {
return httpFallbackProtocol;
}

Logger getLogger() {
return logger;
}
Expand Down Expand Up @@ -354,6 +382,15 @@ public int getFreeChannelCount() {
return pool.getFreeChannels();
}

/**
* Check if "h2" is in the protocols list
*
* @return true if "h2" is in the protocols list
*/
public boolean useHttp2() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again.. shouldn't the list be ordered by preference?

return this.httpProtocols.contains(ApplicationProtocolNames.HTTP_2);
}

/* available for testing */
ConnectionPool getConnectionPool() {
return pool;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -21,9 +22,8 @@
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.ApplicationProtocolNames;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.ssl.SslHandshakeCompletionEvent;
import io.netty.util.concurrent.Future;
Expand All @@ -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;

/**
Expand All @@ -53,6 +49,62 @@ 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());

// Only true when HTTP_2 is the only protocol, as if user set:
// config.setHttpProtocols(ApplicationProtocolNames.HTTP_2);
if (client.useHttp2() &&
ApplicationProtocolNames.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(ApplicationProtocolNames.HTTP_1_1);
if (!client.useHttp2() &&
ApplicationProtocolNames.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(ApplicationProtocolNames.HTTP_2,
// ApplicationProtocolNames.HTTP_1_1)
if (client.useHttp2() &&
ApplicationProtocolNames.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
Expand All @@ -67,28 +119,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 =
Expand All @@ -101,7 +136,7 @@ public void channelCreated(Channel ch) {
client.getProxyUsername(),
client.getProxyPassword());

p.addFirst("proxyServer", proxyHandler);
ch.pipeline().addFirst("proxyServer", proxyHandler);
}
}

Expand Down
Loading