HTTP/2: Fix some errors reported by h2spec.
Motivation: h2spec [1] reported 32 issues [2] with Netty's HTTP/2 implementation. Modifications: Fixed 11 of those issues. Mostly wrong error codes or added missing validation code in the frame reader. Result: 11 fewer errors. h2spec now reports: 70 tests, 48 passed, 1 skipped, 21 failed [1] https://github.com/summerwind/h2spec [2] https://github.com/netty/netty/issues/5761
This commit is contained in:
parent
8cf90f0512
commit
515f8964b4
@ -453,6 +453,11 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
|
||||
@Override
|
||||
public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId,
|
||||
Http2Headers headers, int padding) throws Http2Exception {
|
||||
// A client cannot push.
|
||||
if (connection().isServer()) {
|
||||
throw connectionError(PROTOCOL_ERROR, "A client cannot push.");
|
||||
}
|
||||
|
||||
Http2Stream parentStream = connection.stream(streamId);
|
||||
|
||||
if (shouldIgnoreHeadersOrDataFrame(ctx, streamId, parentStream, "PUSH_PROMISE")) {
|
||||
|
@ -179,7 +179,8 @@ public class DefaultHttp2FrameReader implements Http2FrameReader, Http2FrameSize
|
||||
// Read the header and prepare the unmarshaller to read the frame.
|
||||
payloadLength = in.readUnsignedMedium();
|
||||
if (payloadLength > maxFrameSize) {
|
||||
throw connectionError(PROTOCOL_ERROR, "Frame length: %d exceeds maximum: %d", payloadLength, maxFrameSize);
|
||||
throw connectionError(FRAME_SIZE_ERROR, "Frame length: %d exceeds maximum: %d", payloadLength,
|
||||
maxFrameSize);
|
||||
}
|
||||
frameType = in.readByte();
|
||||
flags = new Http2Flags(in.readUnsignedByte());
|
||||
@ -277,6 +278,7 @@ public class DefaultHttp2FrameReader implements Http2FrameReader, Http2FrameSize
|
||||
}
|
||||
|
||||
private void verifyDataFrame() throws Http2Exception {
|
||||
verifyAssociatedWithAStream();
|
||||
verifyNotProcessingHeaders();
|
||||
verifyPayloadLength(payloadLength);
|
||||
|
||||
@ -287,6 +289,7 @@ public class DefaultHttp2FrameReader implements Http2FrameReader, Http2FrameSize
|
||||
}
|
||||
|
||||
private void verifyHeadersFrame() throws Http2Exception {
|
||||
verifyAssociatedWithAStream();
|
||||
verifyNotProcessingHeaders();
|
||||
verifyPayloadLength(payloadLength);
|
||||
|
||||
@ -298,6 +301,7 @@ public class DefaultHttp2FrameReader implements Http2FrameReader, Http2FrameSize
|
||||
}
|
||||
|
||||
private void verifyPriorityFrame() throws Http2Exception {
|
||||
verifyAssociatedWithAStream();
|
||||
verifyNotProcessingHeaders();
|
||||
|
||||
if (payloadLength != PRIORITY_ENTRY_LENGTH) {
|
||||
@ -307,11 +311,11 @@ public class DefaultHttp2FrameReader implements Http2FrameReader, Http2FrameSize
|
||||
}
|
||||
|
||||
private void verifyRstStreamFrame() throws Http2Exception {
|
||||
verifyAssociatedWithAStream();
|
||||
verifyNotProcessingHeaders();
|
||||
|
||||
if (payloadLength != INT_FIELD_LENGTH) {
|
||||
throw streamError(streamId, FRAME_SIZE_ERROR,
|
||||
"Invalid frame length %d.", payloadLength);
|
||||
throw connectionError(FRAME_SIZE_ERROR, "Invalid frame length %d.", payloadLength);
|
||||
}
|
||||
}
|
||||
|
||||
@ -322,7 +326,7 @@ public class DefaultHttp2FrameReader implements Http2FrameReader, Http2FrameSize
|
||||
throw connectionError(PROTOCOL_ERROR, "A stream ID must be zero.");
|
||||
}
|
||||
if (flags.ack() && payloadLength > 0) {
|
||||
throw connectionError(PROTOCOL_ERROR, "Ack settings frame must have an empty payload.");
|
||||
throw connectionError(FRAME_SIZE_ERROR, "Ack settings frame must have an empty payload.");
|
||||
}
|
||||
if (payloadLength % SETTING_ENTRY_LENGTH > 0) {
|
||||
throw connectionError(FRAME_SIZE_ERROR, "Frame length %d invalid.", payloadLength);
|
||||
@ -370,12 +374,12 @@ public class DefaultHttp2FrameReader implements Http2FrameReader, Http2FrameSize
|
||||
verifyStreamOrConnectionId(streamId, "Stream ID");
|
||||
|
||||
if (payloadLength != INT_FIELD_LENGTH) {
|
||||
throw streamError(streamId, FRAME_SIZE_ERROR,
|
||||
"Invalid frame length %d.", payloadLength);
|
||||
throw connectionError(FRAME_SIZE_ERROR, "Invalid frame length %d.", payloadLength);
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyContinuationFrame() throws Http2Exception {
|
||||
verifyAssociatedWithAStream();
|
||||
verifyPayloadLength(payloadLength);
|
||||
|
||||
if (headersContinuation == null) {
|
||||
@ -397,14 +401,11 @@ public class DefaultHttp2FrameReader implements Http2FrameReader, Http2FrameSize
|
||||
private void readDataFrame(ChannelHandlerContext ctx, ByteBuf payload,
|
||||
Http2FrameListener listener) throws Http2Exception {
|
||||
int padding = readPadding(payload);
|
||||
verifyPadding(padding);
|
||||
|
||||
// Determine how much data there is to read by removing the trailing
|
||||
// padding.
|
||||
int dataLength = lengthWithoutTrailingPadding(payload.readableBytes(), padding);
|
||||
if (dataLength < 0) {
|
||||
throw streamError(streamId, FRAME_SIZE_ERROR,
|
||||
"Frame payload too small for padding.");
|
||||
}
|
||||
|
||||
ByteBuf data = payload.readSlice(dataLength);
|
||||
listener.onDataRead(ctx, streamId, data, padding, flags.endOfStream());
|
||||
@ -416,6 +417,7 @@ public class DefaultHttp2FrameReader implements Http2FrameReader, Http2FrameSize
|
||||
final int headersStreamId = streamId;
|
||||
final Http2Flags headersFlags = flags;
|
||||
final int padding = readPadding(payload);
|
||||
verifyPadding(padding);
|
||||
|
||||
// The callback that is invoked is different depending on whether priority information
|
||||
// is present in the headers frame.
|
||||
@ -423,6 +425,9 @@ public class DefaultHttp2FrameReader implements Http2FrameReader, Http2FrameSize
|
||||
long word1 = payload.readUnsignedInt();
|
||||
final boolean exclusive = (word1 & 0x80000000L) != 0;
|
||||
final int streamDependency = (int) (word1 & 0x7FFFFFFFL);
|
||||
if (streamDependency == streamId) {
|
||||
throw streamError(streamId, PROTOCOL_ERROR, "A stream cannot depend on itself.");
|
||||
}
|
||||
final short weight = (short) (payload.readUnsignedByte() + 1);
|
||||
final ByteBuf fragment = payload.readSlice(lengthWithoutTrailingPadding(payload.readableBytes(), padding));
|
||||
|
||||
@ -480,6 +485,9 @@ public class DefaultHttp2FrameReader implements Http2FrameReader, Http2FrameSize
|
||||
long word1 = payload.readUnsignedInt();
|
||||
boolean exclusive = (word1 & 0x80000000L) != 0;
|
||||
int streamDependency = (int) (word1 & 0x7FFFFFFFL);
|
||||
if (streamDependency == streamId) {
|
||||
throw streamError(streamId, PROTOCOL_ERROR, "A stream cannot depend on itself.");
|
||||
}
|
||||
short weight = (short) (payload.readUnsignedByte() + 1);
|
||||
listener.onPriorityRead(ctx, streamId, streamDependency, weight, exclusive);
|
||||
}
|
||||
@ -505,7 +513,7 @@ public class DefaultHttp2FrameReader implements Http2FrameReader, Http2FrameSize
|
||||
} catch (IllegalArgumentException e) {
|
||||
switch(id) {
|
||||
case SETTINGS_MAX_FRAME_SIZE:
|
||||
throw connectionError(FRAME_SIZE_ERROR, e, e.getMessage());
|
||||
throw connectionError(PROTOCOL_ERROR, e, e.getMessage());
|
||||
case SETTINGS_INITIAL_WINDOW_SIZE:
|
||||
throw connectionError(FLOW_CONTROL_ERROR, e, e.getMessage());
|
||||
default:
|
||||
@ -521,6 +529,7 @@ public class DefaultHttp2FrameReader implements Http2FrameReader, Http2FrameSize
|
||||
Http2FrameListener listener) throws Http2Exception {
|
||||
final int pushPromiseStreamId = streamId;
|
||||
final int padding = readPadding(payload);
|
||||
verifyPadding(padding);
|
||||
final int promisedStreamId = readUnsignedInt(payload);
|
||||
|
||||
// Create a handler that invokes the listener when the header block is complete.
|
||||
@ -599,6 +608,13 @@ public class DefaultHttp2FrameReader implements Http2FrameReader, Http2FrameSize
|
||||
return payload.readUnsignedByte() + 1;
|
||||
}
|
||||
|
||||
private void verifyPadding(int padding) throws Http2Exception {
|
||||
int len = lengthWithoutTrailingPadding(payloadLength, padding);
|
||||
if (len < 0) {
|
||||
throw connectionError(PROTOCOL_ERROR, "Frame payload too small for padding.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The padding parameter consists of the 1 byte pad length field and the trailing padding bytes. This method
|
||||
* returns the number of readable bytes without the trailing padding.
|
||||
@ -740,6 +756,12 @@ public class DefaultHttp2FrameReader implements Http2FrameReader, Http2FrameSize
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyAssociatedWithAStream() throws Http2Exception {
|
||||
if (streamId == 0) {
|
||||
throw connectionError(PROTOCOL_ERROR, "Frame of type %s must be associated with a stream.", frameType);
|
||||
}
|
||||
}
|
||||
|
||||
private static void verifyStreamOrConnectionId(int streamId, String argumentName)
|
||||
throws Http2Exception {
|
||||
if (streamId < 0) {
|
||||
|
@ -99,7 +99,7 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
|
||||
public void gracefulShutdownTimeoutMillis(long gracefulShutdownTimeoutMillis) {
|
||||
if (gracefulShutdownTimeoutMillis < 0) {
|
||||
throw new IllegalArgumentException("gracefulShutdownTimeoutMillis: " + gracefulShutdownTimeoutMillis +
|
||||
" (expected: >= 0)");
|
||||
" (expected: >= 0)");
|
||||
}
|
||||
this.gracefulShutdownTimeoutMillis = gracefulShutdownTimeoutMillis;
|
||||
}
|
||||
@ -264,11 +264,12 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
|
||||
|
||||
// If the input so far doesn't match the preface, break the connection.
|
||||
if (bytesRead == 0 || !ByteBufUtil.equals(in, in.readerIndex(),
|
||||
clientPrefaceString, clientPrefaceString.readerIndex(), bytesRead)) {
|
||||
clientPrefaceString, clientPrefaceString.readerIndex(),
|
||||
bytesRead)) {
|
||||
String receivedBytes = hexDump(in, in.readerIndex(),
|
||||
min(in.readableBytes(), clientPrefaceString.readableBytes()));
|
||||
min(in.readableBytes(), clientPrefaceString.readableBytes()));
|
||||
throw connectionError(PROTOCOL_ERROR, "HTTP/2 client preface string missing or corrupt. " +
|
||||
"Hex dump for received bytes: %s", receivedBytes);
|
||||
"Hex dump for received bytes: %s", receivedBytes);
|
||||
}
|
||||
in.skipBytes(bytesRead);
|
||||
clientPrefaceString.skipBytes(bytesRead);
|
||||
@ -300,7 +301,8 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
|
||||
short flags = in.getUnsignedByte(in.readerIndex() + 4);
|
||||
if (frameType != SETTINGS || (flags & Http2Flags.ACK) != 0) {
|
||||
throw connectionError(PROTOCOL_ERROR, "First received frame was not SETTINGS. " +
|
||||
"Hex dump for first 5 bytes: %s", hexDump(in, in.readerIndex(), 5));
|
||||
"Hex dump for first 5 bytes: %s",
|
||||
hexDump(in, in.readerIndex(), 5));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -434,7 +436,7 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
|
||||
} else {
|
||||
// If there are active streams we should wait until they are all closed before closing the connection.
|
||||
closeListener = new ClosingChannelFutureListener(ctx, promise,
|
||||
gracefulShutdownTimeoutMillis, MILLISECONDS);
|
||||
gracefulShutdownTimeoutMillis, MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@ -604,15 +606,39 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
|
||||
return encoder().frameWriter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a {@code RST_STREAM} frame even if we don't know about the stream. This error condition is most likely
|
||||
* triggered by the first frame of a stream being invalid. That is, there was an error reading the frame before
|
||||
* we could create a new stream.
|
||||
*/
|
||||
private ChannelFuture resetUnknownStream(final ChannelHandlerContext ctx, int streamId, long errorCode,
|
||||
ChannelPromise promise) {
|
||||
ChannelFuture future = frameWriter().writeRstStream(ctx, streamId, errorCode, promise);
|
||||
if (future.isDone()) {
|
||||
closeConnectionOnError(ctx, future);
|
||||
} else {
|
||||
future.addListener(new ChannelFutureListener() {
|
||||
@Override
|
||||
public void operationComplete(ChannelFuture future) throws Exception {
|
||||
closeConnectionOnError(ctx, future);
|
||||
}
|
||||
});
|
||||
}
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture resetStream(final ChannelHandlerContext ctx, int streamId, long errorCode,
|
||||
final ChannelPromise promise) {
|
||||
final ChannelPromise promise) {
|
||||
final Http2Stream stream = connection().stream(streamId);
|
||||
if (stream == null || stream.isResetSent()) {
|
||||
// Don't write a RST_STREAM frame if we are not aware of the stream, or if we have already written one.
|
||||
return promise.setSuccess();
|
||||
if (stream == null) {
|
||||
return resetUnknownStream(ctx, streamId, errorCode, promise);
|
||||
}
|
||||
|
||||
if (stream.isResetSent()) {
|
||||
// Don't write a RST_STREAM frame if we have already written one.
|
||||
return promise.setSuccess();
|
||||
}
|
||||
final ChannelFuture future;
|
||||
if (stream.state() == IDLE) {
|
||||
// We cannot write RST_STREAM frames on IDLE streams https://tools.ietf.org/html/rfc7540#section-6.4.
|
||||
@ -647,8 +673,7 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
|
||||
if (connection.goAwaySent() && lastStreamId > connection.remote().lastStreamKnownByPeer()) {
|
||||
throw connectionError(PROTOCOL_ERROR, "Last stream identifier must not increase between " +
|
||||
"sending multiple GOAWAY frames (was '%d', is '%d').",
|
||||
connection.remote().lastStreamKnownByPeer(),
|
||||
lastStreamId);
|
||||
connection.remote().lastStreamKnownByPeer(), lastStreamId);
|
||||
}
|
||||
connection.goAwaySent(lastStreamId, errorCode, debugData);
|
||||
|
||||
@ -714,6 +739,12 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
|
||||
}
|
||||
}
|
||||
|
||||
private void closeConnectionOnError(ChannelHandlerContext ctx, ChannelFuture future) {
|
||||
if (!future.isSuccess()) {
|
||||
onConnectionError(ctx, future.cause(), null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the client preface string if this is a client connection, otherwise returns {@code null}.
|
||||
*/
|
||||
@ -722,7 +753,7 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
|
||||
}
|
||||
|
||||
private static void processGoAwayWriteResult(final ChannelHandlerContext ctx, final int lastStreamId,
|
||||
final long errorCode, final ByteBuf debugData, ChannelFuture future) {
|
||||
final long errorCode, final ByteBuf debugData, ChannelFuture future) {
|
||||
try {
|
||||
if (future.isSuccess()) {
|
||||
if (errorCode != NO_ERROR.code()) {
|
||||
|
@ -313,13 +313,10 @@ public class Http2ConnectionHandlerTest {
|
||||
@Test
|
||||
public void writeRstOnNonExistantStreamShouldSucceed() throws Exception {
|
||||
handler = newHandler();
|
||||
when(frameWriter.writeRstStream(eq(ctx), eq(NON_EXISTANT_STREAM_ID),
|
||||
eq(STREAM_CLOSED.code()), eq(promise))).thenReturn(future);
|
||||
handler.resetStream(ctx, NON_EXISTANT_STREAM_ID, STREAM_CLOSED.code(), promise);
|
||||
verify(frameWriter, never())
|
||||
.writeRstStream(any(ChannelHandlerContext.class), anyInt(), anyLong(),
|
||||
any(ChannelPromise.class));
|
||||
assertTrue(promise.isDone());
|
||||
assertTrue(promise.isSuccess());
|
||||
assertNull(promise.cause());
|
||||
verify(frameWriter).writeRstStream(eq(ctx), eq(NON_EXISTANT_STREAM_ID), eq(STREAM_CLOSED.code()), eq(promise));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
Loading…
Reference in New Issue
Block a user