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.
This commit is contained in:
Scott Mitchell 2017-07-11 14:53:49 -07:00
parent 4af47f0ced
commit a91df58ca1
8 changed files with 419 additions and 64 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
*/

View File

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

View File

@ -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,6 +344,203 @@ 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);

View File

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