diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/CleartextHttp2ServerUpgradeHandler.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/CleartextHttp2ServerUpgradeHandler.java new file mode 100644 index 0000000000..cca1911b9e --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/CleartextHttp2ServerUpgradeHandler.java @@ -0,0 +1,108 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.http2; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerAdapter; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpServerUpgradeHandler; +import io.netty.util.internal.UnstableApi; + +import java.util.List; + +import static io.netty.buffer.Unpooled.unreleasableBuffer; +import static io.netty.handler.codec.http2.Http2CodecUtil.connectionPrefaceBuf; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * Performing cleartext upgrade, by h2c HTTP upgrade or Prior Knowledge. + * This handler config pipeline for h2c upgrade when handler added. + * And will update pipeline once it detect the connection is starting HTTP/2 by + * prior knowledge or not. + */ +@UnstableApi +public final class CleartextHttp2ServerUpgradeHandler extends ChannelHandlerAdapter { + private static final ByteBuf CONNECTION_PREFACE = unreleasableBuffer(connectionPrefaceBuf()); + + private final HttpServerCodec httpServerCodec; + private final HttpServerUpgradeHandler httpServerUpgradeHandler; + private final ChannelHandler http2ServerHandler; + + /** + * Creates the channel handler provide cleartext HTTP/2 upgrade from HTTP + * upgrade or prior knowledge + * + * @param httpServerCodec the http server codec + * @param httpServerUpgradeHandler the http server upgrade handler for HTTP/2 + * @param http2ServerHandler the http2 server handler, will be added into pipeline + * when starting HTTP/2 by prior knowledge + */ + public CleartextHttp2ServerUpgradeHandler(HttpServerCodec httpServerCodec, + HttpServerUpgradeHandler httpServerUpgradeHandler, + ChannelHandler http2ServerHandler) { + this.httpServerCodec = checkNotNull(httpServerCodec, "httpServerCodec"); + this.httpServerUpgradeHandler = checkNotNull(httpServerUpgradeHandler, "httpServerUpgradeHandler"); + this.http2ServerHandler = checkNotNull(http2ServerHandler, "http2ServerHandler"); + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + ctx.pipeline() + .addBefore(ctx.name(), null, new PriorKnowledgeHandler()) + .addBefore(ctx.name(), null, httpServerCodec) + .replace(this, null, httpServerUpgradeHandler); + } + + /** + * Peek inbound message to determine current connection wants to start HTTP/2 + * by HTTP upgrade or prior knowledge + */ + private final class PriorKnowledgeHandler extends ByteToMessageDecoder { + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + int prefaceLength = CONNECTION_PREFACE.readableBytes(); + int bytesRead = Math.min(in.readableBytes(), prefaceLength); + + if (!ByteBufUtil.equals(CONNECTION_PREFACE, CONNECTION_PREFACE.readerIndex(), + in, in.readerIndex(), bytesRead)) { + ctx.pipeline().remove(this); + } else if (bytesRead == prefaceLength) { + // Full h2 preface match, removed source codec, using http2 codec to handle + // following network traffic + ctx.pipeline() + .remove(httpServerCodec) + .remove(httpServerUpgradeHandler) + .replace(this, null, http2ServerHandler); + ctx.fireUserEventTriggered(PriorKnowledgeUpgradeEvent.INSTANCE); + } + } + } + + /** + * User event that is fired to notify about HTTP/2 protocol is started. + */ + public static final class PriorKnowledgeUpgradeEvent { + private static final PriorKnowledgeUpgradeEvent INSTANCE = new PriorKnowledgeUpgradeEvent(); + + private PriorKnowledgeUpgradeEvent() { + } + } +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/CleartextHttp2ServerUpgradeHandlerTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/CleartextHttp2ServerUpgradeHandlerTest.java new file mode 100644 index 0000000000..e4bd0be2d6 --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/CleartextHttp2ServerUpgradeHandlerTest.java @@ -0,0 +1,195 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.http2; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpServerUpgradeHandler; +import io.netty.handler.codec.http.HttpServerUpgradeHandler.UpgradeCodec; +import io.netty.handler.codec.http.HttpServerUpgradeHandler.UpgradeCodecFactory; +import io.netty.handler.codec.http.HttpServerUpgradeHandler.UpgradeEvent; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler.PriorKnowledgeUpgradeEvent; +import io.netty.handler.codec.http2.Http2Stream.State; +import io.netty.util.CharsetUtil; +import io.netty.util.ReferenceCountUtil; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Tests for {@link CleartextHttp2ServerUpgradeHandler} + */ +public class CleartextHttp2ServerUpgradeHandlerTest { + private EmbeddedChannel channel; + + private Http2FrameListener frameListener; + + private Http2ConnectionHandler http2ConnectionHandler; + + private List userEvents; + + @Before + public void setUp() { + frameListener = mock(Http2FrameListener.class); + + http2ConnectionHandler = new Http2ConnectionHandlerBuilder().frameListener(frameListener).build(); + + UpgradeCodecFactory upgradeCodecFactory = new UpgradeCodecFactory() { + @Override + public UpgradeCodec newUpgradeCodec(CharSequence protocol) { + return new Http2ServerUpgradeCodec(http2ConnectionHandler); + } + }; + + userEvents = new ArrayList(); + + HttpServerCodec httpServerCodec = new HttpServerCodec(); + HttpServerUpgradeHandler upgradeHandler = new HttpServerUpgradeHandler(httpServerCodec, upgradeCodecFactory); + + CleartextHttp2ServerUpgradeHandler handler = new CleartextHttp2ServerUpgradeHandler( + httpServerCodec, upgradeHandler, http2ConnectionHandler); + channel = new EmbeddedChannel(handler, new ChannelInboundHandlerAdapter() { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + userEvents.add(evt); + } + }); + } + + @After + public void tearDown() throws Exception { + channel.finishAndReleaseAll(); + } + + @Test + public void priorKnowledge() throws Exception { + channel.writeInbound(Http2CodecUtil.connectionPrefaceBuf()); + + ByteBuf settingsFrame = settingsFrameBuf(); + + assertFalse(channel.writeInbound(settingsFrame)); + + assertEquals(1, userEvents.size()); + assertTrue(userEvents.get(0) instanceof PriorKnowledgeUpgradeEvent); + + assertEquals(100, http2ConnectionHandler.connection().local().maxActiveStreams()); + assertEquals(65535, http2ConnectionHandler.connection().local().flowController().initialWindowSize()); + + verify(frameListener).onSettingsRead( + any(ChannelHandlerContext.class), eq(expectedSettings())); + } + + @Test + public void upgrade() throws Exception { + String upgradeString = "GET / HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "Connection: Upgrade, HTTP2-Settings\r\n" + + "Upgrade: h2c\r\n" + + "HTTP2-Settings: AAMAAABkAAQAAP__\r\n\r\n"; + ByteBuf upgrade = Unpooled.buffer().writeBytes(upgradeString.getBytes(CharsetUtil.US_ASCII)); + + assertFalse(channel.writeInbound(upgrade)); + + assertEquals(1, userEvents.size()); + + Object userEvent = userEvents.get(0); + assertTrue(userEvent instanceof UpgradeEvent); + assertEquals("h2c", ((UpgradeEvent) userEvent).protocol()); + ReferenceCountUtil.release(userEvent); + + assertEquals(100, http2ConnectionHandler.connection().local().maxActiveStreams()); + assertEquals(65535, http2ConnectionHandler.connection().local().flowController().initialWindowSize()); + + assertEquals(1, http2ConnectionHandler.connection().numActiveStreams()); + assertNotNull(http2ConnectionHandler.connection().stream(1)); + + Http2Stream stream = http2ConnectionHandler.connection().stream(1); + assertEquals(State.HALF_CLOSED_REMOTE, stream.state()); + assertFalse(stream.isHeadersSent()); + } + + @Test + public void priorKnowledgeInFragments() throws Exception { + ByteBuf connectionPreface = Http2CodecUtil.connectionPrefaceBuf(); + assertFalse(channel.writeInbound(connectionPreface.readBytes(5), connectionPreface)); + + ByteBuf settingsFrame = settingsFrameBuf(); + assertFalse(channel.writeInbound(settingsFrame)); + + assertEquals(1, userEvents.size()); + assertTrue(userEvents.get(0) instanceof PriorKnowledgeUpgradeEvent); + + assertEquals(100, http2ConnectionHandler.connection().local().maxActiveStreams()); + assertEquals(65535, http2ConnectionHandler.connection().local().flowController().initialWindowSize()); + + verify(frameListener).onSettingsRead( + any(ChannelHandlerContext.class), eq(expectedSettings())); + } + + @Test + public void downgrade() throws Exception { + String requestString = "GET / HTTP/1.1\r\n" + + "Host: example.com\r\n\r\n"; + ByteBuf inbound = Unpooled.buffer().writeBytes(requestString.getBytes(CharsetUtil.US_ASCII)); + + assertTrue(channel.writeInbound(inbound)); + + Object firstInbound = channel.readInbound(); + assertTrue(firstInbound instanceof HttpRequest); + HttpRequest request = (HttpRequest) firstInbound; + assertEquals(HttpMethod.GET, request.method()); + assertEquals("/", request.uri()); + assertEquals(HttpVersion.HTTP_1_1, request.protocolVersion()); + assertEquals(new DefaultHttpHeaders().add("Host", "example.com"), request.headers()); + + ((LastHttpContent) channel.readInbound()).release(); + + assertNull(channel.readInbound()); + } + + private static ByteBuf settingsFrameBuf() { + ByteBuf settingsFrame = Unpooled.buffer(); + settingsFrame.writeMedium(12); // Payload length + settingsFrame.writeByte(0x4); // Frame type + settingsFrame.writeByte(0x0); // Flags + settingsFrame.writeInt(0x0); // StreamId + settingsFrame.writeShort(0x3); + settingsFrame.writeInt(100); + settingsFrame.writeShort(0x4); + settingsFrame.writeInt(65535); + + return settingsFrame; + } + + private static Http2Settings expectedSettings() { + return new Http2Settings().maxConcurrentStreams(100).initialWindowSize(65535); + } +} diff --git a/example/src/main/java/io/netty/example/http2/helloworld/server/HelloWorldHttp2Handler.java b/example/src/main/java/io/netty/example/http2/helloworld/server/HelloWorldHttp2Handler.java index a763a9de87..817642e3a7 100644 --- a/example/src/main/java/io/netty/example/http2/helloworld/server/HelloWorldHttp2Handler.java +++ b/example/src/main/java/io/netty/example/http2/helloworld/server/HelloWorldHttp2Handler.java @@ -18,6 +18,7 @@ package io.netty.example.http2.helloworld.server; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpServerUpgradeHandler; import io.netty.handler.codec.http2.DefaultHttp2Headers; import io.netty.handler.codec.http2.Http2ConnectionDecoder; @@ -48,6 +49,14 @@ public final class HelloWorldHttp2Handler extends Http2ConnectionHandler impleme super(decoder, encoder, initialSettings); } + private static Http2Headers http1HeadersToHttp2Headers(FullHttpRequest request) { + return new DefaultHttp2Headers() + .authority(request.headers().get("Host")) + .method("GET") + .path(request.uri()) + .scheme("http"); + } + /** * Handles the cleartext HTTP upgrade event. If an upgrade occurred, sends a simple response via HTTP/2 * on stream 1 (the stream specifically reserved for cleartext HTTP upgrade). @@ -55,11 +64,9 @@ public final class HelloWorldHttp2Handler extends Http2ConnectionHandler impleme @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof HttpServerUpgradeHandler.UpgradeEvent) { - // Write an HTTP/2 response to the upgrade request - Http2Headers headers = - new DefaultHttp2Headers().status(OK.codeAsText()) - .set(new AsciiString(UPGRADE_RESPONSE_HEADER), new AsciiString("true")); - encoder().writeHeaders(ctx, 1, headers, 0, true, ctx.newPromise()); + HttpServerUpgradeHandler.UpgradeEvent upgradeEvent = + (HttpServerUpgradeHandler.UpgradeEvent) evt; + onHeadersRead(ctx, 1, http1HeadersToHttp2Headers(upgradeEvent.upgradeRequest()), 0 , true); } super.userEventTriggered(ctx, evt); } diff --git a/example/src/main/java/io/netty/example/http2/helloworld/server/Http2ServerInitializer.java b/example/src/main/java/io/netty/example/http2/helloworld/server/Http2ServerInitializer.java index 64b221a508..b5dff01e30 100644 --- a/example/src/main/java/io/netty/example/http2/helloworld/server/Http2ServerInitializer.java +++ b/example/src/main/java/io/netty/example/http2/helloworld/server/Http2ServerInitializer.java @@ -28,6 +28,7 @@ import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.HttpServerUpgradeHandler; import io.netty.handler.codec.http.HttpServerUpgradeHandler.UpgradeCodec; import io.netty.handler.codec.http.HttpServerUpgradeHandler.UpgradeCodecFactory; +import io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler; import io.netty.handler.codec.http2.Http2CodecUtil; import io.netty.handler.codec.http2.Http2ServerUpgradeCodec; import io.netty.handler.ssl.SslContext; @@ -88,9 +89,12 @@ public class Http2ServerInitializer extends ChannelInitializer { private void configureClearText(SocketChannel ch) { final ChannelPipeline p = ch.pipeline(); final HttpServerCodec sourceCodec = new HttpServerCodec(); + final HttpServerUpgradeHandler upgradeHandler = new HttpServerUpgradeHandler(sourceCodec, upgradeCodecFactory); + final CleartextHttp2ServerUpgradeHandler cleartextHttp2ServerUpgradeHandler = + new CleartextHttp2ServerUpgradeHandler(sourceCodec, upgradeHandler, + new HelloWorldHttp2HandlerBuilder().build()); - p.addLast(sourceCodec); - p.addLast(new HttpServerUpgradeHandler(sourceCodec, upgradeCodecFactory)); + p.addLast(cleartextHttp2ServerUpgradeHandler); p.addLast(new SimpleChannelInboundHandler() { @Override protected void channelRead0(ChannelHandlerContext ctx, HttpMessage msg) throws Exception {