diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpStatusClass.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpStatusClass.java index 9f57e18984..0a4f4c11ab 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpStatusClass.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpStatusClass.java @@ -74,6 +74,27 @@ public enum HttpStatusClass { return UNKNOWN; } + /** + * Returns the class of the specified HTTP status code. + * @param code Just the numeric portion of the http status code. + */ + public static HttpStatusClass valueOf(CharSequence code) { + if (code != null && code.length() == 3) { + char c0 = code.charAt(0); + return isDigit(c0) && isDigit(code.charAt(1)) && isDigit(code.charAt(2)) ? valueOf(digit(c0) * 100) + : UNKNOWN; + } + return UNKNOWN; + } + + private static int digit(char c) { + return c - '0'; + } + + private static boolean isDigit(char c) { + return c >= '0' && c <= '9'; + } + private final int min; private final int max; private final AsciiString defaultReasonPhrase; diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Connection.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Connection.java index 2789423bc7..12815c225c 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Connection.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Connection.java @@ -373,13 +373,16 @@ public class DefaultHttp2Connection implements Http2Connection { * Simple stream implementation. Streams can be compared to each other by priority. */ private class DefaultStream implements Http2Stream { - private static final byte SENT_STATE_RST = 0x1; - private static final byte SENT_STATE_HEADERS = 0x2; - private static final byte SENT_STATE_PUSHPROMISE = 0x4; + private static final byte META_STATE_SENT_RST = 1; + private static final byte META_STATE_SENT_HEADERS = 1 << 1; + private static final byte META_STATE_SENT_TRAILERS = 1 << 2; + private static final byte META_STATE_SENT_PUSHPROMISE = 1 << 3; + private static final byte META_STATE_RECV_HEADERS = 1 << 4; + private static final byte META_STATE_RECV_TRAILERS = 1 << 5; private final int id; private final PropertyMap properties = new PropertyMap(); private State state; - private byte sentState; + private byte metaState; DefaultStream(int id, State state) { this.id = id; @@ -398,35 +401,60 @@ public class DefaultHttp2Connection implements Http2Connection { @Override public boolean isResetSent() { - return (sentState & SENT_STATE_RST) != 0; + return (metaState & META_STATE_SENT_RST) != 0; } @Override public Http2Stream resetSent() { - sentState |= SENT_STATE_RST; + metaState |= META_STATE_SENT_RST; return this; } @Override - public Http2Stream headersSent() { - sentState |= SENT_STATE_HEADERS; + public Http2Stream headersSent(boolean isInformational) { + if (!isInformational) { + metaState |= isHeadersSent() ? META_STATE_SENT_TRAILERS : META_STATE_SENT_HEADERS; + } return this; } @Override public boolean isHeadersSent() { - return (sentState & SENT_STATE_HEADERS) != 0; + return (metaState & META_STATE_SENT_HEADERS) != 0; + } + + @Override + public boolean isTrailersSent() { + return (metaState & META_STATE_SENT_TRAILERS) != 0; + } + + @Override + public Http2Stream headersReceived(boolean isInformational) { + if (!isInformational) { + metaState |= isHeadersReceived() ? META_STATE_RECV_TRAILERS : META_STATE_RECV_HEADERS; + } + return this; + } + + @Override + public boolean isHeadersReceived() { + return (metaState & META_STATE_RECV_HEADERS) != 0; + } + + @Override + public boolean isTrailersReceived() { + return (metaState & META_STATE_RECV_TRAILERS) != 0; } @Override public Http2Stream pushPromiseSent() { - sentState |= SENT_STATE_PUSHPROMISE; + metaState |= META_STATE_SENT_PUSHPROMISE; return this; } @Override public boolean isPushPromiseSent() { - return (sentState & SENT_STATE_PUSHPROMISE) != 0; + return (metaState & META_STATE_SENT_PUSHPROMISE) != 0; } @Override @@ -599,7 +627,7 @@ public class DefaultHttp2Connection implements Http2Connection { } @Override - public Http2Stream headersSent() { + public Http2Stream headersSent(boolean isInformational) { throw new UnsupportedOperationException(); } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoder.java index ef643fafad..4027978651 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoder.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.http2; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpStatusClass; import io.netty.handler.codec.http2.Http2Connection.Endpoint; import io.netty.util.internal.UnstableApi; import io.netty.util.internal.logging.InternalLogger; @@ -23,6 +24,7 @@ import io.netty.util.internal.logging.InternalLoggerFactory; import java.util.List; +import static io.netty.handler.codec.http.HttpStatusClass.INFORMATIONAL; import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT; import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR; import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR; @@ -282,6 +284,14 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder { return; } + boolean isInformational = !connection.isServer() && + HttpStatusClass.valueOf(headers.status()) == INFORMATIONAL; + if ((isInformational || !endOfStream) && stream.isHeadersReceived() || stream.isTrailersReceived()) { + throw streamError(streamId, PROTOCOL_ERROR, + "Stream %d received too many headers EOS: %s state: %s", + streamId, endOfStream, stream.state()); + } + switch (stream.state()) { case RESERVED_REMOTE: stream.open(endOfStream); @@ -305,6 +315,7 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder { stream.state()); } + stream.headersReceived(isInformational); encoder.flowController().updateDependencyTree(streamId, streamDependency, weight, exclusive); listener.onHeadersRead(ctx, streamId, headers, streamDependency, weight, exclusive, padding, endOfStream); diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoder.java index f0af13b394..75eda12ec7 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoder.java @@ -21,10 +21,12 @@ import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.channel.CoalescingBufferQueue; +import io.netty.handler.codec.http.HttpStatusClass; import io.netty.util.internal.UnstableApi; import java.util.ArrayDeque; +import static io.netty.handler.codec.http.HttpStatusClass.INFORMATIONAL; import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT; import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR; import static io.netty.handler.codec.http2.Http2Exception.connectionError; @@ -145,6 +147,15 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder { return writeHeaders(ctx, streamId, headers, 0, DEFAULT_PRIORITY_WEIGHT, false, padding, endStream, promise); } + private static boolean validateHeadersSentState(Http2Stream stream, Http2Headers headers, boolean isServer, + boolean endOfStream) { + boolean isInformational = isServer && HttpStatusClass.valueOf(headers.status()) == INFORMATIONAL; + if ((isInformational || !endOfStream) && stream.isHeadersSent() || stream.isTrailersSent()) { + throw new IllegalStateException("Stream " + stream.id() + " sent too many headers EOS: " + endOfStream); + } + return isInformational; + } + @Override public ChannelFuture writeHeaders(final ChannelHandlerContext ctx, final int streamId, final Http2Headers headers, final int streamDependency, final short weight, @@ -180,6 +191,7 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder { // for this stream. Http2RemoteFlowController flowController = flowController(); if (!endOfStream || !flowController.hasFlowControlled(stream)) { + boolean isInformational = validateHeadersSentState(stream, headers, connection.isServer(), endOfStream); if (endOfStream) { final Http2Stream finalStream = stream; final ChannelFutureListener closeStreamLocalListener = new ChannelFutureListener() { @@ -190,6 +202,7 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder { }; promise = promise.unvoid().addListener(closeStreamLocalListener); } + ChannelFuture future = frameWriter.writeHeaders(ctx, streamId, headers, streamDependency, weight, exclusive, padding, endOfStream, promise); // Writing headers may fail during the encode state if they violate HPACK limits. @@ -197,7 +210,7 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder { if (failureCause == null) { // Synchronously set the headersSent flag to ensure that we do not subsequently write // other headers containing pseudo-header fields. - stream.headersSent(); + stream.headersSent(isInformational); } else { lifecycleManager.onError(ctx, failureCause); } @@ -451,6 +464,7 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder { @Override public void write(ChannelHandlerContext ctx, int allowedBytes) { + boolean isInformational = validateHeadersSentState(stream, headers, connection.isServer(), endOfStream); if (promise.isVoid()) { promise = ctx.newPromise(); } @@ -461,7 +475,7 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder { // Writing headers may fail during the encode state if they violate HPACK limits. Throwable failureCause = f.cause(); if (failureCause == null) { - stream.headersSent(); + stream.headersSent(isInformational); } else { lifecycleManager.onError(ctx, failureCause); } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Stream.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Stream.java index 167087551b..3b654425cf 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Stream.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Stream.java @@ -130,15 +130,41 @@ public interface Http2Stream { V removeProperty(Http2Connection.PropertyKey key); /** - * Indicates that headers has been sent to the remote on this stream. + * Indicates that headers have been sent to the remote endpoint on this stream. The first call to this method would + * be for the initial headers (see {@link #isHeadersSent()}} and the second call would indicate the trailers + * (see {@link #isTrailersReceived()}). + * @param isInformational {@code true} if the headers contain an informational status code (for responses only). */ - Http2Stream headersSent(); + Http2Stream headersSent(boolean isInformational); /** - * Indicates whether or not headers was sent to the remote endpoint. + * Indicates whether or not headers were sent to the remote endpoint. */ boolean isHeadersSent(); + /** + * Indicates whether or not trailers were sent to the remote endpoint. + */ + boolean isTrailersSent(); + + /** + * Indicates that headers have been received. The first call to this method would be for the initial headers + * (see {@link #isHeadersReceived()}} and the second call would indicate the trailers + * (see {@link #isTrailersReceived()}). + * @param isInformational {@code true} if the headers contain an informational status code (for responses only). + */ + Http2Stream headersReceived(boolean isInformational); + + /** + * Indicates whether or not the initial headers have been received. + */ + boolean isHeadersReceived(); + + /** + * Indicates whether or not the trailers have been received. + */ + boolean isTrailersReceived(); + /** * Indicates that a push promise was sent to the remote endpoint. */ diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoderTest.java index 6dc4266799..3fcf560eff 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoderTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoderTest.java @@ -21,6 +21,7 @@ import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.channel.DefaultChannelPromise; +import io.netty.handler.codec.http.HttpResponseStatus; import junit.framework.AssertionFailedError; import org.junit.Before; import org.junit.Test; @@ -50,10 +51,10 @@ import static org.mockito.Mockito.anyBoolean; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.anyShort; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.isNull; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.isNull; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -67,6 +68,8 @@ public class DefaultHttp2ConnectionDecoderTest { private static final int STREAM_ID = 3; private static final int PUSH_STREAM_ID = 2; private static final int STREAM_DEPENDENCY_ID = 5; + private static final int STATE_RECV_HEADERS = 1; + private static final int STATE_RECV_TRAILERS = 1 << 1; private Http2ConnectionDecoder decoder; private ChannelPromise promise; @@ -122,11 +125,49 @@ public class DefaultHttp2ConnectionDecoderTest { promise = new DefaultChannelPromise(channel); + final AtomicInteger headersReceivedState = new AtomicInteger(); when(channel.isActive()).thenReturn(true); when(stream.id()).thenReturn(STREAM_ID); when(stream.state()).thenReturn(OPEN); when(stream.open(anyBoolean())).thenReturn(stream); when(pushStream.id()).thenReturn(PUSH_STREAM_ID); + doAnswer(new Answer() { + @Override + public Boolean answer(InvocationOnMock in) throws Throwable { + return (headersReceivedState.get() & STATE_RECV_HEADERS) != 0; + } + }).when(stream).isHeadersReceived(); + doAnswer(new Answer() { + @Override + public Boolean answer(InvocationOnMock in) throws Throwable { + return (headersReceivedState.get() & STATE_RECV_TRAILERS) != 0; + } + }).when(stream).isTrailersReceived(); + doAnswer(new Answer() { + @Override + public Http2Stream answer(InvocationOnMock in) throws Throwable { + boolean isInformational = in.getArgument(0); + if (isInformational) { + return stream; + } + for (;;) { + int current = headersReceivedState.get(); + int next = current; + if ((current & STATE_RECV_HEADERS) != 0) { + if ((current & STATE_RECV_TRAILERS) != 0) { + throw new IllegalStateException("already sent headers!"); + } + next |= STATE_RECV_TRAILERS; + } else { + next |= STATE_RECV_HEADERS; + } + if (headersReceivedState.compareAndSet(current, next)) { + break; + } + } + return stream; + } + }).when(stream).headersReceived(anyBoolean()); doAnswer(new Answer() { @Override public Http2Stream answer(InvocationOnMock in) throws Throwable { @@ -452,7 +493,65 @@ public class DefaultHttp2ConnectionDecoderTest { eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false)); } + @Test(expected = Http2Exception.class) + public void trailersDoNotEndStreamThrows() throws Exception { + decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false); + // Trailers must end the stream! + decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false); + } + + @Test(expected = Http2Exception.class) + public void tooManyHeadersEOSThrows() throws Exception { + tooManyHeaderThrows(true); + } + + @Test(expected = Http2Exception.class) + public void tooManyHeadersNoEOSThrows() throws Exception { + tooManyHeaderThrows(false); + } + + private void tooManyHeaderThrows(boolean eos) throws Exception { + decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false); + decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, true); + // We already received the trailers! + decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, eos); + } + + private static Http2Headers informationalHeaders() { + Http2Headers headers = new DefaultHttp2Headers(); + headers.status(HttpResponseStatus.CONTINUE.codeAsText()); + return headers; + } + @Test + public void infoHeadersAndTrailersAllowed() throws Exception { + infoHeadersAndTrailersAllowed(true, 1); + } + + @Test + public void multipleInfoHeadersAndTrailersAllowed() throws Exception { + infoHeadersAndTrailersAllowed(true, 10); + } + + @Test(expected = Http2Exception.class) + public void infoHeadersAndTrailersNoEOSThrows() throws Exception { + infoHeadersAndTrailersAllowed(false, 1); + } + + @Test(expected = Http2Exception.class) + public void multipleInfoHeadersAndTrailersNoEOSThrows() throws Exception { + infoHeadersAndTrailersAllowed(false, 10); + } + + private void infoHeadersAndTrailersAllowed(boolean eos, int infoHeaderCount) throws Exception { + for (int i = 0; i < infoHeaderCount; ++i) { + decode().onHeadersRead(ctx, STREAM_ID, informationalHeaders(), 0, false); + } + decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false); + decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, eos); + } + + @Test() public void headersReadForPromisedStreamShouldCloseStream() throws Exception { when(stream.state()).thenReturn(RESERVED_REMOTE); decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, true); diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoderTest.java index ac63f6dce9..4c5482ba9e 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoderTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoderTest.java @@ -24,6 +24,7 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; import io.netty.channel.ChannelPromise; import io.netty.channel.DefaultChannelPromise; +import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http2.Http2RemoteFlowController.FlowControlled; import io.netty.util.concurrent.ImmediateEventExecutor; import junit.framework.AssertionFailedError; @@ -59,11 +60,12 @@ import static org.mockito.Mockito.anyBoolean; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.anyShort; -import static org.mockito.Mockito.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @@ -342,11 +344,208 @@ public class DefaultHttp2ConnectionEncoderTest { eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise)); } + @Test + public void trailersDoNotEndStreamThrows() { + writeAllFlowControlledFrames(); + final int streamId = 6; + ChannelPromise promise = newPromise(); + encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, false, promise); + + ChannelPromise promise2 = newPromise(); + ChannelFuture future = encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, false, promise2); + assertTrue(future.isDone()); + assertFalse(future.isSuccess()); + + verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0), + eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise)); + } + + @Test + public void trailersDoNotEndStreamWithDataThrows() { + writeAllFlowControlledFrames(); + final int streamId = 6; + ChannelPromise promise = newPromise(); + encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, false, promise); + + Http2Stream stream = connection.stream(streamId); + when(remoteFlow.hasFlowControlled(eq(stream))).thenReturn(true); + + ChannelPromise promise2 = newPromise(); + ChannelFuture future = encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, false, promise2); + assertTrue(future.isDone()); + assertFalse(future.isSuccess()); + + verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0), + eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise)); + } + + @Test + public void tooManyHeadersNoEOSThrows() { + tooManyHeadersThrows(false); + } + + @Test + public void tooManyHeadersEOSThrows() { + tooManyHeadersThrows(true); + } + + private void tooManyHeadersThrows(boolean eos) { + writeAllFlowControlledFrames(); + final int streamId = 6; + ChannelPromise promise = newPromise(); + encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, false, promise); + ChannelPromise promise2 = newPromise(); + encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, true, promise2); + + ChannelPromise promise3 = newPromise(); + ChannelFuture future = encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, eos, promise3); + assertTrue(future.isDone()); + assertFalse(future.isSuccess()); + + verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0), + eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise)); + verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0), + eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise2)); + } + + @Test + public void infoHeadersAndTrailersAllowed() throws Exception { + infoHeadersAndTrailers(true, 1); + } + + @Test + public void multipleInfoHeadersAndTrailersAllowed() throws Exception { + infoHeadersAndTrailers(true, 10); + } + + @Test + public void infoHeadersAndTrailersNoEOSThrows() throws Exception { + infoHeadersAndTrailers(false, 1); + } + + @Test + public void multipleInfoHeadersAndTrailersNoEOSThrows() throws Exception { + infoHeadersAndTrailers(false, 10); + } + + private void infoHeadersAndTrailers(boolean eos, int infoHeaderCount) { + writeAllFlowControlledFrames(); + final int streamId = 6; + Http2Headers infoHeaders = informationalHeaders(); + for (int i = 0; i < infoHeaderCount; ++i) { + encoder.writeHeaders(ctx, streamId, infoHeaders, 0, false, newPromise()); + } + ChannelPromise promise2 = newPromise(); + encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, false, promise2); + + ChannelPromise promise3 = newPromise(); + ChannelFuture future = encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, eos, promise3); + assertTrue(future.isDone()); + assertEquals(eos, future.isSuccess()); + + verify(writer, times(infoHeaderCount)).writeHeaders(eq(ctx), eq(streamId), eq(infoHeaders), eq(0), + eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), any(ChannelPromise.class)); + verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0), + eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise2)); + if (eos) { + verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0), + eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise3)); + } + } + + private static Http2Headers informationalHeaders() { + Http2Headers headers = new DefaultHttp2Headers(); + headers.status(HttpResponseStatus.CONTINUE.codeAsText()); + return headers; + } + + @Test + public void tooManyHeadersWithDataNoEOSThrows() { + tooManyHeadersWithDataThrows(false); + } + + @Test + public void tooManyHeadersWithDataEOSThrows() { + tooManyHeadersWithDataThrows(true); + } + + private void tooManyHeadersWithDataThrows(boolean eos) { + writeAllFlowControlledFrames(); + final int streamId = 6; + ChannelPromise promise = newPromise(); + encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, false, promise); + + Http2Stream stream = connection.stream(streamId); + when(remoteFlow.hasFlowControlled(eq(stream))).thenReturn(true); + + ChannelPromise promise2 = newPromise(); + encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, true, promise2); + + ChannelPromise promise3 = newPromise(); + ChannelFuture future = encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, eos, promise3); + assertTrue(future.isDone()); + assertFalse(future.isSuccess()); + + verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0), + eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise)); + verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0), + eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise2)); + } + + @Test + public void infoHeadersAndTrailersWithDataAllowed() { + infoHeadersAndTrailersWithData(true, 1); + } + + @Test + public void multipleInfoHeadersAndTrailersWithDataAllowed() { + infoHeadersAndTrailersWithData(true, 10); + } + + @Test + public void infoHeadersAndTrailersWithDataNoEOSThrows() { + infoHeadersAndTrailersWithData(false, 1); + } + + @Test + public void multipleInfoHeadersAndTrailersWithDataNoEOSThrows() { + infoHeadersAndTrailersWithData(false, 10); + } + + private void infoHeadersAndTrailersWithData(boolean eos, int infoHeaderCount) { + writeAllFlowControlledFrames(); + final int streamId = 6; + Http2Headers infoHeaders = informationalHeaders(); + for (int i = 0; i < infoHeaderCount; ++i) { + encoder.writeHeaders(ctx, streamId, infoHeaders, 0, false, newPromise()); + } + + Http2Stream stream = connection.stream(streamId); + when(remoteFlow.hasFlowControlled(eq(stream))).thenReturn(true); + + ChannelPromise promise2 = newPromise(); + encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, false, promise2); + + ChannelPromise promise3 = newPromise(); + ChannelFuture future = encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, eos, promise3); + assertTrue(future.isDone()); + assertEquals(eos, future.isSuccess()); + + verify(writer, times(infoHeaderCount)).writeHeaders(eq(ctx), eq(streamId), eq(infoHeaders), eq(0), + eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), any(ChannelPromise.class)); + verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0), + eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise2)); + if (eos) { + verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0), + eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise3)); + } + } + @Test public void pushPromiseWriteAfterGoAwayReceivedShouldFail() throws Exception { createStream(STREAM_ID, false); goAwayReceived(0); - ChannelFuture future = encoder.writePushPromise(ctx, STREAM_ID, PUSH_STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, + ChannelFuture future = encoder.writePushPromise(ctx, STREAM_ID, PUSH_STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, newPromise()); assertTrue(future.isDone()); assertFalse(future.isSuccess()); diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapterTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapterTest.java index 28a5c5b44b..33393afc0a 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapterTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapterTest.java @@ -360,49 +360,6 @@ public class InboundHttp2ToHttpAdapterTest { } } - @Test - public void clientRequestMultipleHeaders() throws Exception { - boostrapEnv(1, 1, 1); - // writeHeaders will implicitly add an END_HEADERS tag each time and so this test does not follow the HTTP - // message flow. We currently accept this message flow and just add the second headers to the trailing headers. - final String text = ""; - final ByteBuf content = Unpooled.copiedBuffer(text.getBytes()); - final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, - "/some/path/resource2", content, true); - try { - HttpHeaders httpHeaders = request.headers(); - httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); - httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length()); - httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16); - HttpHeaders trailingHeaders = request.trailingHeaders(); - trailingHeaders.set(of("FoO"), of("goo")); - trailingHeaders.set(of("foO2"), of("goo2")); - trailingHeaders.add(of("fOo2"), of("goo3")); - final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).path( - new AsciiString("/some/path/resource2")); - final Http2Headers http2Headers2 = new DefaultHttp2Headers() - .set(new AsciiString("foo"), new AsciiString("goo")) - .set(new AsciiString("foo2"), new AsciiString("goo2")) - .add(new AsciiString("foo2"), new AsciiString("goo3")); - runInChannel(clientChannel, new Http2Runnable() { - @Override - public void run() throws Http2Exception { - clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient()); - clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers2, 0, false, newPromiseClient()); - clientHandler.encoder().writeData(ctxClient(), 3, content.retain(), 0, true, newPromiseClient()); - clientChannel.flush(); - } - }); - awaitRequests(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class); - verify(serverListener).messageReceived(requestCaptor.capture()); - capturedRequests = requestCaptor.getAllValues(); - assertEquals(request, capturedRequests.get(0)); - } finally { - request.release(); - } - } - @Test public void clientRequestTrailingHeaders() throws Exception { boostrapEnv(1, 1, 1);