netty/CVE-2021-21295-pre4.patch
2021-04-06 15:28:11 +08:00

772 lines
36 KiB
Diff

From a91df58ca17d5b30c57c46dde5b1d60bb659b029 Mon Sep 17 00:00:00 2001
From: Scott Mitchell <scott_mitchell@apple.com>
Date: Tue, 11 Jul 2017 14:53:49 -0700
Subject: [PATCH] HTTP/2 enforce HTTP message flow
Motivation:
codec-http2 currently does not strictly enforce the HTTP/1.x semantics with respect to the number of headers defined in RFC 7540 Section 8.1 [1]. We currently don't validate the number of headers nor do we validate that the trailing headers should indicate EOS.
[1] https://tools.ietf.org/html/rfc7540#section-8.1
Modifications:
- DefaultHttp2ConnectionDecoder should only allow decoding of a single headers and a single trailers
- DefaultHttp2ConnectionEncoder should only allow encoding of a single headers and optionally a single trailers
Result:
Constraints of RFC 7540 restricting the number of headers/trailers is enforced.
---
.../handler/codec/http/HttpStatusClass.java | 21 ++
.../codec/http2/DefaultHttp2Connection.java | 52 +++--
.../http2/DefaultHttp2ConnectionDecoder.java | 11 +
.../http2/DefaultHttp2ConnectionEncoder.java | 18 +-
.../handler/codec/http2/Http2Stream.java | 32 ++-
.../DefaultHttp2ConnectionDecoderTest.java | 103 ++++++++-
.../DefaultHttp2ConnectionEncoderTest.java | 203 +++++++++++++++++-
.../http2/InboundHttp2ToHttpAdapterTest.java | 43 ----
8 files changed, 419 insertions(+), 64 deletions(-)
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 18375db76a..f129f63fd8 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;
@@ -146,6 +148,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,
@@ -181,6 +192,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() {
@@ -191,6 +203,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.
@@ -198,7 +211,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);
}
@@ -452,6 +465,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();
}
@@ -462,7 +476,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> 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<Boolean>() {
+ @Override
+ public Boolean answer(InvocationOnMock in) throws Throwable {
+ return (headersReceivedState.get() & STATE_RECV_HEADERS) != 0;
+ }
+ }).when(stream).isHeadersReceived();
+ doAnswer(new Answer<Boolean>() {
+ @Override
+ public Boolean answer(InvocationOnMock in) throws Throwable {
+ return (headersReceivedState.get() & STATE_RECV_TRAILERS) != 0;
+ }
+ }).when(stream).isTrailersReceived();
+ doAnswer(new Answer<Http2Stream>() {
+ @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<Http2Stream>() {
@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<FullHttpMessage> 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);
--
2.23.0