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:
parent
4af47f0ced
commit
a91df58ca1
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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);
|
||||
|
@ -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());
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user