+ * All outbound frames are disallowed after a connection shutdown has begun by sending a goAway
+ * frame to the remote endpoint. In addition, no outbound frames are allowed until the first non-ack
+ * settings frame is received from the remote endpoint.
+ */
public class Http2ConnectionHandler extends ChannelHandlerAdapter {
private final Http2Connection connection;
private final InboundFlowController inboundFlow;
private final OutboundFlowController outboundFlow;
+ private boolean initialSettingsSent;
+ private boolean initialSettingsReceived;
public Http2ConnectionHandler(boolean server) {
this(new DefaultHttp2Connection(server));
@@ -114,7 +145,11 @@ public class Http2ConnectionHandler extends ChannelHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object inMsg) throws Exception {
try {
- if (inMsg instanceof Http2DataFrame) {
+ if (inMsg == CONNECTION_PREFACE) {
+ // The connection preface has been received from the remote endpoint, we're
+ // beginning an HTTP2 connection. Send the initial settings to the remote endpoint.
+ sendInitialSettings(ctx);
+ } else if (inMsg instanceof Http2DataFrame) {
handleInboundData(ctx, (Http2DataFrame) inMsg);
} else if (inMsg instanceof Http2HeadersFrame) {
handleInboundHeaders(ctx, (Http2HeadersFrame) inMsg);
@@ -146,6 +181,12 @@ public class Http2ConnectionHandler extends ChannelHandlerAdapter {
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
throws Exception {
try {
+ if (!initialSettingsReceived) {
+ throw protocolError(
+ "Attempting to send frame (%s) before initial settings received", msg
+ .getClass().getName());
+ }
+
if (msg instanceof Http2DataFrame) {
handleOutboundData(ctx, (Http2DataFrame) msg, promise);
} else if (msg instanceof Http2HeadersFrame) {
@@ -207,6 +248,7 @@ public class Http2ConnectionHandler extends ChannelHandlerAdapter {
private void handleInboundData(final ChannelHandlerContext ctx, Http2DataFrame frame)
throws Http2Exception {
+ verifyInitialSettingsReceived();
// Check if we received a data frame for a stream which is half-closed
Http2Stream stream = connection.getStreamOrFail(frame.getStreamId());
@@ -236,6 +278,8 @@ public class Http2ConnectionHandler extends ChannelHandlerAdapter {
private void handleInboundHeaders(ChannelHandlerContext ctx, Http2HeadersFrame frame)
throws Http2Exception {
+ verifyInitialSettingsReceived();
+
if (isInboundStreamAfterGoAway(frame)) {
return;
}
@@ -269,6 +313,8 @@ public class Http2ConnectionHandler extends ChannelHandlerAdapter {
private void handleInboundPushPromise(ChannelHandlerContext ctx, Http2PushPromiseFrame frame)
throws Http2Exception {
+ verifyInitialSettingsReceived();
+
if (isInboundStreamAfterGoAway(frame)) {
// Ignore frames for any stream created after we sent a go-away.
return;
@@ -283,6 +329,8 @@ public class Http2ConnectionHandler extends ChannelHandlerAdapter {
private void handleInboundPriority(ChannelHandlerContext ctx, Http2PriorityFrame frame)
throws Http2Exception {
+ verifyInitialSettingsReceived();
+
if (isInboundStreamAfterGoAway(frame)) {
// Ignore frames for any stream created after we sent a go-away.
return;
@@ -304,6 +352,8 @@ public class Http2ConnectionHandler extends ChannelHandlerAdapter {
private void handleInboundWindowUpdate(ChannelHandlerContext ctx, Http2WindowUpdateFrame frame)
throws Http2Exception {
+ verifyInitialSettingsReceived();
+
if (isInboundStreamAfterGoAway(frame)) {
// Ignore frames for any stream created after we sent a go-away.
return;
@@ -325,7 +375,10 @@ public class Http2ConnectionHandler extends ChannelHandlerAdapter {
ctx.fireChannelRead(frame);
}
- private void handleInboundRstStream(ChannelHandlerContext ctx, Http2RstStreamFrame frame) {
+ private void handleInboundRstStream(ChannelHandlerContext ctx, Http2RstStreamFrame frame)
+ throws Http2Exception {
+ verifyInitialSettingsReceived();
+
if (isInboundStreamAfterGoAway(frame)) {
// Ignore frames for any stream created after we sent a go-away.
return;
@@ -342,7 +395,10 @@ public class Http2ConnectionHandler extends ChannelHandlerAdapter {
ctx.fireChannelRead(frame);
}
- private static void handleInboundPing(ChannelHandlerContext ctx, Http2PingFrame frame) {
+ private void handleInboundPing(ChannelHandlerContext ctx, Http2PingFrame frame)
+ throws Http2Exception {
+ verifyInitialSettingsReceived();
+
if (frame.isAck()) {
// The remote enpoint is responding to an Ack that we sent.
ctx.fireChannelRead(frame);
@@ -358,6 +414,10 @@ public class Http2ConnectionHandler extends ChannelHandlerAdapter {
private void handleInboundSettings(ChannelHandlerContext ctx, Http2SettingsFrame frame)
throws Http2Exception {
if (frame.isAck()) {
+ // Should not get an ack before receiving the initial settings from the remote
+ // endpoint.
+ verifyInitialSettingsReceived();
+
// The remote endpoint is acknowledging the settings - fire this up to the next
// handler.
ctx.fireChannelRead(frame);
@@ -386,6 +446,10 @@ public class Http2ConnectionHandler extends ChannelHandlerAdapter {
// Acknowledge receipt of the settings.
Http2Frame ack = new DefaultHttp2SettingsFrame.Builder().setAck(true).build();
ctx.writeAndFlush(ack);
+
+ // We've received at least one non-ack settings frame from the remote endpoint.
+ initialSettingsReceived = true;
+ ctx.fireChannelRead(frame);
}
private void handleInboundGoAway(ChannelHandlerContext ctx, Http2GoAwayFrame frame) {
@@ -575,4 +639,35 @@ public class Http2ConnectionHandler extends ChannelHandlerAdapter {
}
ctx.writeAndFlush(frame, promise);
}
+
+ private void verifyInitialSettingsReceived() throws Http2Exception {
+ if (!initialSettingsReceived) {
+ throw protocolError("Received non-SETTINGS as first frame.");
+ }
+ }
+
+ /**
+ * Sends the initial settings frame upon establishment of the connection, if not already sent.
+ */
+ private void sendInitialSettings(ChannelHandlerContext ctx) throws Http2Exception {
+ if (initialSettingsSent) {
+ throw protocolError("Already sent initial settings.");
+ }
+
+ // Create and send the frame to the remote endpoint.
+ DefaultHttp2SettingsFrame frame =
+ new DefaultHttp2SettingsFrame.Builder()
+ .setInitialWindowSize(inboundFlow.getInitialInboundWindowSize())
+ .setMaxConcurrentStreams(connection.remote().getMaxStreams())
+ .setPushEnabled(connection.local().isPushToAllowed()).build();
+
+ ctx.writeAndFlush(frame).addListener(new ChannelFutureListener() {
+ @Override
+ public void operationComplete(ChannelFuture future) throws Exception {
+ if (future.isSuccess()) {
+ initialSettingsSent = true;
+ }
+ }
+ });
+ }
}
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/InboundFlowController.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/InboundFlowController.java
index 38efd3e85e..6da0b39390 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/InboundFlowController.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/InboundFlowController.java
@@ -44,6 +44,11 @@ public interface InboundFlowController {
*/
void setInitialInboundWindowSize(int newWindowSize) throws Http2Exception;
+ /**
+ * Gets the initial inbound flow control window size.
+ */
+ int getInitialInboundWindowSize();
+
/**
* Applies flow control for the received data frame.
*
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/OutboundFlowController.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/OutboundFlowController.java
index 1ce400d0ef..6255c883b9 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/OutboundFlowController.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/OutboundFlowController.java
@@ -50,6 +50,11 @@ public interface OutboundFlowController {
*/
void setInitialOutboundWindowSize(int newWindowSize) throws Http2Exception;
+ /**
+ * Gets the initial size of the connection's outbound flow control window.
+ */
+ int getInitialOutboundWindowSize();
+
/**
* Updates the size of the stream's outbound flow control window. This is called upon receiving a
* WINDOW_UPDATE frame from the remote endpoint.
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2FrameCodecUtil.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2FrameCodecUtil.java
index 8a375e4844..2d2e0b37aa 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2FrameCodecUtil.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2FrameCodecUtil.java
@@ -16,6 +16,8 @@
package io.netty.handler.codec.http2.draft10.frame;
import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.util.CharsetUtil;
/**
* Constants and utility method used for encoding/decoding HTTP2 frames.
@@ -23,6 +25,20 @@ import io.netty.buffer.ByteBuf;
public final class Http2FrameCodecUtil {
public static final int CONNECTION_STREAM_ID = 0;
+ public static final String CONNECTION_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
+
+ private static final ByteBuf CONNECTION_PREFACE_BUF = Unpooled.unmodifiableBuffer(Unpooled
+ .copiedBuffer(CONNECTION_PREFACE, CharsetUtil.UTF_8));
+
+ /**
+ * Returns a buffer containing the the {@link #CONNECTION_PREFACE}.
+ */
+ public static ByteBuf connectionPrefaceBuf() {
+ // Return a duplicate so that modifications to the reader index will not affect the original
+ // buffer.
+ return CONNECTION_PREFACE_BUF.duplicate().retain();
+ }
+
public static final int DEFAULT_STREAM_PRIORITY = 0x40000000; // 2^30
public static final int MAX_FRAME_PAYLOAD_LENGTH = 16383;
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2FrameDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2FrameDecoder.java
index 7e6bad0dbb..d9db761370 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2FrameDecoder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2FrameDecoder.java
@@ -15,10 +15,13 @@
package io.netty.handler.codec.http2.draft10.frame.decoder;
+import static io.netty.handler.codec.http2.draft10.Http2Error.PROTOCOL_ERROR;
+import static io.netty.handler.codec.http2.draft10.Http2Exception.format;
+import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.CONNECTION_PREFACE;
import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_HEADER_LENGTH;
import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_LENGTH_MASK;
+import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.connectionPrefaceBuf;
import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.readUnsignedInt;
-
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
@@ -40,12 +43,14 @@ import java.util.List;
public class Http2FrameDecoder extends ByteToMessageDecoder {
private enum State {
+ PREFACE,
FRAME_HEADER,
FRAME_PAYLOAD,
ERROR
}
private final Http2FrameUnmarshaller frameUnmarshaller;
+ private final ByteBuf preface;
private State state;
private int payloadLength;
@@ -58,31 +63,41 @@ public class Http2FrameDecoder extends ByteToMessageDecoder {
throw new NullPointerException("frameUnmarshaller");
}
this.frameUnmarshaller = frameUnmarshaller;
- state = State.FRAME_HEADER;
+ preface = connectionPrefaceBuf();
+ state = State.PREFACE;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List