Skip to content

Commit

Permalink
Support h2c direct connections
Browse files Browse the repository at this point in the history
  • Loading branch information
vietj committed Apr 4, 2016
1 parent cc30aa9 commit aa19632
Show file tree
Hide file tree
Showing 12 changed files with 401 additions and 104 deletions.
5 changes: 5 additions & 0 deletions src/main/asciidoc/dataobjects.adoc
Expand Up @@ -1362,6 +1362,11 @@ Set the default port to be used by this client in requests if none is provided w
+++
Add an enabled cipher suite
+++
|[[h2cUpgrade]]`h2cUpgrade`|`Boolean`|
+++
Set to <code>true</code> when an <i>h2c</i> connection is established using an HTTP/1.1 upgrade request, and <code>false</code>
when an <i>h2c</i> connection is established directly (with prior knowledge).
+++
|[[idleTimeout]]`idleTimeout`|`Number (int)`|
+++
Set the idle timeout, in seconds. zero means don't timeout.
Expand Down
7 changes: 6 additions & 1 deletion src/main/asciidoc/java/http.adoc
Expand Up @@ -52,7 +52,7 @@ ALPN will usually agree on the `h2` protocol, although `http/1.1` can be used if
so.

To handle `h2c` requests, TLS must be disabled, the server will upgrade to HTTP/2 any request HTTP/1.1 that wants to
upgrade to HTTP/2.
upgrade to HTTP/2. It will also accept a direct `h2c` connection beginning with the `PRI * HTTP/2.0\r\nSM\r\n` preface.

WARNING: most browsers won't support `h2c`, so for serving web sites you should use `h2` and not `h2c`.

Expand Down Expand Up @@ -809,6 +809,11 @@ HttpClientOptions options = new HttpClientOptions().setProtocolVersion(HttpVersi
HttpClient client = vertx.createHttpClient(options);
----

`h2c` connections can also be established directly, i.e connection started with a prior knowledge, when
`link:../../apidocs/io/vertx/core/http/HttpClientOptions.html#setH2cUpgrade-boolean-[setH2cUpgrade]` options is set to false: after the
connection is established, the client will send the HTTP/2 connection preface and expect to receive
the same preface from the server.

The http server may not support HTTP/2, the actual version can be checked
with `link:../../apidocs/io/vertx/core/http/HttpClientResponse.html#version--[version]` when the response arrives.

Expand Down
Expand Up @@ -41,6 +41,9 @@ public static void fromJson(JsonObject json, HttpClientOptions obj) {
if (json.getValue("defaultPort") instanceof Number) {
obj.setDefaultPort(((Number)json.getValue("defaultPort")).intValue());
}
if (json.getValue("h2cUpgrade") instanceof Boolean) {
obj.setH2cUpgrade((Boolean)json.getValue("h2cUpgrade"));
}
if (json.getValue("initialSettings") instanceof JsonObject) {
obj.setInitialSettings(new io.vertx.core.http.Http2Settings((JsonObject)json.getValue("initialSettings")));
}
Expand Down Expand Up @@ -85,6 +88,7 @@ public static void toJson(HttpClientOptions obj, JsonObject json) {
json.put("defaultHost", obj.getDefaultHost());
}
json.put("defaultPort", obj.getDefaultPort());
json.put("h2cUpgrade", obj.isH2cUpgrade());
if (obj.getInitialSettings() != null) {
json.put("initialSettings", obj.getInitialSettings().toJson());
}
Expand Down
31 changes: 31 additions & 0 deletions src/main/java/io/vertx/core/http/HttpClientOptions.java
Expand Up @@ -99,6 +99,11 @@ public class HttpClientOptions extends ClientOptionsBase {
*/
public static final List<HttpVersion> DEFAULT_ALPN_VERSIONS = Collections.emptyList();

/**
* Default using HTTP/1.1 upgrade for establishing an <i>h2C</i> connection = true
*/
public static final boolean DEFAULT_H2C_UPGRADE = true;

private boolean verifyHost = true;
private int maxPoolSize;
private boolean keepAlive;
Expand All @@ -112,6 +117,7 @@ public class HttpClientOptions extends ClientOptionsBase {
private int maxWaitQueueSize;
private Http2Settings initialSettings;
private List<HttpVersion> alpnVersions;
private boolean h2cUpgrade;

/**
* Default constructor
Expand Down Expand Up @@ -141,6 +147,7 @@ public HttpClientOptions(HttpClientOptions other) {
this.maxWaitQueueSize = other.maxWaitQueueSize;
this.initialSettings = other.initialSettings != null ? new Http2Settings(other.initialSettings) : null;
this.alpnVersions = other.alpnVersions != null ? new ArrayList<>(other.alpnVersions) : null;
this.h2cUpgrade = other.h2cUpgrade;
}

/**
Expand Down Expand Up @@ -168,6 +175,7 @@ private void init() {
maxWaitQueueSize = DEFAULT_MAX_WAIT_QUEUE_SIZE;
initialSettings = new Http2Settings();
alpnVersions = new ArrayList<>(DEFAULT_ALPN_VERSIONS);
h2cUpgrade = DEFAULT_H2C_UPGRADE;
}

@Override
Expand Down Expand Up @@ -564,6 +572,25 @@ public HttpClientOptions setAlpnVersions(List<HttpVersion> alpnVersions) {
return this;
}

/**
* @return true when an <i>h2c</i> connection is established using an HTTP/1.1 upgrade request, false when directly
*/
public boolean isH2cUpgrade() {
return h2cUpgrade;
}

/**
* Set to {@code true} when an <i>h2c</i> connection is established using an HTTP/1.1 upgrade request, and {@code false}
* when an <i>h2c</i> connection is established directly (with prior knowledge).
*
* @param value the upgrade value
* @return a reference to this, so the API can be used fluently
*/
public HttpClientOptions setH2cUpgrade(boolean value) {
this.h2cUpgrade = value;
return this;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand All @@ -584,6 +611,8 @@ public boolean equals(Object o) {
if (maxChunkSize != that.maxChunkSize) return false;
if (maxWaitQueueSize != that.maxWaitQueueSize) return false;
if (initialSettings == null ? that.initialSettings != null : !initialSettings.equals(that.initialSettings)) return false;
if (alpnVersions == null ? that.alpnVersions != null : !alpnVersions.equals(that.alpnVersions)) return false;
if (h2cUpgrade != that.h2cUpgrade) return false;

return true;
}
Expand All @@ -603,6 +632,8 @@ public int hashCode() {
result = 31 * result + maxChunkSize;
result = 31 * result + maxWaitQueueSize;
result = 31 * result + (initialSettings != null ? initialSettings.hashCode() : 0);
result = 31 * result + (alpnVersions != null ? alpnVersions.hashCode() : 0);
result = 31 * result + (h2cUpgrade ? 1 : 0);
return result;
}
}
Expand Down
16 changes: 10 additions & 6 deletions src/main/java/io/vertx/core/http/HttpServerOptions.java
Expand Up @@ -69,6 +69,9 @@ public class HttpServerOptions extends NetServerOptions {
*/
public static final boolean DEFAULT_HANDLE_100_CONTINE_AUTOMATICALLY = false;

/**
* Default Application-Layer Protocol Negotiation versions = [HTTP/2,HTTP/1.1]
*/
public static final List<HttpVersion> DEFAULT_ALPN_VERSIONS = Collections.unmodifiableList(Arrays.asList(HttpVersion.HTTP_2, HttpVersion.HTTP_1_1));

private boolean compressionSupported;
Expand Down Expand Up @@ -197,12 +200,6 @@ public HttpServerOptions setUseAlpn(boolean useAlpn) {
return this;
}

@Override
public HttpServerOptions setSslEngine(SSLEngine sslEngine) {
super.setSslEngine(sslEngine);
return this;
}

@Override
public HttpServerOptions setKeyStoreOptions(JksOptions options) {
super.setKeyStoreOptions(options);
Expand Down Expand Up @@ -281,6 +278,11 @@ public HttpServerOptions setClientAuth(ClientAuth clientAuth) {
return this;
}

@Override
public HttpServerOptions setSslEngine(SSLEngine sslEngine) {
super.setSslEngine(sslEngine);
return this;
}

/**
* @return true if the server supports compression
Expand Down Expand Up @@ -459,6 +461,7 @@ public boolean equals(Object o) {
if (maxInitialLineLength != that.maxInitialLineLength) return false;
if (maxHeaderSize != that.maxHeaderSize) return false;
if (initialSettings == null ? that.initialSettings != null : !initialSettings.equals(that.initialSettings)) return false;
if (alpnVersions == null ? that.alpnVersions != null : !alpnVersions.equals(that.alpnVersions)) return false;
return !(websocketSubProtocols != null ? !websocketSubProtocols.equals(that.websocketSubProtocols) : that.websocketSubProtocols != null);
}

Expand All @@ -473,6 +476,7 @@ public int hashCode() {
result = 31 * result + maxChunkSize;
result = 31 * result + maxInitialLineLength;
result = 31 * result + maxHeaderSize;
result = 31 * result + (alpnVersions != null ? alpnVersions.hashCode() : 0);
return result;
}
}
66 changes: 40 additions & 26 deletions src/main/java/io/vertx/core/http/impl/ConnectionManager.java
Expand Up @@ -262,34 +262,38 @@ protected void handshakeFailure(ChannelHandlerContext ctx, Throwable cause) thro
pipeline.addLast("ssl", sslHelper.createSslHandler(vertx, host, port));
}
if (options.getProtocolVersion() == HttpVersion.HTTP_2) {
HttpClientCodec httpCodec = new HttpClientCodec();
class UpgradeRequestHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
DefaultFullHttpRequest upgradeRequest =
new DefaultFullHttpRequest(io.netty.handler.codec.http.HttpVersion.HTTP_1_1, HttpMethod.GET, "/");
ctx.writeAndFlush(upgradeRequest);
ctx.fireChannelActive();
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
super.userEventTriggered(ctx, evt);
ChannelPipeline p = ctx.pipeline();
if (evt == HttpClientUpgradeHandler.UpgradeEvent.UPGRADE_SUCCESSFUL) {
p.remove(this);
// Upgrade handler will remove itself
http2Connected(context, ch, waiter, true);
} else if (evt == HttpClientUpgradeHandler.UpgradeEvent.UPGRADE_REJECTED) {
p.remove(httpCodec);
p.remove(this);
// Upgrade handler will remove itself
fallbackToHttp1x(ch, context, HttpVersion.HTTP_1_1, port, host, waiter);
if (options.isH2cUpgrade()) {
HttpClientCodec httpCodec = new HttpClientCodec();
class UpgradeRequestHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
DefaultFullHttpRequest upgradeRequest =
new DefaultFullHttpRequest(io.netty.handler.codec.http.HttpVersion.HTTP_1_1, HttpMethod.GET, "/");
ctx.writeAndFlush(upgradeRequest);
ctx.fireChannelActive();
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
super.userEventTriggered(ctx, evt);
ChannelPipeline p = ctx.pipeline();
if (evt == HttpClientUpgradeHandler.UpgradeEvent.UPGRADE_SUCCESSFUL) {
p.remove(this);
// Upgrade handler will remove itself
http2Connected(context, ch, waiter, true);
} else if (evt == HttpClientUpgradeHandler.UpgradeEvent.UPGRADE_REJECTED) {
p.remove(httpCodec);
p.remove(this);
// Upgrade handler will remove itself
fallbackToHttp1x(ch, context, HttpVersion.HTTP_1_1, port, host, waiter);
}
}
}
VertxHttp2ClientUpgradeCodec upgradeCodec = new VertxHttp2ClientUpgradeCodec(client.getOptions().getInitialSettings());
HttpClientUpgradeHandler upgradeHandler = new HttpClientUpgradeHandler(httpCodec, upgradeCodec, 65536);
ch.pipeline().addLast(httpCodec, upgradeHandler, new UpgradeRequestHandler());
} else {
applyH2ConnectionOptions(pipeline, context);
}
VertxHttp2ClientUpgradeCodec upgradeCodec = new VertxHttp2ClientUpgradeCodec(client.getOptions().getInitialSettings());
HttpClientUpgradeHandler upgradeHandler = new HttpClientUpgradeHandler(httpCodec, upgradeCodec, 65536);
ch.pipeline().addLast(httpCodec, upgradeHandler, new UpgradeRequestHandler());
} else {
applyHttp1xConnectionOptions(pipeline, context);
}
Expand Down Expand Up @@ -317,7 +321,11 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc
if (ch.pipeline().get(HttpClientUpgradeHandler.class) != null) {
// Upgrade handler do nothing
} else {
http1xConnected(options.getProtocolVersion(), context, port, host, ch, waiter);
if (options.getProtocolVersion() == HttpVersion.HTTP_2 && !options.isH2cUpgrade()) {
http2Connected(context, ch, waiter, false);
} else {
http1xConnected(options.getProtocolVersion(), context, port, host, ch, waiter);
}
}
}
}
Expand Down Expand Up @@ -632,6 +640,12 @@ void applyConnectionOptions(HttpClientOptions options, Bootstrap bootstrap) {
bootstrap.option(ChannelOption.SO_REUSEADDR, options.isReuseAddress());
}

void applyH2ConnectionOptions(ChannelPipeline pipeline, ContextImpl context) {
if (options.getIdleTimeout() > 0) {
pipeline.addLast("idle", new IdleStateHandler(0, 0, options.getIdleTimeout()));
}
}

void applyHttp1xConnectionOptions(ChannelPipeline pipeline, ContextImpl context) {
pipeline.addLast("codec", new HttpClientCodec(4096, 8192, options.getMaxChunkSize(), false, false));
if (options.isTryUseCompression()) {
Expand Down
82 changes: 73 additions & 9 deletions src/main/java/io/vertx/core/http/impl/HttpServerImpl.java
Expand Up @@ -17,6 +17,7 @@
package io.vertx.core.http.impl;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.group.ChannelGroup;
Expand Down Expand Up @@ -81,6 +82,7 @@ public class HttpServerImpl implements HttpServer, Closeable, MetricsProvider {
private static final String DISABLE_H2C_PROP_NAME = "vertx.disableH2c";
private final boolean DISABLE_HC2 = Boolean.getBoolean(DISABLE_H2C_PROP_NAME);
private static final String[] H2C_HANDLERS_TO_REMOVE = { "idle", "flashpolicy", "deflater", "chunkwriter" };
private static final byte[] HTTP_2_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes();

private final HttpServerOptions options;
private final VertxInternal vertx;
Expand Down Expand Up @@ -227,22 +229,19 @@ protected void configurePipeline(ChannelHandlerContext ctx, String protocol) thr
if (protocol.equals("http/1.1")) {
configureHttp1(pipeline);
} else {
HandlerHolder<HttpHandler> holder = reqHandlerManager.chooseHandler(ch.eventLoop());
VertxHttp2ConnectionHandler<Http2ServerConnection> handler = createHttp2Handler(holder, ch);
configureHttp2(pipeline, handler);
if (holder.handler.connectionHandler != null) {
holder.context.executeFromIO(() -> {
holder.handler.connectionHandler.handle(handler.connection);
});
}
handleHttp2(ch);
}
}
});
} else {
configureHttp1(pipeline);
}
} else {
configureHttp1(pipeline);
if (DISABLE_HC2) {
configureHttp1(pipeline);
} else {
pipeline.addLast(new Http1xOrHttp2Handler());
}
}
}
});
Expand Down Expand Up @@ -328,6 +327,17 @@ private void configureHttp1(ChannelPipeline pipeline) {
pipeline.addLast("handler", new ServerHandler());
}

public void handleHttp2(Channel ch) {
HandlerHolder<HttpHandler> holder = reqHandlerManager.chooseHandler(ch.eventLoop());
VertxHttp2ConnectionHandler<Http2ServerConnection> handler = createHttp2Handler(holder, ch);
configureHttp2(ch.pipeline(), handler);
if (holder.handler.connectionHandler != null) {
holder.context.executeFromIO(() -> {
holder.handler.connectionHandler.handle(handler.connection);
});
}
}

public void configureHttp2(ChannelPipeline pipeline, VertxHttp2ConnectionHandler<Http2ServerConnection> handler) {
if (options.getIdleTimeout() > 0) {
pipeline.addLast("idle", new IdleStateHandler(0, 0, options.getIdleTimeout()));
Expand Down Expand Up @@ -930,4 +940,58 @@ public int hashCode() {
return result;
}
}

/**
* Handler that detects whether the HTTP/2 connection preface or just process the request
* with the HTTP 1.x pipeline to support H2C with prior knowledge, i.e a client that connects
* and uses HTTP/2 in clear text directly without an HTTP upgrade.
*/
private class Http1xOrHttp2Handler extends ChannelInboundHandlerAdapter {

private int index = 0;

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
int len = buf.readableBytes();
for (int i = index;i < len;i++) {
if (i == HTTP_2_PREFACE.length) {
// H2C
http2(ctx, buf);
break;
} else {
if (buf.getByte(i) != HTTP_2_PREFACE[i]) {
http1(ctx, buf);
break;
}
}
}
}

private void http2(ChannelHandlerContext ctx, ByteBuf buf) {
ByteBuf msg;
if (index > 0) {
msg = Unpooled.buffer(index + buf.readableBytes());
msg.setBytes(0, HTTP_2_PREFACE, 0, index);
msg.setBytes(index, buf);
buf = msg;
}
handleHttp2(ctx.channel());
ctx.fireChannelRead(buf);
ctx.pipeline().remove(this);
}

private void http1(ChannelHandlerContext ctx, ByteBuf buf) {
ByteBuf msg;
if (index > 0) {
msg = Unpooled.buffer(index + buf.readableBytes());
msg.setBytes(0, HTTP_2_PREFACE, 0, index);
msg.setBytes(index, buf);
buf = msg;
}
configureHttp1(ctx.pipeline());
ctx.fireChannelRead(buf);
ctx.pipeline().remove(this);
}
}
}

0 comments on commit aa19632

Please sign in to comment.