HTTP/2 Simplify Headers Decode Bounds Checking
Motivation: The HPACK decoder keeps state so that the decode method can be called multiple times with successive header fragments. This decoder also requires that a method is called to signify the decoding is complete. At this point status is returned to indicate if the max header size has been violated. Netty always accumulates the header fragments into a single buffer before attempting to HPACK decode process and so keeping state and delaying notification that bounds have been exceeded is not necessary. Modifications: - HPACK Decoder#decode(..) now must be called with a complete header block - HPACK will terminate immediately if the maximum header length, or maximum number of headers is exceeded - Reduce member variables in the HPACK Decoder class because they can now live in the decode(..) method Result: HPACK bounds checking is done earlier and less class state is needed.
This commit is contained in:
parent
95ee705e17
commit
21e8d84b79
@ -24,7 +24,6 @@ import java.io.IOException;
|
||||
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_HEADER_TABLE_SIZE;
|
||||
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_MAX_HEADER_SIZE;
|
||||
import static io.netty.handler.codec.http2.Http2Error.COMPRESSION_ERROR;
|
||||
import static io.netty.handler.codec.http2.Http2Error.ENHANCE_YOUR_CALM;
|
||||
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
|
||||
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
|
||||
|
||||
@ -77,33 +76,14 @@ public class DefaultHttp2HeadersDecoder implements Http2HeadersDecoder, Http2Hea
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Respond to headers block resulting in the maximum header size being exceeded.
|
||||
* @throws Http2Exception If we can not recover from the truncation.
|
||||
*/
|
||||
protected void maxHeaderSizeExceeded() throws Http2Exception {
|
||||
throw connectionError(ENHANCE_YOUR_CALM, "Header size exceeded max allowed bytes (%d)", maxHeaderSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Http2Headers decodeHeaders(ByteBuf headerBlock) throws Http2Exception {
|
||||
try {
|
||||
final Http2Headers headers = new DefaultHttp2Headers(validateHeaders, (int) headerArraySizeAccumulator);
|
||||
decoder.decode(headerBlock, headers);
|
||||
if (decoder.endHeaderBlock()) {
|
||||
maxHeaderSizeExceeded();
|
||||
}
|
||||
|
||||
if (headers.size() > headerTable.maxHeaderListSize()) {
|
||||
throw connectionError(PROTOCOL_ERROR, "Number of headers (%d) exceeds maxHeaderListSize (%d)",
|
||||
headers.size(), headerTable.maxHeaderListSize());
|
||||
}
|
||||
|
||||
headerArraySizeAccumulator = HEADERS_COUNT_WEIGHT_NEW * headers.size() +
|
||||
HEADERS_COUNT_WEIGHT_HISTORICAL * headerArraySizeAccumulator;
|
||||
return headers;
|
||||
} catch (IOException e) {
|
||||
throw connectionError(COMPRESSION_ERROR, e, e.getMessage());
|
||||
} catch (Http2Exception e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
|
@ -32,89 +32,82 @@
|
||||
package io.netty.handler.codec.http2.internal.hpack;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.handler.codec.http2.Http2Exception;
|
||||
import io.netty.handler.codec.http2.Http2Headers;
|
||||
import io.netty.handler.codec.http2.internal.hpack.HpackUtil.IndexType;
|
||||
import io.netty.util.internal.ThrowableUtil;
|
||||
import io.netty.util.AsciiString;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static io.netty.handler.codec.http2.Http2Error.COMPRESSION_ERROR;
|
||||
import static io.netty.handler.codec.http2.Http2Error.ENHANCE_YOUR_CALM;
|
||||
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
|
||||
import static io.netty.util.AsciiString.EMPTY_STRING;
|
||||
|
||||
import static java.lang.Math.min;
|
||||
import static io.netty.util.internal.ThrowableUtil.unknownStackTrace;
|
||||
|
||||
public final class Decoder {
|
||||
|
||||
private static final IOException DECODE_DECOMPRESSION_EXCEPTION = ThrowableUtil.unknownStackTrace(
|
||||
new IOException("HPACK - decompression failure"), Decoder.class, "decode(...)");
|
||||
private static final IOException DECODE_ULE_128_DECOMPRESSION_EXCEPTION = ThrowableUtil.unknownStackTrace(
|
||||
new IOException("HPACK - decompression failure"), Decoder.class, "decodeULE128(...)");
|
||||
private static final IOException DECODE_ILLEGAL_INDEX_VALUE = ThrowableUtil.unknownStackTrace(
|
||||
new IOException("HPACK - illegal index value"), Decoder.class, "decode(...)");
|
||||
private static final IOException INDEX_HEADER_ILLEGAL_INDEX_VALUE = ThrowableUtil.unknownStackTrace(
|
||||
new IOException("HPACK - illegal index value"), Decoder.class, "indexHeader(...)");
|
||||
private static final IOException READ_NAME_ILLEGAL_INDEX_VALUE = ThrowableUtil.unknownStackTrace(
|
||||
new IOException("HPACK - illegal index value"), Decoder.class, "readName(...)");
|
||||
private static final IOException INVALID_MAX_DYNAMIC_TABLE_SIZE = ThrowableUtil.unknownStackTrace(
|
||||
new IOException("HPACK - invalid max dynamic table size"), Decoder.class, "setDynamicTableSize(...)");
|
||||
private static final IOException MAX_DYNAMIC_TABLE_SIZE_CHANGE_REQUIRED = ThrowableUtil.unknownStackTrace(
|
||||
new IOException("HPACK - max dynamic table size change required"), Decoder.class, "decode(...)");
|
||||
private static final Http2Exception DECODE_DECOMPRESSION_EXCEPTION = unknownStackTrace(
|
||||
connectionError(COMPRESSION_ERROR, "HPACK - decompression failure"), Decoder.class, "decode(...)");
|
||||
private static final Http2Exception DECODE_ULE_128_DECOMPRESSION_EXCEPTION = unknownStackTrace(
|
||||
connectionError(COMPRESSION_ERROR, "HPACK - decompression failure"), Decoder.class, "decodeULE128(...)");
|
||||
private static final Http2Exception DECODE_ILLEGAL_INDEX_VALUE = unknownStackTrace(
|
||||
connectionError(COMPRESSION_ERROR, "HPACK - illegal index value"), Decoder.class, "decode(...)");
|
||||
private static final Http2Exception INDEX_HEADER_ILLEGAL_INDEX_VALUE = unknownStackTrace(
|
||||
connectionError(COMPRESSION_ERROR, "HPACK - illegal index value"), Decoder.class, "indexHeader(...)");
|
||||
private static final Http2Exception READ_NAME_ILLEGAL_INDEX_VALUE = unknownStackTrace(
|
||||
connectionError(COMPRESSION_ERROR, "HPACK - illegal index value"), Decoder.class, "readName(...)");
|
||||
private static final Http2Exception INVALID_MAX_DYNAMIC_TABLE_SIZE = unknownStackTrace(
|
||||
connectionError(COMPRESSION_ERROR, "HPACK - invalid max dynamic table size"), Decoder.class,
|
||||
"setDynamicTableSize(...)");
|
||||
private static final Http2Exception MAX_DYNAMIC_TABLE_SIZE_CHANGE_REQUIRED = unknownStackTrace(
|
||||
connectionError(COMPRESSION_ERROR, "HPACK - max dynamic table size change required"), Decoder.class,
|
||||
"decode(...)");
|
||||
private static final byte READ_HEADER_REPRESENTATION = 0;
|
||||
private static final byte READ_MAX_DYNAMIC_TABLE_SIZE = 1;
|
||||
private static final byte READ_INDEXED_HEADER = 2;
|
||||
private static final byte READ_INDEXED_HEADER_NAME = 3;
|
||||
private static final byte READ_LITERAL_HEADER_NAME_LENGTH_PREFIX = 4;
|
||||
private static final byte READ_LITERAL_HEADER_NAME_LENGTH = 5;
|
||||
private static final byte READ_LITERAL_HEADER_NAME = 6;
|
||||
private static final byte READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX = 7;
|
||||
private static final byte READ_LITERAL_HEADER_VALUE_LENGTH = 8;
|
||||
private static final byte READ_LITERAL_HEADER_VALUE = 9;
|
||||
|
||||
private final DynamicTable dynamicTable;
|
||||
private final HuffmanDecoder huffmanDecoder;
|
||||
private final int maxHeaderSize;
|
||||
private final int maxHeadersLength;
|
||||
private int maxDynamicTableSize;
|
||||
private int encoderMaxDynamicTableSize;
|
||||
private boolean maxDynamicTableSizeChangeRequired;
|
||||
|
||||
private long headerSize;
|
||||
private State state;
|
||||
private IndexType indexType;
|
||||
private int index;
|
||||
private boolean huffmanEncoded;
|
||||
private int skipLength;
|
||||
private int nameLength;
|
||||
private int valueLength;
|
||||
private CharSequence name;
|
||||
|
||||
private enum State {
|
||||
READ_HEADER_REPRESENTATION,
|
||||
READ_MAX_DYNAMIC_TABLE_SIZE,
|
||||
READ_INDEXED_HEADER,
|
||||
READ_INDEXED_HEADER_NAME,
|
||||
READ_LITERAL_HEADER_NAME_LENGTH_PREFIX,
|
||||
READ_LITERAL_HEADER_NAME_LENGTH,
|
||||
READ_LITERAL_HEADER_NAME,
|
||||
SKIP_LITERAL_HEADER_NAME,
|
||||
READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX,
|
||||
READ_LITERAL_HEADER_VALUE_LENGTH,
|
||||
READ_LITERAL_HEADER_VALUE,
|
||||
SKIP_LITERAL_HEADER_VALUE
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new decoder.
|
||||
* Create a new instance.
|
||||
* @param maxHeadersLength The maximum size (in bytes) that is allowed for a single header decode operation.
|
||||
* @param maxHeaderTableSize
|
||||
* <a href="https://tools.ietf.org/html/rfc7540#section-6.5.2">SETTINGS_HEADER_TABLE_SIZE</a>.
|
||||
* @param initialHuffmanDecodeCapacity The initial size of the byte array used to do huffman decoding.
|
||||
*/
|
||||
public Decoder(int maxHeaderSize, int maxHeaderTableSize, int initialHuffmanDecodeCapacity) {
|
||||
public Decoder(int maxHeadersLength, int maxHeaderTableSize, int initialHuffmanDecodeCapacity) {
|
||||
dynamicTable = new DynamicTable(maxHeaderTableSize);
|
||||
this.maxHeaderSize = maxHeaderSize;
|
||||
this.maxHeadersLength = maxHeadersLength;
|
||||
maxDynamicTableSize = maxHeaderTableSize;
|
||||
encoderMaxDynamicTableSize = maxHeaderTableSize;
|
||||
maxDynamicTableSizeChangeRequired = false;
|
||||
huffmanDecoder = new HuffmanDecoder(initialHuffmanDecodeCapacity);
|
||||
reset();
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
headerSize = 0;
|
||||
state = State.READ_HEADER_REPRESENTATION;
|
||||
indexType = IndexType.NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the header block into header fields.
|
||||
* <p>
|
||||
* This method assumes the entire header block is contained in {@code in}.
|
||||
*/
|
||||
public void decode(ByteBuf in, Http2Headers headers) throws IOException {
|
||||
public void decode(ByteBuf in, Http2Headers headers) throws Http2Exception {
|
||||
int index = 0;
|
||||
int headersLength = 0;
|
||||
int nameLength = 0;
|
||||
int valueLength = 0;
|
||||
byte state = READ_HEADER_REPRESENTATION;
|
||||
boolean huffmanEncoded = false;
|
||||
CharSequence name = null;
|
||||
IndexType indexType = IndexType.NONE;
|
||||
while (in.isReadable()) {
|
||||
switch (state) {
|
||||
case READ_HEADER_REPRESENTATION:
|
||||
@ -126,95 +119,91 @@ public final class Decoder {
|
||||
if (b < 0) {
|
||||
// Indexed Header Field
|
||||
index = b & 0x7F;
|
||||
if (index == 0) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
throw DECODE_ILLEGAL_INDEX_VALUE;
|
||||
} else if (index == 0x7F) {
|
||||
state = State.READ_INDEXED_HEADER;
|
||||
} else {
|
||||
indexHeader(index, headers);
|
||||
case 0x7F:
|
||||
state = READ_INDEXED_HEADER;
|
||||
break;
|
||||
default:
|
||||
headersLength = indexHeader(index, headers, headersLength);
|
||||
}
|
||||
} else if ((b & 0x40) == 0x40) {
|
||||
// Literal Header Field with Incremental Indexing
|
||||
indexType = IndexType.INCREMENTAL;
|
||||
index = b & 0x3F;
|
||||
if (index == 0) {
|
||||
state = State.READ_LITERAL_HEADER_NAME_LENGTH_PREFIX;
|
||||
} else if (index == 0x3F) {
|
||||
state = State.READ_INDEXED_HEADER_NAME;
|
||||
} else {
|
||||
switch (index) {
|
||||
case 0:
|
||||
state = READ_LITERAL_HEADER_NAME_LENGTH_PREFIX;
|
||||
break;
|
||||
case 0x3F:
|
||||
state = READ_INDEXED_HEADER_NAME;
|
||||
break;
|
||||
default:
|
||||
// Index was stored as the prefix
|
||||
readName(index);
|
||||
state = State.READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;
|
||||
name = readName(index);
|
||||
state = READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;
|
||||
}
|
||||
} else if ((b & 0x20) == 0x20) {
|
||||
// Dynamic Table Size Update
|
||||
index = b & 0x1F;
|
||||
if (index == 0x1F) {
|
||||
state = State.READ_MAX_DYNAMIC_TABLE_SIZE;
|
||||
state = READ_MAX_DYNAMIC_TABLE_SIZE;
|
||||
} else {
|
||||
setDynamicTableSize(index);
|
||||
state = State.READ_HEADER_REPRESENTATION;
|
||||
state = READ_HEADER_REPRESENTATION;
|
||||
}
|
||||
} else {
|
||||
// Literal Header Field without Indexing / never Indexed
|
||||
indexType = ((b & 0x10) == 0x10) ? IndexType.NEVER : IndexType.NONE;
|
||||
index = b & 0x0F;
|
||||
if (index == 0) {
|
||||
state = State.READ_LITERAL_HEADER_NAME_LENGTH_PREFIX;
|
||||
} else if (index == 0x0F) {
|
||||
state = State.READ_INDEXED_HEADER_NAME;
|
||||
} else {
|
||||
switch (index) {
|
||||
case 0:
|
||||
state = READ_LITERAL_HEADER_NAME_LENGTH_PREFIX;
|
||||
break;
|
||||
case 0x0F:
|
||||
state = READ_INDEXED_HEADER_NAME;
|
||||
break;
|
||||
default:
|
||||
// Index was stored as the prefix
|
||||
readName(index);
|
||||
state = State.READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;
|
||||
name = readName(index);
|
||||
state = READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case READ_MAX_DYNAMIC_TABLE_SIZE:
|
||||
int maxSize = decodeULE128(in);
|
||||
if (maxSize == -1) {
|
||||
return;
|
||||
}
|
||||
int maxSize = decodeULE128(in) + index;
|
||||
|
||||
// Check for numerical overflow
|
||||
if (maxSize > Integer.MAX_VALUE - index) {
|
||||
if (maxSize < 0) { // Check for numerical overflow
|
||||
throw DECODE_DECOMPRESSION_EXCEPTION;
|
||||
}
|
||||
|
||||
setDynamicTableSize(index + maxSize);
|
||||
state = State.READ_HEADER_REPRESENTATION;
|
||||
setDynamicTableSize(maxSize);
|
||||
state = READ_HEADER_REPRESENTATION;
|
||||
break;
|
||||
|
||||
case READ_INDEXED_HEADER:
|
||||
int headerIndex = decodeULE128(in);
|
||||
if (headerIndex == -1) {
|
||||
return;
|
||||
}
|
||||
int headerIndex = decodeULE128(in) + index;
|
||||
|
||||
// Check for numerical overflow
|
||||
if (headerIndex > Integer.MAX_VALUE - index) {
|
||||
if (headerIndex < 0) { // Check for numerical overflow
|
||||
throw DECODE_DECOMPRESSION_EXCEPTION;
|
||||
}
|
||||
|
||||
indexHeader(index + headerIndex, headers);
|
||||
state = State.READ_HEADER_REPRESENTATION;
|
||||
headersLength = indexHeader(headerIndex, headers, headersLength);
|
||||
state = READ_HEADER_REPRESENTATION;
|
||||
break;
|
||||
|
||||
case READ_INDEXED_HEADER_NAME:
|
||||
// Header Name matches an entry in the Header Table
|
||||
int nameIndex = decodeULE128(in);
|
||||
if (nameIndex == -1) {
|
||||
return;
|
||||
}
|
||||
int nameIndex = decodeULE128(in) + index;
|
||||
|
||||
// Check for numerical overflow
|
||||
if (nameIndex > Integer.MAX_VALUE - index) {
|
||||
if (nameIndex < 0) { // Check for numerical overflow
|
||||
throw DECODE_DECOMPRESSION_EXCEPTION;
|
||||
}
|
||||
|
||||
readName(index + nameIndex);
|
||||
state = State.READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;
|
||||
name = readName(nameIndex);
|
||||
state = READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;
|
||||
break;
|
||||
|
||||
case READ_LITERAL_HEADER_NAME_LENGTH_PREFIX:
|
||||
@ -222,200 +211,96 @@ public final class Decoder {
|
||||
huffmanEncoded = (b & 0x80) == 0x80;
|
||||
index = b & 0x7F;
|
||||
if (index == 0x7f) {
|
||||
state = State.READ_LITERAL_HEADER_NAME_LENGTH;
|
||||
state = READ_LITERAL_HEADER_NAME_LENGTH;
|
||||
} else {
|
||||
if (nameLength > maxHeadersLength - headersLength) {
|
||||
maxHeaderSizeExceeded();
|
||||
}
|
||||
nameLength = index;
|
||||
|
||||
// Check name length against max header size
|
||||
if (exceedsMaxHeaderSize(nameLength)) {
|
||||
|
||||
if (indexType == IndexType.NONE) {
|
||||
// Name is unused so skip bytes
|
||||
name = EMPTY_STRING;
|
||||
skipLength = nameLength;
|
||||
state = State.SKIP_LITERAL_HEADER_NAME;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check name length against max dynamic table size
|
||||
if (nameLength + HeaderField.HEADER_ENTRY_OVERHEAD > dynamicTable.capacity()) {
|
||||
dynamicTable.clear();
|
||||
name = EMPTY_STRING;
|
||||
skipLength = nameLength;
|
||||
state = State.SKIP_LITERAL_HEADER_NAME;
|
||||
break;
|
||||
}
|
||||
}
|
||||
state = State.READ_LITERAL_HEADER_NAME;
|
||||
state = READ_LITERAL_HEADER_NAME;
|
||||
}
|
||||
break;
|
||||
|
||||
case READ_LITERAL_HEADER_NAME_LENGTH:
|
||||
// Header Name is a Literal String
|
||||
nameLength = decodeULE128(in);
|
||||
if (nameLength == -1) {
|
||||
return;
|
||||
}
|
||||
nameLength = decodeULE128(in) + index;
|
||||
|
||||
// Check for numerical overflow
|
||||
if (nameLength > Integer.MAX_VALUE - index) {
|
||||
if (nameLength < 0) { // Check for numerical overflow
|
||||
throw DECODE_DECOMPRESSION_EXCEPTION;
|
||||
}
|
||||
nameLength += index;
|
||||
|
||||
// Check name length against max header size
|
||||
if (exceedsMaxHeaderSize(nameLength)) {
|
||||
if (indexType == IndexType.NONE) {
|
||||
// Name is unused so skip bytes
|
||||
name = EMPTY_STRING;
|
||||
skipLength = nameLength;
|
||||
state = State.SKIP_LITERAL_HEADER_NAME;
|
||||
break;
|
||||
if (nameLength > maxHeadersLength - headersLength) {
|
||||
maxHeaderSizeExceeded();
|
||||
}
|
||||
|
||||
// Check name length against max dynamic table size
|
||||
if (nameLength + HeaderField.HEADER_ENTRY_OVERHEAD > dynamicTable.capacity()) {
|
||||
dynamicTable.clear();
|
||||
name = EMPTY_STRING;
|
||||
skipLength = nameLength;
|
||||
state = State.SKIP_LITERAL_HEADER_NAME;
|
||||
break;
|
||||
}
|
||||
}
|
||||
state = State.READ_LITERAL_HEADER_NAME;
|
||||
state = READ_LITERAL_HEADER_NAME;
|
||||
break;
|
||||
|
||||
case READ_LITERAL_HEADER_NAME:
|
||||
// Wait until entire name is readable
|
||||
if (in.readableBytes() < nameLength) {
|
||||
return;
|
||||
throw notEnoughDataException(in);
|
||||
}
|
||||
|
||||
name = readStringLiteral(in, nameLength);
|
||||
name = readStringLiteral(in, nameLength, huffmanEncoded);
|
||||
|
||||
state = State.READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;
|
||||
break;
|
||||
|
||||
case SKIP_LITERAL_HEADER_NAME:
|
||||
int skip = min(in.readableBytes(), skipLength);
|
||||
in.skipBytes(skip);
|
||||
skipLength -= skip;
|
||||
|
||||
if (skipLength == 0) {
|
||||
state = State.READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;
|
||||
}
|
||||
state = READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;
|
||||
break;
|
||||
|
||||
case READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX:
|
||||
b = in.readByte();
|
||||
huffmanEncoded = (b & 0x80) == 0x80;
|
||||
index = b & 0x7F;
|
||||
if (index == 0x7f) {
|
||||
state = State.READ_LITERAL_HEADER_VALUE_LENGTH;
|
||||
} else {
|
||||
valueLength = index;
|
||||
|
||||
switch (index) {
|
||||
case 0x7f:
|
||||
state = READ_LITERAL_HEADER_VALUE_LENGTH;
|
||||
break;
|
||||
case 0:
|
||||
headersLength = insertHeader(headers, name, EMPTY_STRING, indexType, headersLength);
|
||||
state = READ_HEADER_REPRESENTATION;
|
||||
break;
|
||||
default:
|
||||
// Check new header size against max header size
|
||||
long newHeaderSize = (long) nameLength + (long) valueLength;
|
||||
if (exceedsMaxHeaderSize(newHeaderSize)) {
|
||||
// truncation will be reported during endHeaderBlock
|
||||
headerSize = maxHeaderSize + 1;
|
||||
|
||||
if (indexType == IndexType.NONE) {
|
||||
// Value is unused so skip bytes
|
||||
state = State.SKIP_LITERAL_HEADER_VALUE;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check new header size against max dynamic table size
|
||||
if (newHeaderSize + HeaderField.HEADER_ENTRY_OVERHEAD > dynamicTable.capacity()) {
|
||||
dynamicTable.clear();
|
||||
state = State.SKIP_LITERAL_HEADER_VALUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (valueLength == 0) {
|
||||
insertHeader(headers, name, EMPTY_STRING, indexType);
|
||||
state = State.READ_HEADER_REPRESENTATION;
|
||||
} else {
|
||||
state = State.READ_LITERAL_HEADER_VALUE;
|
||||
if ((long) index + nameLength > maxHeadersLength - headersLength) {
|
||||
maxHeaderSizeExceeded();
|
||||
}
|
||||
valueLength = index;
|
||||
state = READ_LITERAL_HEADER_VALUE;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case READ_LITERAL_HEADER_VALUE_LENGTH:
|
||||
// Header Value is a Literal String
|
||||
valueLength = decodeULE128(in);
|
||||
if (valueLength == -1) {
|
||||
return;
|
||||
}
|
||||
valueLength = decodeULE128(in) + index;
|
||||
|
||||
// Check for numerical overflow
|
||||
if (valueLength > Integer.MAX_VALUE - index) {
|
||||
if (valueLength < 0) { // Check for numerical overflow
|
||||
throw DECODE_DECOMPRESSION_EXCEPTION;
|
||||
}
|
||||
valueLength += index;
|
||||
|
||||
// Check new header size against max header size
|
||||
long newHeaderSize = (long) nameLength + (long) valueLength;
|
||||
if (newHeaderSize + headerSize > maxHeaderSize) {
|
||||
// truncation will be reported during endHeaderBlock
|
||||
headerSize = maxHeaderSize + 1;
|
||||
|
||||
if (indexType == IndexType.NONE) {
|
||||
// Value is unused so skip bytes
|
||||
state = State.SKIP_LITERAL_HEADER_VALUE;
|
||||
break;
|
||||
if ((long) valueLength + nameLength > maxHeadersLength - headersLength) {
|
||||
maxHeaderSizeExceeded();
|
||||
}
|
||||
|
||||
// Check new header size against max dynamic table size
|
||||
if (newHeaderSize + HeaderField.HEADER_ENTRY_OVERHEAD > dynamicTable.capacity()) {
|
||||
dynamicTable.clear();
|
||||
state = State.SKIP_LITERAL_HEADER_VALUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
state = State.READ_LITERAL_HEADER_VALUE;
|
||||
state = READ_LITERAL_HEADER_VALUE;
|
||||
break;
|
||||
|
||||
case READ_LITERAL_HEADER_VALUE:
|
||||
// Wait until entire value is readable
|
||||
if (in.readableBytes() < valueLength) {
|
||||
return;
|
||||
throw notEnoughDataException(in);
|
||||
}
|
||||
|
||||
CharSequence value = readStringLiteral(in, valueLength);
|
||||
insertHeader(headers, name, value, indexType);
|
||||
state = State.READ_HEADER_REPRESENTATION;
|
||||
break;
|
||||
|
||||
case SKIP_LITERAL_HEADER_VALUE:
|
||||
int skipBytes = min(in.readableBytes(), valueLength);
|
||||
in.skipBytes(skipBytes);
|
||||
valueLength -= skipBytes;
|
||||
if (valueLength == 0) {
|
||||
state = State.READ_HEADER_REPRESENTATION;
|
||||
}
|
||||
CharSequence value = readStringLiteral(in, valueLength, huffmanEncoded);
|
||||
headersLength = insertHeader(headers, name, value, indexType, headersLength);
|
||||
state = READ_HEADER_REPRESENTATION;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException("should not reach here");
|
||||
throw new Error("should not reach here state: " + state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* End the current header block. Returns if the header field has been truncated. This must be
|
||||
* called after the header block has been completely decoded.
|
||||
*/
|
||||
public boolean endHeaderBlock() {
|
||||
boolean truncated = headerSize > maxHeaderSize;
|
||||
reset();
|
||||
return truncated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum table size. If this is below the maximum size of the dynamic table used by
|
||||
* the encoder, the beginning of the next header block MUST signal this change.
|
||||
@ -459,7 +344,7 @@ public final class Decoder {
|
||||
return dynamicTable.getEntry(index + 1);
|
||||
}
|
||||
|
||||
private void setDynamicTableSize(int dynamicTableSize) throws IOException {
|
||||
private void setDynamicTableSize(int dynamicTableSize) throws Http2Exception {
|
||||
if (dynamicTableSize > maxDynamicTableSize) {
|
||||
throw INVALID_MAX_DYNAMIC_TABLE_SIZE;
|
||||
}
|
||||
@ -468,33 +353,33 @@ public final class Decoder {
|
||||
dynamicTable.setCapacity(dynamicTableSize);
|
||||
}
|
||||
|
||||
private void readName(int index) throws IOException {
|
||||
private CharSequence readName(int index) throws Http2Exception {
|
||||
if (index <= StaticTable.length) {
|
||||
HeaderField headerField = StaticTable.getEntry(index);
|
||||
name = headerField.name;
|
||||
} else if (index - StaticTable.length <= dynamicTable.length()) {
|
||||
return headerField.name;
|
||||
}
|
||||
if (index - StaticTable.length <= dynamicTable.length()) {
|
||||
HeaderField headerField = dynamicTable.getEntry(index - StaticTable.length);
|
||||
name = headerField.name;
|
||||
} else {
|
||||
return headerField.name;
|
||||
}
|
||||
throw READ_NAME_ILLEGAL_INDEX_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
private void indexHeader(int index, Http2Headers headers) throws IOException {
|
||||
private int indexHeader(int index, Http2Headers headers, int headersLength) throws Http2Exception {
|
||||
if (index <= StaticTable.length) {
|
||||
HeaderField headerField = StaticTable.getEntry(index);
|
||||
addHeader(headers, headerField.name, headerField.value);
|
||||
} else if (index - StaticTable.length <= dynamicTable.length()) {
|
||||
return addHeader(headers, headerField.name, headerField.value, headersLength);
|
||||
}
|
||||
if (index - StaticTable.length <= dynamicTable.length()) {
|
||||
HeaderField headerField = dynamicTable.getEntry(index - StaticTable.length);
|
||||
addHeader(headers, headerField.name, headerField.value);
|
||||
} else {
|
||||
return addHeader(headers, headerField.name, headerField.value, headersLength);
|
||||
}
|
||||
throw INDEX_HEADER_ILLEGAL_INDEX_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
private void insertHeader(Http2Headers headers, CharSequence name, CharSequence value,
|
||||
IndexType indexType) {
|
||||
addHeader(headers, name, value);
|
||||
private int insertHeader(Http2Headers headers, CharSequence name, CharSequence value, IndexType indexType,
|
||||
int headerSize) throws Http2Exception {
|
||||
headerSize = addHeader(headers, name, value, headerSize);
|
||||
|
||||
switch (indexType) {
|
||||
case NONE:
|
||||
@ -506,66 +391,61 @@ public final class Decoder {
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException("should not reach here");
|
||||
}
|
||||
throw new Error("should not reach here");
|
||||
}
|
||||
|
||||
private void addHeader(Http2Headers headers, CharSequence name, CharSequence value) {
|
||||
long newSize = headerSize + name.length() + value.length();
|
||||
if (newSize <= maxHeaderSize) {
|
||||
return headerSize;
|
||||
}
|
||||
|
||||
private int addHeader(Http2Headers headers, CharSequence name, CharSequence value, int headersLength)
|
||||
throws Http2Exception {
|
||||
long newHeadersLength = (long) headersLength + name.length() + value.length();
|
||||
if (newHeadersLength > maxHeadersLength) {
|
||||
maxHeaderSizeExceeded();
|
||||
}
|
||||
headers.add(name, value);
|
||||
headerSize = (int) newSize;
|
||||
} else {
|
||||
// truncation will be reported during endHeaderBlock
|
||||
headerSize = maxHeaderSize + 1;
|
||||
}
|
||||
return (int) newHeadersLength;
|
||||
}
|
||||
|
||||
private boolean exceedsMaxHeaderSize(long size) {
|
||||
// Check new header size against max header size
|
||||
if (size + headerSize <= maxHeaderSize) {
|
||||
return false;
|
||||
/**
|
||||
* Respond to headers block resulting in the maximum header size being exceeded.
|
||||
* @throws Http2Exception If we can not recover from the truncation.
|
||||
*/
|
||||
private void maxHeaderSizeExceeded() throws Http2Exception {
|
||||
throw connectionError(ENHANCE_YOUR_CALM, "Header size exceeded max allowed bytes (%d)", maxHeadersLength);
|
||||
}
|
||||
|
||||
// truncation will be reported during endHeaderBlock
|
||||
headerSize = maxHeaderSize + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
private CharSequence readStringLiteral(ByteBuf in, int length) throws IOException {
|
||||
private CharSequence readStringLiteral(ByteBuf in, int length, boolean huffmanEncoded) throws Http2Exception {
|
||||
if (huffmanEncoded) {
|
||||
return huffmanDecoder.decode(in, length);
|
||||
} else {
|
||||
}
|
||||
byte[] buf = new byte[length];
|
||||
in.readBytes(buf);
|
||||
return new AsciiString(buf, false);
|
||||
}
|
||||
|
||||
private static IllegalArgumentException notEnoughDataException(ByteBuf in) {
|
||||
return new IllegalArgumentException("decode only works with an entire header block! " + in);
|
||||
}
|
||||
|
||||
// Unsigned Little Endian Base 128 Variable-Length Integer Encoding
|
||||
private static int decodeULE128(ByteBuf in) throws IOException {
|
||||
in.markReaderIndex();
|
||||
int result = 0;
|
||||
int shift = 0;
|
||||
while (shift < 32) {
|
||||
if (!in.isReadable()) {
|
||||
// Buffer does not contain entire integer,
|
||||
// reset reader index and return -1.
|
||||
in.resetReaderIndex();
|
||||
return -1;
|
||||
}
|
||||
byte b = in.readByte();
|
||||
private static int decodeULE128(ByteBuf in) throws Http2Exception {
|
||||
final int writerIndex = in.writerIndex();
|
||||
for (int readerIndex = in.readerIndex(), shift = 0, result = 0;
|
||||
readerIndex < writerIndex; ++readerIndex, shift += 7) {
|
||||
byte b = in.getByte(readerIndex);
|
||||
if (shift == 28 && (b & 0xF8) != 0) {
|
||||
in.readerIndex(readerIndex + 1);
|
||||
break;
|
||||
}
|
||||
result |= (b & 0x7F) << shift;
|
||||
|
||||
if ((b & 0x80) == 0) {
|
||||
return result;
|
||||
in.readerIndex(readerIndex + 1);
|
||||
return result | ((b & 0x7F) << shift);
|
||||
}
|
||||
shift += 7;
|
||||
result |= (b & 0x7F) << shift;
|
||||
}
|
||||
// Value exceeds Integer.MAX_VALUE
|
||||
in.resetReaderIndex();
|
||||
|
||||
throw DECODE_ULE_128_DECOMPRESSION_EXCEPTION;
|
||||
}
|
||||
}
|
||||
|
@ -31,23 +31,26 @@
|
||||
*/
|
||||
package io.netty.handler.codec.http2.internal.hpack;
|
||||
|
||||
import io.netty.util.internal.ThrowableUtil;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.handler.codec.http2.Http2Exception;
|
||||
import io.netty.util.AsciiString;
|
||||
import io.netty.util.ByteProcessor;
|
||||
import io.netty.util.internal.ObjectUtil;
|
||||
import io.netty.util.internal.ThrowableUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static io.netty.handler.codec.http2.Http2Error.COMPRESSION_ERROR;
|
||||
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
|
||||
import static io.netty.handler.codec.http2.internal.hpack.HpackUtil.HUFFMAN_CODES;
|
||||
import static io.netty.handler.codec.http2.internal.hpack.HpackUtil.HUFFMAN_CODE_LENGTHS;
|
||||
|
||||
final class HuffmanDecoder {
|
||||
|
||||
private static final IOException EOS_DECODED = ThrowableUtil.unknownStackTrace(
|
||||
new IOException("HPACK - EOS Decoded"), HuffmanDecoder.class, "decode(...)");
|
||||
private static final IOException INVALID_PADDING = ThrowableUtil.unknownStackTrace(
|
||||
new IOException("HPACK - Invalid Padding"), HuffmanDecoder.class, "decode(...)");
|
||||
private static final Http2Exception EOS_DECODED = ThrowableUtil.unknownStackTrace(
|
||||
connectionError(COMPRESSION_ERROR, "HPACK - EOS Decoded"), HuffmanDecoder.class, "decode(...)");
|
||||
private static final Http2Exception INVALID_PADDING = ThrowableUtil.unknownStackTrace(
|
||||
connectionError(COMPRESSION_ERROR, "HPACK - Invalid Padding"), HuffmanDecoder.class, "decode(...)");
|
||||
|
||||
private static final Node ROOT = buildTree(HUFFMAN_CODES, HUFFMAN_CODE_LENGTHS);
|
||||
|
||||
@ -65,7 +68,7 @@ final class HuffmanDecoder {
|
||||
* @throws IOException if an I/O error occurs. In particular, an <code>IOException</code> may be
|
||||
* thrown if the output stream has been closed.
|
||||
*/
|
||||
public AsciiString decode(ByteBuf buf, int length) throws IOException {
|
||||
public AsciiString decode(ByteBuf buf, int length) throws Http2Exception {
|
||||
processor.reset();
|
||||
buf.forEachByte(buf.readerIndex(), length, processor);
|
||||
buf.skipBytes(length);
|
||||
@ -178,7 +181,7 @@ final class HuffmanDecoder {
|
||||
* there is too much padding.
|
||||
*/
|
||||
@Override
|
||||
public boolean process(byte value) throws IOException {
|
||||
public boolean process(byte value) throws Http2Exception {
|
||||
current = (current << 8) | (value & 0xFF);
|
||||
currentBits += 8;
|
||||
symbolBits += 8;
|
||||
@ -200,7 +203,7 @@ final class HuffmanDecoder {
|
||||
return true;
|
||||
}
|
||||
|
||||
AsciiString end() throws IOException {
|
||||
AsciiString end() throws Http2Exception {
|
||||
/*
|
||||
* We have consumed all the bytes in buf, but haven't consumed all the symbols. We may be on
|
||||
* a partial symbol, so consume until there is nothing left. This will loop at most 2 times.
|
||||
|
@ -33,17 +33,14 @@ package io.netty.handler.codec.http2.internal.hpack;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.handler.codec.http2.Http2Exception;
|
||||
import io.netty.handler.codec.http2.Http2Headers;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static io.netty.util.AsciiString.EMPTY_STRING;
|
||||
import static io.netty.util.AsciiString.of;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.times;
|
||||
@ -62,7 +59,7 @@ public class DecoderTest {
|
||||
return Hex.encodeHexString(s.getBytes());
|
||||
}
|
||||
|
||||
private void decode(String encoded) throws IOException {
|
||||
private void decode(String encoded) throws Http2Exception {
|
||||
byte[] b = Hex.decodeHex(encoded.toCharArray());
|
||||
ByteBuf in = Unpooled.wrappedBuffer(b);
|
||||
try {
|
||||
@ -79,7 +76,7 @@ public class DecoderTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLiteralHuffmanEncodedWithEmptyNameAndValue() throws IOException {
|
||||
public void testLiteralHuffmanEncodedWithEmptyNameAndValue() throws Http2Exception {
|
||||
byte[] input = {0, (byte) 0x80, 0};
|
||||
ByteBuf in = Unpooled.wrappedBuffer(input);
|
||||
try {
|
||||
@ -90,8 +87,8 @@ public class DecoderTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testLiteralHuffmanEncodedWithPaddingGreaterThan7Throws() throws IOException {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testLiteralHuffmanEncodedWithPaddingGreaterThan7Throws() throws Http2Exception {
|
||||
byte[] input = {0, (byte) 0x81, -1};
|
||||
ByteBuf in = Unpooled.wrappedBuffer(input);
|
||||
try {
|
||||
@ -101,8 +98,8 @@ public class DecoderTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testLiteralHuffmanEncodedWithDecodingEOSThrows() throws IOException {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testLiteralHuffmanEncodedWithDecodingEOSThrows() throws Http2Exception {
|
||||
byte[] input = {0, (byte) 0x84, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF};
|
||||
ByteBuf in = Unpooled.wrappedBuffer(input);
|
||||
try {
|
||||
@ -112,8 +109,8 @@ public class DecoderTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testLiteralHuffmanEncodedWithPaddingNotCorrespondingToMSBThrows() throws IOException {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testLiteralHuffmanEncodedWithPaddingNotCorrespondingToMSBThrows() throws Http2Exception {
|
||||
byte[] input = {0, (byte) 0x81, 0};
|
||||
ByteBuf in = Unpooled.wrappedBuffer(input);
|
||||
try {
|
||||
@ -123,41 +120,39 @@ public class DecoderTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIncompleteIndex() throws IOException {
|
||||
// Verify incomplete indices are unread
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testIncompleteIndex() throws Http2Exception, Http2Exception {
|
||||
byte[] compressed = Hex.decodeHex("FFF0".toCharArray());
|
||||
ByteBuf in = Unpooled.wrappedBuffer(compressed);
|
||||
try {
|
||||
decoder.decode(in, mockHeaders);
|
||||
assertEquals(1, in.readableBytes());
|
||||
decoder.decode(in, mockHeaders);
|
||||
assertEquals(1, in.readableBytes());
|
||||
} finally {
|
||||
in.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testUnusedIndex() throws IOException {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testUnusedIndex() throws Http2Exception {
|
||||
// Index 0 is not used
|
||||
decode("80");
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testIllegalIndex() throws IOException {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testIllegalIndex() throws Http2Exception {
|
||||
// Index larger than the header table
|
||||
decode("FF00");
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testInsidiousIndex() throws IOException {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testInsidiousIndex() throws Http2Exception {
|
||||
// Insidious index so the last shift causes sign overflow
|
||||
decode("FF8080808008");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDynamicTableSizeUpdate() throws Exception {
|
||||
public void testDynamicTableSizeUpdate() throws Http2Exception {
|
||||
decode("20");
|
||||
assertEquals(0, decoder.getMaxHeaderTableSize());
|
||||
decode("3FE11F");
|
||||
@ -165,58 +160,57 @@ public class DecoderTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDynamicTableSizeUpdateRequired() throws Exception {
|
||||
public void testDynamicTableSizeUpdateRequired() throws Http2Exception {
|
||||
decoder.setMaxHeaderTableSize(32);
|
||||
decode("3F00");
|
||||
assertEquals(31, decoder.getMaxHeaderTableSize());
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testIllegalDynamicTableSizeUpdate() throws Exception {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testIllegalDynamicTableSizeUpdate() throws Http2Exception {
|
||||
// max header table size = MAX_HEADER_TABLE_SIZE + 1
|
||||
decode("3FE21F");
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testInsidiousMaxDynamicTableSize() throws IOException {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testInsidiousMaxDynamicTableSize() throws Http2Exception {
|
||||
// max header table size sign overflow
|
||||
decode("3FE1FFFFFF07");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReduceMaxDynamicTableSize() throws Exception {
|
||||
public void testReduceMaxDynamicTableSize() throws Http2Exception {
|
||||
decoder.setMaxHeaderTableSize(0);
|
||||
assertEquals(0, decoder.getMaxHeaderTableSize());
|
||||
decode("2081");
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testTooLargeDynamicTableSizeUpdate() throws Exception {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testTooLargeDynamicTableSizeUpdate() throws Http2Exception {
|
||||
decoder.setMaxHeaderTableSize(0);
|
||||
assertEquals(0, decoder.getMaxHeaderTableSize());
|
||||
decode("21"); // encoder max header table size not small enough
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testMissingDynamicTableSizeUpdate() throws Exception {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testMissingDynamicTableSizeUpdate() throws Http2Exception {
|
||||
decoder.setMaxHeaderTableSize(0);
|
||||
assertEquals(0, decoder.getMaxHeaderTableSize());
|
||||
decode("81");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLiteralWithIncrementalIndexingWithEmptyName() throws Exception {
|
||||
public void testLiteralWithIncrementalIndexingWithEmptyName() throws Http2Exception {
|
||||
decode("400005" + hex("value"));
|
||||
verify(mockHeaders, times(1)).add(EMPTY_STRING, of("value"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLiteralWithIncrementalIndexingCompleteEviction() throws Exception {
|
||||
public void testLiteralWithIncrementalIndexingCompleteEviction() throws Http2Exception {
|
||||
// Verify indexed host header
|
||||
decode("4004" + hex("name") + "05" + hex("value"));
|
||||
verify(mockHeaders).add(of("name"), of("value"));
|
||||
verifyNoMoreInteractions(mockHeaders);
|
||||
assertFalse(decoder.endHeaderBlock());
|
||||
|
||||
reset(mockHeaders);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
@ -232,7 +226,7 @@ public class DecoderTest {
|
||||
decode(sb.toString());
|
||||
verify(mockHeaders).add(of(":authority"), of(value));
|
||||
verifyNoMoreInteractions(mockHeaders);
|
||||
assertFalse(decoder.endHeaderBlock());
|
||||
reset(mockHeaders);
|
||||
|
||||
// Verify next header is inserted at index 62
|
||||
decode("4004" + hex("name") + "05" + hex("value") + "BE");
|
||||
@ -240,29 +234,8 @@ public class DecoderTest {
|
||||
verifyNoMoreInteractions(mockHeaders);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLiteralWithIncrementalIndexingWithLargeName() throws Exception {
|
||||
// Ignore header name that exceeds max header size
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("407F817F");
|
||||
for (int i = 0; i < 16384; i++) {
|
||||
sb.append("61"); // 'a'
|
||||
}
|
||||
sb.append("00");
|
||||
decode(sb.toString());
|
||||
verifyNoMoreInteractions(mockHeaders);
|
||||
|
||||
// Verify header block is reported as truncated
|
||||
assertTrue(decoder.endHeaderBlock());
|
||||
|
||||
// Verify next header is inserted at index 62
|
||||
decode("4004" + hex("name") + "05" + hex("value") + "BE");
|
||||
verify(mockHeaders, times(2)).add(of("name"), of("value"));
|
||||
verifyNoMoreInteractions(mockHeaders);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLiteralWithIncrementalIndexingWithLargeValue() throws Exception {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testLiteralWithIncrementalIndexingWithLargeValue() throws Http2Exception {
|
||||
// Ignore header that exceeds max header size
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("4004");
|
||||
@ -272,25 +245,16 @@ public class DecoderTest {
|
||||
sb.append("61"); // 'a'
|
||||
}
|
||||
decode(sb.toString());
|
||||
verifyNoMoreInteractions(mockHeaders);
|
||||
|
||||
// Verify header block is reported as truncated
|
||||
assertTrue(decoder.endHeaderBlock());
|
||||
|
||||
// Verify next header is inserted at index 62
|
||||
decode("4004" + hex("name") + "05" + hex("value") + "BE");
|
||||
verify(mockHeaders, times(2)).add(of("name"), of("value"));
|
||||
verifyNoMoreInteractions(mockHeaders);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLiteralWithoutIndexingWithEmptyName() throws Exception {
|
||||
public void testLiteralWithoutIndexingWithEmptyName() throws Http2Exception {
|
||||
decode("000005" + hex("value"));
|
||||
verify(mockHeaders, times(1)).add(EMPTY_STRING, of("value"));
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testLiteralWithoutIndexingWithLargeName() throws Exception {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testLiteralWithoutIndexingWithLargeName() throws Http2Exception {
|
||||
// Ignore header name that exceeds max header size
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("007F817F");
|
||||
@ -299,17 +263,10 @@ public class DecoderTest {
|
||||
}
|
||||
sb.append("00");
|
||||
decode(sb.toString());
|
||||
verifyNoMoreInteractions(mockHeaders);
|
||||
|
||||
// Verify header block is reported as truncated
|
||||
assertTrue(decoder.endHeaderBlock());
|
||||
|
||||
// Verify table is unmodified
|
||||
decode("BE");
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testLiteralWithoutIndexingWithLargeValue() throws Exception {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testLiteralWithoutIndexingWithLargeValue() throws Http2Exception {
|
||||
// Ignore header that exceeds max header size
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("0004");
|
||||
@ -319,23 +276,16 @@ public class DecoderTest {
|
||||
sb.append("61"); // 'a'
|
||||
}
|
||||
decode(sb.toString());
|
||||
verifyNoMoreInteractions(mockHeaders);
|
||||
|
||||
// Verify header block is reported as truncated
|
||||
assertTrue(decoder.endHeaderBlock());
|
||||
|
||||
// Verify table is unmodified
|
||||
decode("BE");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLiteralNeverIndexedWithEmptyName() throws Exception {
|
||||
public void testLiteralNeverIndexedWithEmptyName() throws Http2Exception {
|
||||
decode("100005" + hex("value"));
|
||||
verify(mockHeaders, times(1)).add(EMPTY_STRING, of("value"));
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testLiteralNeverIndexedWithLargeName() throws Exception {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testLiteralNeverIndexedWithLargeName() throws Http2Exception {
|
||||
// Ignore header name that exceeds max header size
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("107F817F");
|
||||
@ -344,17 +294,10 @@ public class DecoderTest {
|
||||
}
|
||||
sb.append("00");
|
||||
decode(sb.toString());
|
||||
verifyNoMoreInteractions(mockHeaders);
|
||||
|
||||
// Verify header block is reported as truncated
|
||||
assertTrue(decoder.endHeaderBlock());
|
||||
|
||||
// Verify table is unmodified
|
||||
decode("BE");
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testLiteralNeverIndexedWithLargeValue() throws Exception {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testLiteralNeverIndexedWithLargeValue() throws Http2Exception {
|
||||
// Ignore header that exceeds max header size
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("1004");
|
||||
@ -364,12 +307,5 @@ public class DecoderTest {
|
||||
sb.append("61"); // 'a'
|
||||
}
|
||||
decode(sb.toString());
|
||||
verifyNoMoreInteractions(mockHeaders);
|
||||
|
||||
// Verify header block is reported as truncated
|
||||
assertTrue(decoder.endHeaderBlock());
|
||||
|
||||
// Verify table is unmodified
|
||||
decode("BE");
|
||||
}
|
||||
}
|
||||
|
@ -33,8 +33,6 @@
|
||||
|
||||
package io.netty.handler.codec.http2.internal.hpack;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Extracted from org/apache/commons/codec/binary/Hex.java Copyright Apache Software Foundation
|
||||
*/
|
||||
@ -62,14 +60,14 @@ final class Hex {
|
||||
*
|
||||
* @param data An array of characters containing hexadecimal digits
|
||||
* @return A byte array containing binary data decoded from the supplied char array.
|
||||
* @throws IOException Thrown if an odd number or illegal of characters is supplied
|
||||
* @throws IllegalArgumentException Thrown if an odd number or illegal of characters is supplied
|
||||
*/
|
||||
public static byte[] decodeHex(char[] data) throws IOException {
|
||||
public static byte[] decodeHex(char[] data) {
|
||||
|
||||
int len = data.length;
|
||||
|
||||
if ((len & 0x01) != 0) {
|
||||
throw new IOException("Odd number of characters.");
|
||||
throw new IllegalArgumentException("Odd number of characters.");
|
||||
}
|
||||
|
||||
byte[] out = new byte[len >> 1];
|
||||
@ -152,12 +150,12 @@ final class Hex {
|
||||
* @param ch A character to convert to an integer digit
|
||||
* @param index The index of the character in the source
|
||||
* @return An integer
|
||||
* @throws IOException Thrown if ch is an illegal hex character
|
||||
* @throws IllegalArgumentException Thrown if ch is an illegal hex character
|
||||
*/
|
||||
protected static int toDigit(char ch, int index) throws IOException {
|
||||
protected static int toDigit(char ch, int index) throws IllegalArgumentException {
|
||||
int digit = Character.digit(ch, 16);
|
||||
if (digit == -1) {
|
||||
throw new IOException("Illegal hexadecimal character " + ch + " at index " + index);
|
||||
throw new IllegalArgumentException("Illegal hexadecimal character " + ch + " at index " + index);
|
||||
}
|
||||
return digit;
|
||||
}
|
||||
|
@ -33,18 +33,18 @@ package io.netty.handler.codec.http2.internal.hpack;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.handler.codec.http2.Http2Exception;
|
||||
import io.netty.util.AsciiString;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
|
||||
public class HuffmanTest {
|
||||
|
||||
@Test
|
||||
public void testHuffman() throws IOException {
|
||||
public void testHuffman() throws Http2Exception {
|
||||
String s = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
roundTrip(s.substring(0, i));
|
||||
@ -56,8 +56,8 @@ public class HuffmanTest {
|
||||
roundTrip(buf);
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testDecodeEOS() throws IOException {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testDecodeEOS() throws Http2Exception {
|
||||
byte[] buf = new byte[4];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
buf[i] = (byte) 0xFF;
|
||||
@ -65,51 +65,51 @@ public class HuffmanTest {
|
||||
decode(newHuffmanDecoder(), buf);
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testDecodeIllegalPadding() throws IOException {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testDecodeIllegalPadding() throws Http2Exception {
|
||||
byte[] buf = new byte[1];
|
||||
buf[0] = 0x00; // '0', invalid padding
|
||||
decode(newHuffmanDecoder(), buf);
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testDecodeExtraPadding() throws IOException {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testDecodeExtraPadding() throws Http2Exception {
|
||||
byte[] buf = makeBuf(0x0f, 0xFF); // '1', 'EOS'
|
||||
decode(newHuffmanDecoder(), buf);
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testDecodeExtraPadding1byte() throws IOException {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testDecodeExtraPadding1byte() throws Http2Exception {
|
||||
byte[] buf = makeBuf(0xFF);
|
||||
decode(newHuffmanDecoder(), buf);
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testDecodeExtraPadding2byte() throws IOException {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testDecodeExtraPadding2byte() throws Http2Exception {
|
||||
byte[] buf = makeBuf(0x1F, 0xFF); // 'a'
|
||||
decode(newHuffmanDecoder(), buf);
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testDecodeExtraPadding3byte() throws IOException {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testDecodeExtraPadding3byte() throws Http2Exception {
|
||||
byte[] buf = makeBuf(0x1F, 0xFF, 0xFF); // 'a'
|
||||
decode(newHuffmanDecoder(), buf);
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testDecodeExtraPadding4byte() throws IOException {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testDecodeExtraPadding4byte() throws Http2Exception {
|
||||
byte[] buf = makeBuf(0x1F, 0xFF, 0xFF, 0xFF); // 'a'
|
||||
decode(newHuffmanDecoder(), buf);
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testDecodeExtraPadding29bit() throws IOException {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testDecodeExtraPadding29bit() throws Http2Exception {
|
||||
byte[] buf = makeBuf(0xFF, 0x9F, 0xFF, 0xFF, 0xFF); // '|'
|
||||
decode(newHuffmanDecoder(), buf);
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testDecodePartialSymbol() throws IOException {
|
||||
@Test(expected = Http2Exception.class)
|
||||
public void testDecodePartialSymbol() throws Http2Exception {
|
||||
byte[] buf = makeBuf(0x52, 0xBC, 0x30, 0xFF, 0xFF, 0xFF, 0xFF); // " pFA\x00", 31 bits of padding, a.k.a. EOS
|
||||
decode(newHuffmanDecoder(), buf);
|
||||
}
|
||||
@ -122,21 +122,21 @@ public class HuffmanTest {
|
||||
return buf;
|
||||
}
|
||||
|
||||
private static void roundTrip(String s) throws IOException {
|
||||
private static void roundTrip(String s) throws Http2Exception {
|
||||
roundTrip(new HuffmanEncoder(), newHuffmanDecoder(), s);
|
||||
}
|
||||
|
||||
private static void roundTrip(HuffmanEncoder encoder, HuffmanDecoder decoder, String s)
|
||||
throws IOException {
|
||||
throws Http2Exception {
|
||||
roundTrip(encoder, decoder, s.getBytes());
|
||||
}
|
||||
|
||||
private static void roundTrip(byte[] buf) throws IOException {
|
||||
private static void roundTrip(byte[] buf) throws Http2Exception {
|
||||
roundTrip(new HuffmanEncoder(), newHuffmanDecoder(), buf);
|
||||
}
|
||||
|
||||
private static void roundTrip(HuffmanEncoder encoder, HuffmanDecoder decoder, byte[] buf)
|
||||
throws IOException {
|
||||
throws Http2Exception {
|
||||
ByteBuf buffer = Unpooled.buffer();
|
||||
try {
|
||||
encoder.encode(buffer, new AsciiString(buf, false));
|
||||
@ -151,7 +151,7 @@ public class HuffmanTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] decode(HuffmanDecoder decoder, byte[] bytes) throws IOException {
|
||||
private static byte[] decode(HuffmanDecoder decoder, byte[] bytes) throws Http2Exception {
|
||||
ByteBuf buffer = Unpooled.wrappedBuffer(bytes);
|
||||
try {
|
||||
AsciiString decoded = decoder.decode(buffer, buffer.readableBytes());
|
||||
|
@ -192,13 +192,12 @@ final class TestCase {
|
||||
}
|
||||
}
|
||||
|
||||
private static List<HeaderField> decode(Decoder decoder, byte[] expected) throws IOException {
|
||||
private static List<HeaderField> decode(Decoder decoder, byte[] expected) throws Exception {
|
||||
ByteBuf in = Unpooled.wrappedBuffer(expected);
|
||||
try {
|
||||
List<HeaderField> headers = new ArrayList<HeaderField>();
|
||||
TestHeaderListener listener = new TestHeaderListener(headers);
|
||||
decoder.decode(in, listener);
|
||||
decoder.endHeaderBlock();
|
||||
return headers;
|
||||
} finally {
|
||||
in.release();
|
||||
|
@ -34,6 +34,7 @@ package io.netty.microbench.http2.internal.hpack;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.handler.codec.http2.DefaultHttp2Headers;
|
||||
import io.netty.handler.codec.http2.Http2Exception;
|
||||
import io.netty.handler.codec.http2.Http2Headers;
|
||||
import io.netty.handler.codec.http2.internal.hpack.Decoder;
|
||||
import io.netty.handler.codec.http2.internal.hpack.Encoder;
|
||||
@ -47,7 +48,6 @@ import org.openjdk.jmh.annotations.Setup;
|
||||
import org.openjdk.jmh.annotations.TearDown;
|
||||
import org.openjdk.jmh.infra.Blackhole;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class DecoderBenchmark extends AbstractMicrobenchmark {
|
||||
@ -70,18 +70,18 @@ public class DecoderBenchmark extends AbstractMicrobenchmark {
|
||||
private ByteBuf input;
|
||||
|
||||
@Setup(Level.Trial)
|
||||
public void setup() throws IOException {
|
||||
public void setup() {
|
||||
input = Unpooled.wrappedBuffer(getSerializedHeaders(Util.headers(size, limitToAscii), sensitive));
|
||||
}
|
||||
|
||||
@TearDown(Level.Trial)
|
||||
public void teardown() throws IOException {
|
||||
public void teardown() {
|
||||
input.release();
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
@BenchmarkMode(Mode.Throughput)
|
||||
public void decode(final Blackhole bh) throws IOException {
|
||||
public void decode(final Blackhole bh) throws Http2Exception {
|
||||
Decoder decoder = new Decoder(maxHeaderSize, maxTableSize, 32);
|
||||
@SuppressWarnings("unchecked")
|
||||
Http2Headers headers =
|
||||
@ -93,7 +93,6 @@ public class DecoderBenchmark extends AbstractMicrobenchmark {
|
||||
}
|
||||
};
|
||||
decoder.decode(input.duplicate(), headers);
|
||||
decoder.endHeaderBlock();
|
||||
}
|
||||
|
||||
private byte[] getSerializedHeaders(List<Header> headers, boolean sensitive) {
|
||||
|
Loading…
Reference in New Issue
Block a user