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 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)`| |[[idleTimeout]]`idleTimeout`|`Number (int)`|
+++ +++
Set the idle timeout, in seconds. zero means don't timeout. 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. so.


To handle `h2c` requests, TLS must be disabled, the server will upgrade to HTTP/2 any request HTTP/1.1 that wants to 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`. 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); 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 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. 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) { if (json.getValue("defaultPort") instanceof Number) {
obj.setDefaultPort(((Number)json.getValue("defaultPort")).intValue()); 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) { if (json.getValue("initialSettings") instanceof JsonObject) {
obj.setInitialSettings(new io.vertx.core.http.Http2Settings((JsonObject)json.getValue("initialSettings"))); 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("defaultHost", obj.getDefaultHost());
} }
json.put("defaultPort", obj.getDefaultPort()); json.put("defaultPort", obj.getDefaultPort());
json.put("h2cUpgrade", obj.isH2cUpgrade());
if (obj.getInitialSettings() != null) { if (obj.getInitialSettings() != null) {
json.put("initialSettings", obj.getInitialSettings().toJson()); 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(); 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 boolean verifyHost = true;
private int maxPoolSize; private int maxPoolSize;
private boolean keepAlive; private boolean keepAlive;
Expand All @@ -112,6 +117,7 @@ public class HttpClientOptions extends ClientOptionsBase {
private int maxWaitQueueSize; private int maxWaitQueueSize;
private Http2Settings initialSettings; private Http2Settings initialSettings;
private List<HttpVersion> alpnVersions; private List<HttpVersion> alpnVersions;
private boolean h2cUpgrade;


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


@Override @Override
Expand Down Expand Up @@ -564,6 +572,25 @@ public HttpClientOptions setAlpnVersions(List<HttpVersion> alpnVersions) {
return this; 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 @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
Expand All @@ -584,6 +611,8 @@ public boolean equals(Object o) {
if (maxChunkSize != that.maxChunkSize) return false; if (maxChunkSize != that.maxChunkSize) return false;
if (maxWaitQueueSize != that.maxWaitQueueSize) return false; if (maxWaitQueueSize != that.maxWaitQueueSize) return false;
if (initialSettings == null ? that.initialSettings != null : !initialSettings.equals(that.initialSettings)) 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; return true;
} }
Expand All @@ -603,6 +632,8 @@ public int hashCode() {
result = 31 * result + maxChunkSize; result = 31 * result + maxChunkSize;
result = 31 * result + maxWaitQueueSize; result = 31 * result + maxWaitQueueSize;
result = 31 * result + (initialSettings != null ? initialSettings.hashCode() : 0); result = 31 * result + (initialSettings != null ? initialSettings.hashCode() : 0);
result = 31 * result + (alpnVersions != null ? alpnVersions.hashCode() : 0);
result = 31 * result + (h2cUpgrade ? 1 : 0);
return result; 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; 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)); public static final List<HttpVersion> DEFAULT_ALPN_VERSIONS = Collections.unmodifiableList(Arrays.asList(HttpVersion.HTTP_2, HttpVersion.HTTP_1_1));


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


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

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


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


/** /**
* @return true if the server supports compression * @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 (maxInitialLineLength != that.maxInitialLineLength) return false;
if (maxHeaderSize != that.maxHeaderSize) return false; if (maxHeaderSize != that.maxHeaderSize) return false;
if (initialSettings == null ? that.initialSettings != null : !initialSettings.equals(that.initialSettings)) 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); 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 + maxChunkSize;
result = 31 * result + maxInitialLineLength; result = 31 * result + maxInitialLineLength;
result = 31 * result + maxHeaderSize; result = 31 * result + maxHeaderSize;
result = 31 * result + (alpnVersions != null ? alpnVersions.hashCode() : 0);
return result; 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)); pipeline.addLast("ssl", sslHelper.createSslHandler(vertx, host, port));
} }
if (options.getProtocolVersion() == HttpVersion.HTTP_2) { if (options.getProtocolVersion() == HttpVersion.HTTP_2) {
HttpClientCodec httpCodec = new HttpClientCodec(); if (options.isH2cUpgrade()) {
class UpgradeRequestHandler extends ChannelInboundHandlerAdapter { HttpClientCodec httpCodec = new HttpClientCodec();
@Override class UpgradeRequestHandler extends ChannelInboundHandlerAdapter {
public void channelActive(ChannelHandlerContext ctx) throws Exception { @Override
DefaultFullHttpRequest upgradeRequest = public void channelActive(ChannelHandlerContext ctx) throws Exception {
new DefaultFullHttpRequest(io.netty.handler.codec.http.HttpVersion.HTTP_1_1, HttpMethod.GET, "/"); DefaultFullHttpRequest upgradeRequest =
ctx.writeAndFlush(upgradeRequest); new DefaultFullHttpRequest(io.netty.handler.codec.http.HttpVersion.HTTP_1_1, HttpMethod.GET, "/");
ctx.fireChannelActive(); ctx.writeAndFlush(upgradeRequest);
} ctx.fireChannelActive();
@Override }
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { @Override
super.userEventTriggered(ctx, evt); public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
ChannelPipeline p = ctx.pipeline(); super.userEventTriggered(ctx, evt);
if (evt == HttpClientUpgradeHandler.UpgradeEvent.UPGRADE_SUCCESSFUL) { ChannelPipeline p = ctx.pipeline();
p.remove(this); if (evt == HttpClientUpgradeHandler.UpgradeEvent.UPGRADE_SUCCESSFUL) {
// Upgrade handler will remove itself p.remove(this);
http2Connected(context, ch, waiter, true); // Upgrade handler will remove itself
} else if (evt == HttpClientUpgradeHandler.UpgradeEvent.UPGRADE_REJECTED) { http2Connected(context, ch, waiter, true);
p.remove(httpCodec); } else if (evt == HttpClientUpgradeHandler.UpgradeEvent.UPGRADE_REJECTED) {
p.remove(this); p.remove(httpCodec);
// Upgrade handler will remove itself p.remove(this);
fallbackToHttp1x(ch, context, HttpVersion.HTTP_1_1, port, host, waiter); // 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 { } else {
applyHttp1xConnectionOptions(pipeline, context); 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) { if (ch.pipeline().get(HttpClientUpgradeHandler.class) != null) {
// Upgrade handler do nothing // Upgrade handler do nothing
} else { } 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()); 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) { void applyHttp1xConnectionOptions(ChannelPipeline pipeline, ContextImpl context) {
pipeline.addLast("codec", new HttpClientCodec(4096, 8192, options.getMaxChunkSize(), false, false)); pipeline.addLast("codec", new HttpClientCodec(4096, 8192, options.getMaxChunkSize(), false, false));
if (options.isTryUseCompression()) { 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; package io.vertx.core.http.impl;


import io.netty.bootstrap.ServerBootstrap; import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import io.netty.channel.*; import io.netty.channel.*;
import io.netty.channel.group.ChannelGroup; 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 static final String DISABLE_H2C_PROP_NAME = "vertx.disableH2c";
private final boolean DISABLE_HC2 = Boolean.getBoolean(DISABLE_H2C_PROP_NAME); 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 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 HttpServerOptions options;
private final VertxInternal vertx; 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")) { if (protocol.equals("http/1.1")) {
configureHttp1(pipeline); configureHttp1(pipeline);
} else { } else {
HandlerHolder<HttpHandler> holder = reqHandlerManager.chooseHandler(ch.eventLoop()); handleHttp2(ch);
VertxHttp2ConnectionHandler<Http2ServerConnection> handler = createHttp2Handler(holder, ch);
configureHttp2(pipeline, handler);
if (holder.handler.connectionHandler != null) {
holder.context.executeFromIO(() -> {
holder.handler.connectionHandler.handle(handler.connection);
});
}
} }
} }
}); });
} else { } else {
configureHttp1(pipeline); configureHttp1(pipeline);
} }
} else { } 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()); 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) { public void configureHttp2(ChannelPipeline pipeline, VertxHttp2ConnectionHandler<Http2ServerConnection> handler) {
if (options.getIdleTimeout() > 0) { if (options.getIdleTimeout() > 0) {
pipeline.addLast("idle", new IdleStateHandler(0, 0, options.getIdleTimeout())); pipeline.addLast("idle", new IdleStateHandler(0, 0, options.getIdleTimeout()));
Expand Down Expand Up @@ -930,4 +940,58 @@ public int hashCode() {
return result; 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.