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; 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 min;
private final int max; private final int max;
private final AsciiString defaultReasonPhrase; 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. * Simple stream implementation. Streams can be compared to each other by priority.
*/ */
private class DefaultStream implements Http2Stream { private class DefaultStream implements Http2Stream {
private static final byte SENT_STATE_RST = 0x1; private static final byte META_STATE_SENT_RST = 1;
private static final byte SENT_STATE_HEADERS = 0x2; private static final byte META_STATE_SENT_HEADERS = 1 << 1;
private static final byte SENT_STATE_PUSHPROMISE = 0x4; 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 int id;
private final PropertyMap properties = new PropertyMap(); private final PropertyMap properties = new PropertyMap();
private State state; private State state;
private byte sentState; private byte metaState;
DefaultStream(int id, State state) { DefaultStream(int id, State state) {
this.id = id; this.id = id;
@ -398,35 +401,60 @@ public class DefaultHttp2Connection implements Http2Connection {
@Override @Override
public boolean isResetSent() { public boolean isResetSent() {
return (sentState & SENT_STATE_RST) != 0; return (metaState & META_STATE_SENT_RST) != 0;
} }
@Override @Override
public Http2Stream resetSent() { public Http2Stream resetSent() {
sentState |= SENT_STATE_RST; metaState |= META_STATE_SENT_RST;
return this; return this;
} }
@Override @Override
public Http2Stream headersSent() { public Http2Stream headersSent(boolean isInformational) {
sentState |= SENT_STATE_HEADERS; if (!isInformational) {
metaState |= isHeadersSent() ? META_STATE_SENT_TRAILERS : META_STATE_SENT_HEADERS;
}
return this; return this;
} }
@Override @Override
public boolean isHeadersSent() { 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 @Override
public Http2Stream pushPromiseSent() { public Http2Stream pushPromiseSent() {
sentState |= SENT_STATE_PUSHPROMISE; metaState |= META_STATE_SENT_PUSHPROMISE;
return this; return this;
} }
@Override @Override
public boolean isPushPromiseSent() { public boolean isPushPromiseSent() {
return (sentState & SENT_STATE_PUSHPROMISE) != 0; return (metaState & META_STATE_SENT_PUSHPROMISE) != 0;
} }
@Override @Override
@ -599,7 +627,7 @@ public class DefaultHttp2Connection implements Http2Connection {
} }
@Override @Override
public Http2Stream headersSent() { public Http2Stream headersSent(boolean isInformational) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }

View File

@ -16,6 +16,7 @@ package io.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpStatusClass;
import io.netty.handler.codec.http2.Http2Connection.Endpoint; import io.netty.handler.codec.http2.Http2Connection.Endpoint;
import io.netty.util.internal.UnstableApi; import io.netty.util.internal.UnstableApi;
import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLogger;
@ -23,6 +24,7 @@ import io.netty.util.internal.logging.InternalLoggerFactory;
import java.util.List; 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.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT;
import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR; import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR; import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
@ -282,6 +284,14 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
return; 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()) { switch (stream.state()) {
case RESERVED_REMOTE: case RESERVED_REMOTE:
stream.open(endOfStream); stream.open(endOfStream);
@ -305,6 +315,7 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
stream.state()); stream.state());
} }
stream.headersReceived(isInformational);
encoder.flowController().updateDependencyTree(streamId, streamDependency, weight, exclusive); encoder.flowController().updateDependencyTree(streamId, streamDependency, weight, exclusive);
listener.onHeadersRead(ctx, streamId, headers, streamDependency, weight, exclusive, padding, endOfStream); 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.ChannelHandlerContext;
import io.netty.channel.ChannelPromise; import io.netty.channel.ChannelPromise;
import io.netty.channel.CoalescingBufferQueue; import io.netty.channel.CoalescingBufferQueue;
import io.netty.handler.codec.http.HttpStatusClass;
import io.netty.util.internal.UnstableApi; import io.netty.util.internal.UnstableApi;
import java.util.ArrayDeque; 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.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR; import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.netty.handler.codec.http2.Http2Exception.connectionError; 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); 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 @Override
public ChannelFuture writeHeaders(final ChannelHandlerContext ctx, final int streamId, public ChannelFuture writeHeaders(final ChannelHandlerContext ctx, final int streamId,
final Http2Headers headers, final int streamDependency, final short weight, final Http2Headers headers, final int streamDependency, final short weight,
@ -180,6 +191,7 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
// for this stream. // for this stream.
Http2RemoteFlowController flowController = flowController(); Http2RemoteFlowController flowController = flowController();
if (!endOfStream || !flowController.hasFlowControlled(stream)) { if (!endOfStream || !flowController.hasFlowControlled(stream)) {
boolean isInformational = validateHeadersSentState(stream, headers, connection.isServer(), endOfStream);
if (endOfStream) { if (endOfStream) {
final Http2Stream finalStream = stream; final Http2Stream finalStream = stream;
final ChannelFutureListener closeStreamLocalListener = new ChannelFutureListener() { final ChannelFutureListener closeStreamLocalListener = new ChannelFutureListener() {
@ -190,6 +202,7 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
}; };
promise = promise.unvoid().addListener(closeStreamLocalListener); promise = promise.unvoid().addListener(closeStreamLocalListener);
} }
ChannelFuture future = frameWriter.writeHeaders(ctx, streamId, headers, streamDependency, ChannelFuture future = frameWriter.writeHeaders(ctx, streamId, headers, streamDependency,
weight, exclusive, padding, endOfStream, promise); weight, exclusive, padding, endOfStream, promise);
// Writing headers may fail during the encode state if they violate HPACK limits. // 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) { if (failureCause == null) {
// Synchronously set the headersSent flag to ensure that we do not subsequently write // Synchronously set the headersSent flag to ensure that we do not subsequently write
// other headers containing pseudo-header fields. // other headers containing pseudo-header fields.
stream.headersSent(); stream.headersSent(isInformational);
} else { } else {
lifecycleManager.onError(ctx, failureCause); lifecycleManager.onError(ctx, failureCause);
} }
@ -451,6 +464,7 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
@Override @Override
public void write(ChannelHandlerContext ctx, int allowedBytes) { public void write(ChannelHandlerContext ctx, int allowedBytes) {
boolean isInformational = validateHeadersSentState(stream, headers, connection.isServer(), endOfStream);
if (promise.isVoid()) { if (promise.isVoid()) {
promise = ctx.newPromise(); 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. // Writing headers may fail during the encode state if they violate HPACK limits.
Throwable failureCause = f.cause(); Throwable failureCause = f.cause();
if (failureCause == null) { if (failureCause == null) {
stream.headersSent(); stream.headersSent(isInformational);
} else { } else {
lifecycleManager.onError(ctx, failureCause); lifecycleManager.onError(ctx, failureCause);
} }

View File

@ -130,15 +130,41 @@ public interface Http2Stream {
<V> V removeProperty(Http2Connection.PropertyKey key); <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(); 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. * 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.ChannelHandlerContext;
import io.netty.channel.ChannelPromise; import io.netty.channel.ChannelPromise;
import io.netty.channel.DefaultChannelPromise; import io.netty.channel.DefaultChannelPromise;
import io.netty.handler.codec.http.HttpResponseStatus;
import junit.framework.AssertionFailedError; import junit.framework.AssertionFailedError;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; 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.anyInt;
import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.anyShort; 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.doAnswer;
import static org.mockito.Mockito.doNothing; 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.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@ -67,6 +68,8 @@ public class DefaultHttp2ConnectionDecoderTest {
private static final int STREAM_ID = 3; private static final int STREAM_ID = 3;
private static final int PUSH_STREAM_ID = 2; private static final int PUSH_STREAM_ID = 2;
private static final int STREAM_DEPENDENCY_ID = 5; 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 Http2ConnectionDecoder decoder;
private ChannelPromise promise; private ChannelPromise promise;
@ -122,11 +125,49 @@ public class DefaultHttp2ConnectionDecoderTest {
promise = new DefaultChannelPromise(channel); promise = new DefaultChannelPromise(channel);
final AtomicInteger headersReceivedState = new AtomicInteger();
when(channel.isActive()).thenReturn(true); when(channel.isActive()).thenReturn(true);
when(stream.id()).thenReturn(STREAM_ID); when(stream.id()).thenReturn(STREAM_ID);
when(stream.state()).thenReturn(OPEN); when(stream.state()).thenReturn(OPEN);
when(stream.open(anyBoolean())).thenReturn(stream); when(stream.open(anyBoolean())).thenReturn(stream);
when(pushStream.id()).thenReturn(PUSH_STREAM_ID); 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>() { doAnswer(new Answer<Http2Stream>() {
@Override @Override
public Http2Stream answer(InvocationOnMock in) throws Throwable { public Http2Stream answer(InvocationOnMock in) throws Throwable {
@ -452,7 +493,65 @@ public class DefaultHttp2ConnectionDecoderTest {
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false)); 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 @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 { public void headersReadForPromisedStreamShouldCloseStream() throws Exception {
when(stream.state()).thenReturn(RESERVED_REMOTE); when(stream.state()).thenReturn(RESERVED_REMOTE);
decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, true); 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.ChannelPipeline;
import io.netty.channel.ChannelPromise; import io.netty.channel.ChannelPromise;
import io.netty.channel.DefaultChannelPromise; import io.netty.channel.DefaultChannelPromise;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http2.Http2RemoteFlowController.FlowControlled; import io.netty.handler.codec.http2.Http2RemoteFlowController.FlowControlled;
import io.netty.util.concurrent.ImmediateEventExecutor; import io.netty.util.concurrent.ImmediateEventExecutor;
import junit.framework.AssertionFailedError; 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.anyInt;
import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.anyShort; import static org.mockito.Mockito.anyShort;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when; 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)); 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 @Test
public void pushPromiseWriteAfterGoAwayReceivedShouldFail() throws Exception { public void pushPromiseWriteAfterGoAwayReceivedShouldFail() throws Exception {
createStream(STREAM_ID, false); createStream(STREAM_ID, false);
goAwayReceived(0); 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()); newPromise());
assertTrue(future.isDone()); assertTrue(future.isDone());
assertFalse(future.isSuccess()); assertFalse(future.isSuccess());

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 @Test
public void clientRequestTrailingHeaders() throws Exception { public void clientRequestTrailingHeaders() throws Exception {
boostrapEnv(1, 1, 1); boostrapEnv(1, 1, 1);