Accept two ways to start HTTP/2 over clear text

Motivation:

HTTP/2 support two ways to start on a no-tls tcp connection,
http/1.1 upgrade and prior knowlege methodology to start HTTP/2.
Currently, the http2-server from example only support
starting by upgrade. I think we can do a simple dispatch by peek first
bytes from inbound that match to prior knowledge preface or not and
determine which handlers to set into pipeline.

Modifications:

Add ClearTextHttp2ServerUpgradeHandler to support start HTTP/2 via clear
text with two approach. And update example/http2-server to support
this functionality.

Result:

netty HTTP/2 and the example http2-server accept for two ways to start
HTTP/2 over clear text.

Fixed memory leak problem

Update fields to final

Rename ClearText to cleartext

Addressed comments for code improvement

- Always prefer static, final, and private if possible
- Add UnstableApi annotation
- Used EmbeddedChannel.readInbound instead of unhandled inbound handler
- More assertion

Update javadoc for CleartextHttp2ServerUpgradeHandler

Rename ClearTextHttp2ServerUpgradeHandler to CleartextHttp2ServerUpgradeHandler

Removed redundant code about configure pipeline

nit: PriorKnowledgeHandler

Removed Mockito.spy, investigate conn state instead

Add Http2UpgradeEvent

Check null of the constructor arguments

Rename Http2UpgradeEvent to PriorKnowledgeUpgradeEvent

Update unit test
This commit is contained in:
chhsiao90 2017-03-05 23:22:54 +08:00 committed by Scott Mitchell
parent 1419f5b601
commit 0ee36fef00
4 changed files with 321 additions and 7 deletions

View File

@ -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<Object> 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() {
}
}
}

View File

@ -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<Object> 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<Object>();
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);
}
}

View File

@ -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);
}

View File

@ -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<SocketChannel> {
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<HttpMessage>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpMessage msg) throws Exception {