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:
parent
1419f5b601
commit
0ee36fef00
@ -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() {
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user