diff --git a/NOTICE.txt b/NOTICE.txt index dd1d88c3e9..c051eacb54 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -211,3 +211,11 @@ non-blocking XML processor, which can be obtained at: * HOMEPAGE: * http://wiki.fasterxml.com/AaltoHome +This product contains a modified version of 'HPACK', a Java implementation of +the HTTP/2 HPACK algorithm written by Twitter. It can be obtained at: + + * LICENSE: + * license/LICENSE.hpack.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/twitter/hpack + diff --git a/codec-http2/pom.xml b/codec-http2/pom.xml index 7b7edbde90..d819aab873 100644 --- a/codec-http2/pom.xml +++ b/codec-http2/pom.xml @@ -39,15 +39,15 @@ netty-handler ${project.version} - - com.twitter - hpack - com.jcraft jzlib true + + com.google.code.gson + gson + diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java index 688498c9d0..e0f6cb1254 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java @@ -15,10 +15,10 @@ package io.netty.handler.codec.http2; -import com.twitter.hpack.Decoder; -import com.twitter.hpack.HeaderListener; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufInputStream; +import io.netty.handler.codec.http2.hpack.Decoder; +import io.netty.handler.codec.http2.hpack.HeaderListener; import io.netty.util.AsciiString; import java.io.IOException; diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java index 33f948479b..854498c3a2 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java @@ -15,9 +15,9 @@ package io.netty.handler.codec.http2; -import com.twitter.hpack.Encoder; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufOutputStream; +import io.netty.handler.codec.http2.hpack.Encoder; import io.netty.util.AsciiString; import java.io.ByteArrayOutputStream; diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/Decoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/Decoder.java new file mode 100644 index 0000000000..9b801f2745 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/Decoder.java @@ -0,0 +1,568 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Copyright 2014 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.netty.handler.codec.http2.hpack; + +import io.netty.handler.codec.http2.hpack.HpackUtil.IndexType; + +import java.io.IOException; +import java.io.InputStream; + +public final class Decoder { + + private static final IOException DECOMPRESSION_EXCEPTION = + new IOException("decompression failure"); + private static final IOException ILLEGAL_INDEX_VALUE = + new IOException("illegal index value"); + private static final IOException INVALID_MAX_DYNAMIC_TABLE_SIZE = + new IOException("invalid max dynamic table size"); + private static final IOException MAX_DYNAMIC_TABLE_SIZE_CHANGE_REQUIRED = + new IOException("max dynamic table size change required"); + + private static final byte[] EMPTY = {}; + + private final DynamicTable dynamicTable; + + private int maxHeaderSize; + 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 byte[] 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. + */ + public Decoder(int maxHeaderSize, int maxHeaderTableSize) { + dynamicTable = new DynamicTable(maxHeaderTableSize); + this.maxHeaderSize = maxHeaderSize; + maxDynamicTableSize = maxHeaderTableSize; + encoderMaxDynamicTableSize = maxHeaderTableSize; + maxDynamicTableSizeChangeRequired = false; + reset(); + } + + private void reset() { + headerSize = 0; + state = State.READ_HEADER_REPRESENTATION; + indexType = IndexType.NONE; + } + + /** + * Decode the header block into header fields. + */ + public void decode(InputStream in, HeaderListener headerListener) throws IOException { + while (in.available() > 0) { + switch (state) { + case READ_HEADER_REPRESENTATION: + byte b = (byte) in.read(); + if (maxDynamicTableSizeChangeRequired && (b & 0xE0) != 0x20) { + // Encoder MUST signal maximum dynamic table size change + throw MAX_DYNAMIC_TABLE_SIZE_CHANGE_REQUIRED; + } + if (b < 0) { + // Indexed Header Field + index = b & 0x7F; + if (index == 0) { + throw ILLEGAL_INDEX_VALUE; + } else if (index == 0x7F) { + state = State.READ_INDEXED_HEADER; + } else { + indexHeader(index, headerListener); + } + } 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 { + // Index was stored as the prefix + readName(index); + state = 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; + } else { + setDynamicTableSize(index); + state = 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 { + // Index was stored as the prefix + readName(index); + state = State.READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX; + } + } + break; + + case READ_MAX_DYNAMIC_TABLE_SIZE: + int maxSize = decodeULE128(in); + if (maxSize == -1) { + return; + } + + // Check for numerical overflow + if (maxSize > Integer.MAX_VALUE - index) { + throw DECOMPRESSION_EXCEPTION; + } + + setDynamicTableSize(index + maxSize); + state = State.READ_HEADER_REPRESENTATION; + break; + + case READ_INDEXED_HEADER: + int headerIndex = decodeULE128(in); + if (headerIndex == -1) { + return; + } + + // Check for numerical overflow + if (headerIndex > Integer.MAX_VALUE - index) { + throw DECOMPRESSION_EXCEPTION; + } + + indexHeader(index + headerIndex, headerListener); + state = 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; + } + + // Check for numerical overflow + if (nameIndex > Integer.MAX_VALUE - index) { + throw DECOMPRESSION_EXCEPTION; + } + + readName(index + nameIndex); + state = State.READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX; + break; + + case READ_LITERAL_HEADER_NAME_LENGTH_PREFIX: + b = (byte) in.read(); + huffmanEncoded = (b & 0x80) == 0x80; + index = b & 0x7F; + if (index == 0x7f) { + state = State.READ_LITERAL_HEADER_NAME_LENGTH; + } else { + nameLength = index; + + // Disallow empty names -- they cannot be represented in HTTP/1.x + if (nameLength == 0) { + throw DECOMPRESSION_EXCEPTION; + } + + // Check name length against max header size + if (exceedsMaxHeaderSize(nameLength)) { + + if (indexType == IndexType.NONE) { + // Name is unused so skip bytes + name = EMPTY; + 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; + skipLength = nameLength; + state = State.SKIP_LITERAL_HEADER_NAME; + break; + } + } + state = 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; + } + + // Check for numerical overflow + if (nameLength > Integer.MAX_VALUE - index) { + throw 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; + 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; + skipLength = nameLength; + state = State.SKIP_LITERAL_HEADER_NAME; + break; + } + } + state = State.READ_LITERAL_HEADER_NAME; + break; + + case READ_LITERAL_HEADER_NAME: + // Wait until entire name is readable + if (in.available() < nameLength) { + return; + } + + name = readStringLiteral(in, nameLength); + + state = State.READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX; + break; + + case SKIP_LITERAL_HEADER_NAME: + skipLength -= in.skip(skipLength); + + if (skipLength == 0) { + state = State.READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX; + } + break; + + case READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX: + b = (byte) in.read(); + huffmanEncoded = (b & 0x80) == 0x80; + index = b & 0x7F; + if (index == 0x7f) { + state = State.READ_LITERAL_HEADER_VALUE_LENGTH; + } else { + valueLength = index; + + // 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(headerListener, name, EMPTY, indexType); + state = State.READ_HEADER_REPRESENTATION; + } else { + state = 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; + } + + // Check for numerical overflow + if (valueLength > Integer.MAX_VALUE - index) { + throw 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; + } + + // 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; + break; + + case READ_LITERAL_HEADER_VALUE: + // Wait until entire value is readable + if (in.available() < valueLength) { + return; + } + + byte[] value = readStringLiteral(in, valueLength); + insertHeader(headerListener, name, value, indexType); + state = State.READ_HEADER_REPRESENTATION; + break; + + case SKIP_LITERAL_HEADER_VALUE: + valueLength -= in.skip(valueLength); + + if (valueLength == 0) { + state = State.READ_HEADER_REPRESENTATION; + } + break; + + default: + throw new IllegalStateException("should not reach here"); + } + } + } + + /** + * 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. + */ + public void setMaxHeaderTableSize(int maxHeaderTableSize) { + maxDynamicTableSize = maxHeaderTableSize; + if (maxDynamicTableSize < encoderMaxDynamicTableSize) { + // decoder requires less space than encoder + // encoder MUST signal this change + maxDynamicTableSizeChangeRequired = true; + dynamicTable.setCapacity(maxDynamicTableSize); + } + } + + /** + * Return the maximum table size. This is the maximum size allowed by both the encoder and the + * decoder. + */ + public int getMaxHeaderTableSize() { + return dynamicTable.capacity(); + } + + /** + * Return the number of header fields in the dynamic table. Exposed for testing. + */ + int length() { + return dynamicTable.length(); + } + + /** + * Return the size of the dynamic table. Exposed for testing. + */ + int size() { + return dynamicTable.size(); + } + + /** + * Return the header field at the given index. Exposed for testing. + */ + HeaderField getHeaderField(int index) { + return dynamicTable.getEntry(index + 1); + } + + private void setDynamicTableSize(int dynamicTableSize) throws IOException { + if (dynamicTableSize > maxDynamicTableSize) { + throw INVALID_MAX_DYNAMIC_TABLE_SIZE; + } + encoderMaxDynamicTableSize = dynamicTableSize; + maxDynamicTableSizeChangeRequired = false; + dynamicTable.setCapacity(dynamicTableSize); + } + + private void readName(int index) throws IOException { + if (index <= StaticTable.length) { + HeaderField headerField = StaticTable.getEntry(index); + name = headerField.name; + } else if (index - StaticTable.length <= dynamicTable.length()) { + HeaderField headerField = dynamicTable.getEntry(index - StaticTable.length); + name = headerField.name; + } else { + throw ILLEGAL_INDEX_VALUE; + } + } + + private void indexHeader(int index, HeaderListener headerListener) throws IOException { + if (index <= StaticTable.length) { + HeaderField headerField = StaticTable.getEntry(index); + addHeader(headerListener, headerField.name, headerField.value, false); + } else if (index - StaticTable.length <= dynamicTable.length()) { + HeaderField headerField = dynamicTable.getEntry(index - StaticTable.length); + addHeader(headerListener, headerField.name, headerField.value, false); + } else { + throw ILLEGAL_INDEX_VALUE; + } + } + + private void insertHeader(HeaderListener headerListener, byte[] name, byte[] value, + IndexType indexType) { + addHeader(headerListener, name, value, indexType == IndexType.NEVER); + + switch (indexType) { + case NONE: + case NEVER: + break; + + case INCREMENTAL: + dynamicTable.add(new HeaderField(name, value)); + break; + + default: + throw new IllegalStateException("should not reach here"); + } + } + + private void addHeader(HeaderListener headerListener, byte[] name, byte[] value, + boolean sensitive) { + if (name.length == 0) { + throw new AssertionError("name is empty"); + } + long newSize = headerSize + name.length + value.length; + if (newSize <= maxHeaderSize) { + headerListener.addHeader(name, value, sensitive); + headerSize = (int) newSize; + } else { + // truncation will be reported during endHeaderBlock + headerSize = maxHeaderSize + 1; + } + } + + private boolean exceedsMaxHeaderSize(long size) { + // Check new header size against max header size + if (size + headerSize <= maxHeaderSize) { + return false; + } + + // truncation will be reported during endHeaderBlock + headerSize = maxHeaderSize + 1; + return true; + } + + private byte[] readStringLiteral(InputStream in, int length) throws IOException { + byte[] buf = new byte[length]; + if (in.read(buf) != length) { + throw DECOMPRESSION_EXCEPTION; + } + + if (huffmanEncoded) { + return Huffman.DECODER.decode(buf); + } else { + return buf; + } + } + + // Unsigned Little Endian Base 128 Variable-Length Integer Encoding + private static int decodeULE128(InputStream in) throws IOException { + in.mark(5); + int result = 0; + int shift = 0; + while (shift < 32) { + if (in.available() == 0) { + // Buffer does not contain entire integer, + // reset reader index and return -1. + in.reset(); + return -1; + } + byte b = (byte) in.read(); + if (shift == 28 && (b & 0xF8) != 0) { + break; + } + result |= (b & 0x7F) << shift; + if ((b & 0x80) == 0) { + return result; + } + shift += 7; + } + // Value exceeds Integer.MAX_VALUE + in.reset(); + throw DECOMPRESSION_EXCEPTION; + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/DynamicTable.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/DynamicTable.java new file mode 100644 index 0000000000..2c4457992c --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/DynamicTable.java @@ -0,0 +1,199 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Copyright 2014 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.netty.handler.codec.http2.hpack; + +import static io.netty.handler.codec.http2.hpack.HeaderField.HEADER_ENTRY_OVERHEAD; + +final class DynamicTable { + + // a circular queue of header fields + HeaderField[] headerFields; + int head; + int tail; + private int size; + private int capacity = -1; // ensure setCapacity creates the array + + /** + * Creates a new dynamic table with the specified initial capacity. + */ + DynamicTable(int initialCapacity) { + setCapacity(initialCapacity); + } + + /** + * Return the number of header fields in the dynamic table. + */ + public int length() { + int length; + if (head < tail) { + length = headerFields.length - tail + head; + } else { + length = head - tail; + } + return length; + } + + /** + * Return the current size of the dynamic table. This is the sum of the size of the entries. + */ + public int size() { + return size; + } + + /** + * Return the maximum allowable size of the dynamic table. + */ + public int capacity() { + return capacity; + } + + /** + * Return the header field at the given index. The first and newest entry is always at index 1, + * and the oldest entry is at the index length(). + */ + public HeaderField getEntry(int index) { + if (index <= 0 || index > length()) { + throw new IndexOutOfBoundsException(); + } + int i = head - index; + if (i < 0) { + return headerFields[i + headerFields.length]; + } else { + return headerFields[i]; + } + } + + /** + * Add the header field to the dynamic table. Entries are evicted from the dynamic table until + * the size of the table and the new header field is less than or equal to the table's capacity. + * If the size of the new entry is larger than the table's capacity, the dynamic table will be + * cleared. + */ + public void add(HeaderField header) { + int headerSize = header.size(); + if (headerSize > capacity) { + clear(); + return; + } + while (size + headerSize > capacity) { + remove(); + } + headerFields[head++] = header; + size += header.size(); + if (head == headerFields.length) { + head = 0; + } + } + + /** + * Remove and return the oldest header field from the dynamic table. + */ + public HeaderField remove() { + HeaderField removed = headerFields[tail]; + if (removed == null) { + return null; + } + size -= removed.size(); + headerFields[tail++] = null; + if (tail == headerFields.length) { + tail = 0; + } + return removed; + } + + /** + * Remove all entries from the dynamic table. + */ + public void clear() { + while (tail != head) { + headerFields[tail++] = null; + if (tail == headerFields.length) { + tail = 0; + } + } + head = 0; + tail = 0; + size = 0; + } + + /** + * Set the maximum size of the dynamic table. Entries are evicted from the dynamic table until + * the size of the table is less than or equal to the maximum size. + */ + public void setCapacity(int capacity) { + if (capacity < 0) { + throw new IllegalArgumentException("Illegal Capacity: " + capacity); + } + + // initially capacity will be -1 so init won't return here + if (this.capacity == capacity) { + return; + } + this.capacity = capacity; + + if (capacity == 0) { + clear(); + } else { + // initially size will be 0 so remove won't be called + while (size > capacity) { + remove(); + } + } + + int maxEntries = capacity / HEADER_ENTRY_OVERHEAD; + if (capacity % HEADER_ENTRY_OVERHEAD != 0) { + maxEntries++; + } + + // check if capacity change requires us to reallocate the array + if (headerFields != null && headerFields.length == maxEntries) { + return; + } + + HeaderField[] tmp = new HeaderField[maxEntries]; + + // initially length will be 0 so there will be no copy + int len = length(); + int cursor = tail; + for (int i = 0; i < len; i++) { + HeaderField entry = headerFields[cursor++]; + tmp[i] = entry; + if (cursor == headerFields.length) { + cursor = 0; + } + } + + this.tail = 0; + this.head = tail + len; + this.headerFields = tmp; + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/Encoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/Encoder.java new file mode 100644 index 0000000000..3c2a0c7e23 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/Encoder.java @@ -0,0 +1,469 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Copyright 2014 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.netty.handler.codec.http2.hpack; + +import io.netty.handler.codec.http2.hpack.HpackUtil.IndexType; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; + +public final class Encoder { + + private static final int BUCKET_SIZE = 17; + private static final byte[] EMPTY = {}; + + // for testing + private final boolean useIndexing; + private final boolean forceHuffmanOn; + private final boolean forceHuffmanOff; + + // a linked hash map of header fields + private final HeaderEntry[] headerFields = new HeaderEntry[BUCKET_SIZE]; + private final HeaderEntry head = new HeaderEntry(-1, EMPTY, EMPTY, Integer.MAX_VALUE, null); + private int size; + private int capacity; + + /** + * Creates a new encoder. + */ + public Encoder(int maxHeaderTableSize) { + this(maxHeaderTableSize, true, false, false); + } + + /** + * Constructor for testing only. + */ + Encoder( + int maxHeaderTableSize, + boolean useIndexing, + boolean forceHuffmanOn, + boolean forceHuffmanOff + ) { + if (maxHeaderTableSize < 0) { + throw new IllegalArgumentException("Illegal Capacity: " + maxHeaderTableSize); + } + this.useIndexing = useIndexing; + this.forceHuffmanOn = forceHuffmanOn; + this.forceHuffmanOff = forceHuffmanOff; + this.capacity = maxHeaderTableSize; + head.before = head.after = head; + } + + /** + * Encode the header field into the header block. + */ + public void encodeHeader(OutputStream out, byte[] name, byte[] value, boolean sensitive) + throws IOException { + + // If the header value is sensitive then it must never be indexed + if (sensitive) { + int nameIndex = getNameIndex(name); + encodeLiteral(out, name, value, IndexType.NEVER, nameIndex); + return; + } + + // If the peer will only use the static table + if (capacity == 0) { + int staticTableIndex = StaticTable.getIndex(name, value); + if (staticTableIndex == -1) { + int nameIndex = StaticTable.getIndex(name); + encodeLiteral(out, name, value, IndexType.NONE, nameIndex); + } else { + encodeInteger(out, 0x80, 7, staticTableIndex); + } + return; + } + + int headerSize = HeaderField.sizeOf(name, value); + + // If the headerSize is greater than the max table size then it must be encoded literally + if (headerSize > capacity) { + int nameIndex = getNameIndex(name); + encodeLiteral(out, name, value, IndexType.NONE, nameIndex); + return; + } + + HeaderEntry headerField = getEntry(name, value); + if (headerField != null) { + int index = getIndex(headerField.index) + StaticTable.length; + // Section 6.1. Indexed Header Field Representation + encodeInteger(out, 0x80, 7, index); + } else { + int staticTableIndex = StaticTable.getIndex(name, value); + if (staticTableIndex != -1) { + // Section 6.1. Indexed Header Field Representation + encodeInteger(out, 0x80, 7, staticTableIndex); + } else { + int nameIndex = getNameIndex(name); + if (useIndexing) { + ensureCapacity(headerSize); + } + IndexType indexType = useIndexing ? IndexType.INCREMENTAL : IndexType.NONE; + encodeLiteral(out, name, value, indexType, nameIndex); + if (useIndexing) { + add(name, value); + } + } + } + } + + /** + * Set the maximum table size. + */ + public void setMaxHeaderTableSize(OutputStream out, int maxHeaderTableSize) throws IOException { + if (maxHeaderTableSize < 0) { + throw new IllegalArgumentException("Illegal Capacity: " + maxHeaderTableSize); + } + if (capacity == maxHeaderTableSize) { + return; + } + capacity = maxHeaderTableSize; + ensureCapacity(0); + encodeInteger(out, 0x20, 5, maxHeaderTableSize); + } + + /** + * Return the maximum table size. + */ + public int getMaxHeaderTableSize() { + return capacity; + } + + /** + * Encode integer according to Section 5.1. + */ + private static void encodeInteger(OutputStream out, int mask, int n, int i) throws IOException { + if (n < 0 || n > 8) { + throw new IllegalArgumentException("N: " + n); + } + int nbits = 0xFF >>> (8 - n); + if (i < nbits) { + out.write(mask | i); + } else { + out.write(mask | nbits); + int length = i - nbits; + while (true) { + if ((length & ~0x7F) == 0) { + out.write(length); + return; + } else { + out.write((length & 0x7F) | 0x80); + length >>>= 7; + } + } + } + } + + /** + * Encode string literal according to Section 5.2. + */ + private void encodeStringLiteral(OutputStream out, byte[] string) throws IOException { + int huffmanLength = Huffman.ENCODER.getEncodedLength(string); + if ((huffmanLength < string.length && !forceHuffmanOff) || forceHuffmanOn) { + encodeInteger(out, 0x80, 7, huffmanLength); + Huffman.ENCODER.encode(out, string); + } else { + encodeInteger(out, 0x00, 7, string.length); + out.write(string, 0, string.length); + } + } + + /** + * Encode literal header field according to Section 6.2. + */ + private void encodeLiteral(OutputStream out, byte[] name, byte[] value, IndexType indexType, + int nameIndex) + throws IOException { + int mask; + int prefixBits; + switch (indexType) { + case INCREMENTAL: + mask = 0x40; + prefixBits = 6; + break; + case NONE: + mask = 0x00; + prefixBits = 4; + break; + case NEVER: + mask = 0x10; + prefixBits = 4; + break; + default: + throw new IllegalStateException("should not reach here"); + } + encodeInteger(out, mask, prefixBits, nameIndex == -1 ? 0 : nameIndex); + if (nameIndex == -1) { + encodeStringLiteral(out, name); + } + encodeStringLiteral(out, value); + } + + private int getNameIndex(byte[] name) { + int index = StaticTable.getIndex(name); + if (index == -1) { + index = getIndex(name); + if (index >= 0) { + index += StaticTable.length; + } + } + return index; + } + + /** + * Ensure that the dynamic table has enough room to hold 'headerSize' more bytes. Removes the + * oldest entry from the dynamic table until sufficient space is available. + */ + private void ensureCapacity(int headerSize) throws IOException { + while (size + headerSize > capacity) { + int index = length(); + if (index == 0) { + break; + } + remove(); + } + } + + /** + * Return the number of header fields in the dynamic table. Exposed for testing. + */ + int length() { + return size == 0 ? 0 : head.after.index - head.before.index + 1; + } + + /** + * Return the size of the dynamic table. Exposed for testing. + */ + int size() { + return size; + } + + /** + * Return the header field at the given index. Exposed for testing. + */ + HeaderField getHeaderField(int index) { + HeaderEntry entry = head; + while (index-- >= 0) { + entry = entry.before; + } + return entry; + } + + /** + * Returns the header entry with the lowest index value for the header field. Returns null if + * header field is not in the dynamic table. + */ + private HeaderEntry getEntry(byte[] name, byte[] value) { + if (length() == 0 || name == null || value == null) { + return null; + } + int h = hash(name); + int i = index(h); + for (HeaderEntry e = headerFields[i]; e != null; e = e.next) { + if (e.hash == h && + HpackUtil.equals(name, e.name) && + HpackUtil.equals(value, e.value)) { + return e; + } + } + return null; + } + + /** + * Returns the lowest index value for the header field name in the dynamic table. Returns -1 if + * the header field name is not in the dynamic table. + */ + private int getIndex(byte[] name) { + if (length() == 0 || name == null) { + return -1; + } + int h = hash(name); + int i = index(h); + int index = -1; + for (HeaderEntry e = headerFields[i]; e != null; e = e.next) { + if (e.hash == h && HpackUtil.equals(name, e.name)) { + index = e.index; + break; + } + } + return getIndex(index); + } + + /** + * Compute the index into the dynamic table given the index in the header entry. + */ + private int getIndex(int index) { + if (index == -1) { + return index; + } + return index - head.before.index + 1; + } + + /** + * Add the header field to the dynamic table. Entries are evicted from the dynamic table until + * the size of the table and the new header field is less than the table's capacity. If the size + * of the new entry is larger than the table's capacity, the dynamic table will be cleared. + */ + private void add(byte[] name, byte[] value) { + int headerSize = HeaderField.sizeOf(name, value); + + // Clear the table if the header field size is larger than the capacity. + if (headerSize > capacity) { + clear(); + return; + } + + // Evict oldest entries until we have enough capacity. + while (size + headerSize > capacity) { + remove(); + } + + // Copy name and value that modifications of original do not affect the dynamic table. + name = Arrays.copyOf(name, name.length); + value = Arrays.copyOf(value, value.length); + + int h = hash(name); + int i = index(h); + HeaderEntry old = headerFields[i]; + HeaderEntry e = new HeaderEntry(h, name, value, head.before.index - 1, old); + headerFields[i] = e; + e.addBefore(head); + size += headerSize; + } + + /** + * Remove and return the oldest header field from the dynamic table. + */ + private HeaderField remove() { + if (size == 0) { + return null; + } + HeaderEntry eldest = head.after; + int h = eldest.hash; + int i = index(h); + HeaderEntry prev = headerFields[i]; + HeaderEntry e = prev; + while (e != null) { + HeaderEntry next = e.next; + if (e == eldest) { + if (prev == eldest) { + headerFields[i] = next; + } else { + prev.next = next; + } + eldest.remove(); + size -= eldest.size(); + return eldest; + } + prev = e; + e = next; + } + return null; + } + + /** + * Remove all entries from the dynamic table. + */ + private void clear() { + Arrays.fill(headerFields, null); + head.before = head.after = head; + this.size = 0; + } + + /** + * Returns the hash code for the given header field name. + */ + private static int hash(byte[] name) { + int h = 0; + for (int i = 0; i < name.length; i++) { + h = 31 * h + name[i]; + } + if (h > 0) { + return h; + } else if (h == Integer.MIN_VALUE) { + return Integer.MAX_VALUE; + } else { + return -h; + } + } + + /** + * Returns the index into the hash table for the hash code h. + */ + private static int index(int h) { + return h % BUCKET_SIZE; + } + + /** + * A linked hash map HeaderField entry. + */ + private static class HeaderEntry extends HeaderField { + // These fields comprise the doubly linked list used for iteration. + HeaderEntry before, after; + + // These fields comprise the chained list for header fields with the same hash. + HeaderEntry next; + int hash; + + // This is used to compute the index in the dynamic table. + int index; + + /** + * Creates new entry. + */ + HeaderEntry(int hash, byte[] name, byte[] value, int index, HeaderEntry next) { + super(name, value); + this.index = index; + this.hash = hash; + this.next = next; + } + + /** + * Removes this entry from the linked list. + */ + private void remove() { + before.after = after; + after.before = before; + } + + /** + * Inserts this entry before the specified existing entry in the list. + */ + private void addBefore(HeaderEntry existingEntry) { + after = existingEntry; + before = existingEntry.before; + before.after = this; + after.before = this; + } + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HeaderField.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HeaderField.java new file mode 100644 index 0000000000..4a028658e5 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HeaderField.java @@ -0,0 +1,117 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Copyright 2014 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.netty.handler.codec.http2.hpack; + +import static io.netty.handler.codec.http2.hpack.HpackUtil.ISO_8859_1; +import static io.netty.handler.codec.http2.hpack.HpackUtil.requireNonNull; + +class HeaderField implements Comparable { + + // Section 4.1. Calculating Table Size + // The additional 32 octets account for an estimated + // overhead associated with the structure. + static final int HEADER_ENTRY_OVERHEAD = 32; + + static int sizeOf(byte[] name, byte[] value) { + return name.length + value.length + HEADER_ENTRY_OVERHEAD; + } + + final byte[] name; + final byte[] value; + + // This constructor can only be used if name and value are ISO-8859-1 encoded. + HeaderField(String name, String value) { + this(name.getBytes(ISO_8859_1), value.getBytes(ISO_8859_1)); + } + + HeaderField(byte[] name, byte[] value) { + this.name = requireNonNull(name); + this.value = requireNonNull(value); + } + + int size() { + return name.length + value.length + HEADER_ENTRY_OVERHEAD; + } + + @Override + public int hashCode() { + // TODO(nmittler): Netty's build rules require this. Probably need a better implementation. + return super.hashCode(); + } + + @Override + public int compareTo(HeaderField anotherHeaderField) { + int ret = compareTo(name, anotherHeaderField.name); + if (ret == 0) { + ret = compareTo(value, anotherHeaderField.value); + } + return ret; + } + + private int compareTo(byte[] s1, byte[] s2) { + int len1 = s1.length; + int len2 = s2.length; + int lim = Math.min(len1, len2); + + int k = 0; + while (k < lim) { + byte b1 = s1[k]; + byte b2 = s2[k]; + if (b1 != b2) { + return b1 - b2; + } + k++; + } + return len1 - len2; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof HeaderField)) { + return false; + } + HeaderField other = (HeaderField) obj; + boolean nameEquals = HpackUtil.equals(name, other.name); + boolean valueEquals = HpackUtil.equals(value, other.value); + return nameEquals && valueEquals; + } + + @Override + public String toString() { + String nameString = new String(name); + String valueString = new String(value); + return nameString + ": " + valueString; + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HeaderListener.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HeaderListener.java new file mode 100644 index 0000000000..6fd488d41e --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HeaderListener.java @@ -0,0 +1,41 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Copyright 2014 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.netty.handler.codec.http2.hpack; + +public interface HeaderListener { + + /** + * emitHeader is called by the decoder during header field emission. + * The name and value byte arrays must not be modified. + */ + void addHeader(byte[] name, byte[] value, boolean sensitive); +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HpackUtil.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HpackUtil.java new file mode 100644 index 0000000000..53660f6657 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HpackUtil.java @@ -0,0 +1,358 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Copyright 2014 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.netty.handler.codec.http2.hpack; + +import java.nio.charset.Charset; + +final class HpackUtil { + + static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); + + /** + * A string compare that doesn't leak timing information. + */ + static boolean equals(byte[] s1, byte[] s2) { + if (s1.length != s2.length) { + return false; + } + char c = 0; + for (int i = 0; i < s1.length; i++) { + c |= s1[i] ^ s2[i]; + } + return c == 0; + } + + /** + * Checks that the specified object reference is not {@code null}. + */ + static T requireNonNull(T obj) { + if (obj == null) { + throw new NullPointerException(); + } + return obj; + } + + // Section 6.2. Literal Header Field Representation + enum IndexType { + INCREMENTAL, // Section 6.2.1. Literal Header Field with Incremental Indexing + NONE, // Section 6.2.2. Literal Header Field without Indexing + NEVER // Section 6.2.3. Literal Header Field never Indexed + } + + // Appendix B: Huffman Codes + // http://tools.ietf.org/html/rfc7541#appendix-B + static final int[] HUFFMAN_CODES = { + 0x1ff8, + 0x7fffd8, + 0xfffffe2, + 0xfffffe3, + 0xfffffe4, + 0xfffffe5, + 0xfffffe6, + 0xfffffe7, + 0xfffffe8, + 0xffffea, + 0x3ffffffc, + 0xfffffe9, + 0xfffffea, + 0x3ffffffd, + 0xfffffeb, + 0xfffffec, + 0xfffffed, + 0xfffffee, + 0xfffffef, + 0xffffff0, + 0xffffff1, + 0xffffff2, + 0x3ffffffe, + 0xffffff3, + 0xffffff4, + 0xffffff5, + 0xffffff6, + 0xffffff7, + 0xffffff8, + 0xffffff9, + 0xffffffa, + 0xffffffb, + 0x14, + 0x3f8, + 0x3f9, + 0xffa, + 0x1ff9, + 0x15, + 0xf8, + 0x7fa, + 0x3fa, + 0x3fb, + 0xf9, + 0x7fb, + 0xfa, + 0x16, + 0x17, + 0x18, + 0x0, + 0x1, + 0x2, + 0x19, + 0x1a, + 0x1b, + 0x1c, + 0x1d, + 0x1e, + 0x1f, + 0x5c, + 0xfb, + 0x7ffc, + 0x20, + 0xffb, + 0x3fc, + 0x1ffa, + 0x21, + 0x5d, + 0x5e, + 0x5f, + 0x60, + 0x61, + 0x62, + 0x63, + 0x64, + 0x65, + 0x66, + 0x67, + 0x68, + 0x69, + 0x6a, + 0x6b, + 0x6c, + 0x6d, + 0x6e, + 0x6f, + 0x70, + 0x71, + 0x72, + 0xfc, + 0x73, + 0xfd, + 0x1ffb, + 0x7fff0, + 0x1ffc, + 0x3ffc, + 0x22, + 0x7ffd, + 0x3, + 0x23, + 0x4, + 0x24, + 0x5, + 0x25, + 0x26, + 0x27, + 0x6, + 0x74, + 0x75, + 0x28, + 0x29, + 0x2a, + 0x7, + 0x2b, + 0x76, + 0x2c, + 0x8, + 0x9, + 0x2d, + 0x77, + 0x78, + 0x79, + 0x7a, + 0x7b, + 0x7ffe, + 0x7fc, + 0x3ffd, + 0x1ffd, + 0xffffffc, + 0xfffe6, + 0x3fffd2, + 0xfffe7, + 0xfffe8, + 0x3fffd3, + 0x3fffd4, + 0x3fffd5, + 0x7fffd9, + 0x3fffd6, + 0x7fffda, + 0x7fffdb, + 0x7fffdc, + 0x7fffdd, + 0x7fffde, + 0xffffeb, + 0x7fffdf, + 0xffffec, + 0xffffed, + 0x3fffd7, + 0x7fffe0, + 0xffffee, + 0x7fffe1, + 0x7fffe2, + 0x7fffe3, + 0x7fffe4, + 0x1fffdc, + 0x3fffd8, + 0x7fffe5, + 0x3fffd9, + 0x7fffe6, + 0x7fffe7, + 0xffffef, + 0x3fffda, + 0x1fffdd, + 0xfffe9, + 0x3fffdb, + 0x3fffdc, + 0x7fffe8, + 0x7fffe9, + 0x1fffde, + 0x7fffea, + 0x3fffdd, + 0x3fffde, + 0xfffff0, + 0x1fffdf, + 0x3fffdf, + 0x7fffeb, + 0x7fffec, + 0x1fffe0, + 0x1fffe1, + 0x3fffe0, + 0x1fffe2, + 0x7fffed, + 0x3fffe1, + 0x7fffee, + 0x7fffef, + 0xfffea, + 0x3fffe2, + 0x3fffe3, + 0x3fffe4, + 0x7ffff0, + 0x3fffe5, + 0x3fffe6, + 0x7ffff1, + 0x3ffffe0, + 0x3ffffe1, + 0xfffeb, + 0x7fff1, + 0x3fffe7, + 0x7ffff2, + 0x3fffe8, + 0x1ffffec, + 0x3ffffe2, + 0x3ffffe3, + 0x3ffffe4, + 0x7ffffde, + 0x7ffffdf, + 0x3ffffe5, + 0xfffff1, + 0x1ffffed, + 0x7fff2, + 0x1fffe3, + 0x3ffffe6, + 0x7ffffe0, + 0x7ffffe1, + 0x3ffffe7, + 0x7ffffe2, + 0xfffff2, + 0x1fffe4, + 0x1fffe5, + 0x3ffffe8, + 0x3ffffe9, + 0xffffffd, + 0x7ffffe3, + 0x7ffffe4, + 0x7ffffe5, + 0xfffec, + 0xfffff3, + 0xfffed, + 0x1fffe6, + 0x3fffe9, + 0x1fffe7, + 0x1fffe8, + 0x7ffff3, + 0x3fffea, + 0x3fffeb, + 0x1ffffee, + 0x1ffffef, + 0xfffff4, + 0xfffff5, + 0x3ffffea, + 0x7ffff4, + 0x3ffffeb, + 0x7ffffe6, + 0x3ffffec, + 0x3ffffed, + 0x7ffffe7, + 0x7ffffe8, + 0x7ffffe9, + 0x7ffffea, + 0x7ffffeb, + 0xffffffe, + 0x7ffffec, + 0x7ffffed, + 0x7ffffee, + 0x7ffffef, + 0x7fffff0, + 0x3ffffee, + 0x3fffffff // EOS + }; + + static final byte[] HUFFMAN_CODE_LENGTHS = { + 13, 23, 28, 28, 28, 28, 28, 28, 28, 24, 30, 28, 28, 30, 28, 28, + 28, 28, 28, 28, 28, 28, 30, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 6, 10, 10, 12, 13, 6, 8, 11, 10, 10, 8, 11, 8, 6, 6, 6, + 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 7, 8, 15, 6, 12, 10, + 13, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 8, 13, 19, 13, 14, 6, + 15, 5, 6, 5, 6, 5, 6, 6, 6, 5, 7, 7, 6, 6, 6, 5, + 6, 7, 6, 5, 5, 6, 7, 7, 7, 7, 7, 15, 11, 14, 13, 28, + 20, 22, 20, 20, 22, 22, 22, 23, 22, 23, 23, 23, 23, 23, 24, 23, + 24, 24, 22, 23, 24, 23, 23, 23, 23, 21, 22, 23, 22, 23, 23, 24, + 22, 21, 20, 22, 22, 23, 23, 21, 23, 22, 22, 24, 21, 22, 23, 23, + 21, 21, 22, 21, 23, 22, 23, 23, 20, 22, 22, 22, 23, 22, 22, 23, + 26, 26, 20, 19, 22, 23, 22, 25, 26, 26, 26, 27, 27, 26, 24, 25, + 19, 21, 26, 27, 27, 26, 27, 24, 21, 21, 26, 26, 28, 27, 27, 27, + 20, 24, 20, 21, 22, 21, 21, 23, 22, 22, 25, 25, 24, 24, 26, 23, + 26, 27, 26, 26, 27, 27, 27, 27, 27, 28, 27, 27, 27, 27, 27, 26, + 30 // EOS + }; + + static final int HUFFMAN_EOS = 256; + + private HpackUtil() { + // utility class + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/Huffman.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/Huffman.java new file mode 100644 index 0000000000..771fab8e3e --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/Huffman.java @@ -0,0 +1,54 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Copyright 2014 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.netty.handler.codec.http2.hpack; + +import static io.netty.handler.codec.http2.hpack.HpackUtil.HUFFMAN_CODES; +import static io.netty.handler.codec.http2.hpack.HpackUtil.HUFFMAN_CODE_LENGTHS; + +public final class Huffman { + + /** + * Huffman Decoder + */ + public static final HuffmanDecoder DECODER = + new HuffmanDecoder(HUFFMAN_CODES, HUFFMAN_CODE_LENGTHS); + + /** + * Huffman Encoder + */ + public static final HuffmanEncoder ENCODER = + new HuffmanEncoder(HUFFMAN_CODES, HUFFMAN_CODE_LENGTHS); + + private Huffman() { + // utility class + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HuffmanDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HuffmanDecoder.java new file mode 100644 index 0000000000..c0b33e05d7 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HuffmanDecoder.java @@ -0,0 +1,176 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Copyright 2014 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.netty.handler.codec.http2.hpack; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +final class HuffmanDecoder { + + private static final IOException EOS_DECODED = new IOException("EOS Decoded"); + private static final IOException INVALID_PADDING = new IOException("Invalid Padding"); + + private final Node root; + + /** + * Creates a new Huffman decoder with the specified Huffman coding. + * + * @param codes the Huffman codes indexed by symbol + * @param lengths the length of each Huffman code + */ + HuffmanDecoder(int[] codes, byte[] lengths) { + if (codes.length != 257 || codes.length != lengths.length) { + throw new IllegalArgumentException("invalid Huffman coding"); + } + root = buildTree(codes, lengths); + } + + /** + * Decompresses the given Huffman coded string literal. + * + * @param buf the string literal to be decoded + * @return the output stream for the compressed data + * @throws IOException if an I/O error occurs. In particular, an IOException may be + * thrown if the output stream has been closed. + */ + public byte[] decode(byte[] buf) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + Node node = root; + int current = 0; + int bits = 0; + for (int i = 0; i < buf.length; i++) { + int b = buf[i] & 0xFF; + current = (current << 8) | b; + bits += 8; + while (bits >= 8) { + int c = (current >>> (bits - 8)) & 0xFF; + node = node.children[c]; + bits -= node.bits; + if (node.isTerminal()) { + if (node.symbol == HpackUtil.HUFFMAN_EOS) { + throw EOS_DECODED; + } + baos.write(node.symbol); + node = root; + } + } + } + + while (bits > 0) { + int c = (current << (8 - bits)) & 0xFF; + node = node.children[c]; + if (node.isTerminal() && node.bits <= bits) { + bits -= node.bits; + baos.write(node.symbol); + node = root; + } else { + break; + } + } + + // Section 5.2. String Literal Representation + // Padding not corresponding to the most significant bits of the code + // for the EOS symbol (0xFF) MUST be treated as a decoding error. + int mask = (1 << bits) - 1; + if ((current & mask) != mask) { + throw INVALID_PADDING; + } + + return baos.toByteArray(); + } + + private static final class Node { + + private final int symbol; // terminal nodes have a symbol + private final int bits; // number of bits matched by the node + private final Node[] children; // internal nodes have children + + /** + * Construct an internal node + */ + private Node() { + symbol = 0; + bits = 8; + children = new Node[256]; + } + + /** + * Construct a terminal node + * + * @param symbol the symbol the node represents + * @param bits the number of bits matched by this node + */ + private Node(int symbol, int bits) { + assert bits > 0 && bits <= 8; + this.symbol = symbol; + this.bits = bits; + children = null; + } + + private boolean isTerminal() { + return children == null; + } + } + + private static Node buildTree(int[] codes, byte[] lengths) { + Node root = new Node(); + for (int i = 0; i < codes.length; i++) { + insert(root, i, codes[i], lengths[i]); + } + return root; + } + + private static void insert(Node root, int symbol, int code, byte length) { + // traverse tree using the most significant bytes of code + Node current = root; + while (length > 8) { + if (current.isTerminal()) { + throw new IllegalStateException("invalid Huffman code: prefix not unique"); + } + length -= 8; + int i = (code >>> length) & 0xFF; + if (current.children[i] == null) { + current.children[i] = new Node(); + } + current = current.children[i]; + } + + Node terminal = new Node(symbol, length); + int shift = 8 - length; + int start = (code << shift) & 0xFF; + int end = 1 << shift; + for (int i = start; i < start + end; i++) { + current.children[i] = terminal; + } + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HuffmanEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HuffmanEncoder.java new file mode 100644 index 0000000000..1e7e2654ba --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HuffmanEncoder.java @@ -0,0 +1,128 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Copyright 2014 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.netty.handler.codec.http2.hpack; + +import java.io.IOException; +import java.io.OutputStream; + +final class HuffmanEncoder { + + private final int[] codes; + private final byte[] lengths; + + /** + * Creates a new Huffman encoder with the specified Huffman coding. + * + * @param codes the Huffman codes indexed by symbol + * @param lengths the length of each Huffman code + */ + HuffmanEncoder(int[] codes, byte[] lengths) { + this.codes = codes; + this.lengths = lengths; + } + + /** + * Compresses the input string literal using the Huffman coding. + * + * @param out the output stream for the compressed data + * @param data the string literal to be Huffman encoded + * @throws IOException if an I/O error occurs. + * @see HuffmanEncoder#encode(OutputStream, byte[], int, int) + */ + public void encode(OutputStream out, byte[] data) throws IOException { + encode(out, data, 0, data.length); + } + + /** + * Compresses the input string literal using the Huffman coding. + * + * @param out the output stream for the compressed data + * @param data the string literal to be Huffman encoded + * @param off the start offset in the data + * @param len the number of bytes to encode + * @throws IOException if an I/O error occurs. In particular, an IOException may be + * thrown if the output stream has been closed. + */ + public void encode(OutputStream out, byte[] data, int off, int len) throws IOException { + if (out == null) { + throw new NullPointerException("out"); + } else if (data == null) { + throw new NullPointerException("data"); + } else if (off < 0 || len < 0 || (off + len) < 0 || off > data.length || + (off + len) > data.length) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + + long current = 0; + int n = 0; + + for (int i = 0; i < len; i++) { + int b = data[off + i] & 0xFF; + int code = codes[b]; + int nbits = lengths[b]; + + current <<= nbits; + current |= code; + n += nbits; + + while (n >= 8) { + n -= 8; + out.write((int) (current >> n)); + } + } + + if (n > 0) { + current <<= 8 - n; + current |= 0xFF >>> n; // this should be EOS symbol + out.write((int) current); + } + } + + /** + * Returns the number of bytes required to Huffman encode the input string literal. + * + * @param data the string literal to be Huffman encoded + * @return the number of bytes required to Huffman encode data + */ + public int getEncodedLength(byte[] data) { + if (data == null) { + throw new NullPointerException("data"); + } + long len = 0; + for (byte b : data) { + len += lengths[b & 0xFF]; + } + return (int) ((len + 7) >> 3); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/StaticTable.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/StaticTable.java new file mode 100644 index 0000000000..ff8543b324 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/StaticTable.java @@ -0,0 +1,178 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Copyright 2014 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.netty.handler.codec.http2.hpack; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +final class StaticTable { + + private static final String EMPTY = ""; + + // Appendix A: Static Table + // http://tools.ietf.org/html/rfc7541#appendix-A + private static final List STATIC_TABLE = Arrays.asList( + /* 1 */ new HeaderField(":authority", EMPTY), + /* 2 */ new HeaderField(":method", "GET"), + /* 3 */ new HeaderField(":method", "POST"), + /* 4 */ new HeaderField(":path", "/"), + /* 5 */ new HeaderField(":path", "/index.html"), + /* 6 */ new HeaderField(":scheme", "http"), + /* 7 */ new HeaderField(":scheme", "https"), + /* 8 */ new HeaderField(":status", "200"), + /* 9 */ new HeaderField(":status", "204"), + /* 10 */ new HeaderField(":status", "206"), + /* 11 */ new HeaderField(":status", "304"), + /* 12 */ new HeaderField(":status", "400"), + /* 13 */ new HeaderField(":status", "404"), + /* 14 */ new HeaderField(":status", "500"), + /* 15 */ new HeaderField("accept-charset", EMPTY), + /* 16 */ new HeaderField("accept-encoding", "gzip, deflate"), + /* 17 */ new HeaderField("accept-language", EMPTY), + /* 18 */ new HeaderField("accept-ranges", EMPTY), + /* 19 */ new HeaderField("accept", EMPTY), + /* 20 */ new HeaderField("access-control-allow-origin", EMPTY), + /* 21 */ new HeaderField("age", EMPTY), + /* 22 */ new HeaderField("allow", EMPTY), + /* 23 */ new HeaderField("authorization", EMPTY), + /* 24 */ new HeaderField("cache-control", EMPTY), + /* 25 */ new HeaderField("content-disposition", EMPTY), + /* 26 */ new HeaderField("content-encoding", EMPTY), + /* 27 */ new HeaderField("content-language", EMPTY), + /* 28 */ new HeaderField("content-length", EMPTY), + /* 29 */ new HeaderField("content-location", EMPTY), + /* 30 */ new HeaderField("content-range", EMPTY), + /* 31 */ new HeaderField("content-type", EMPTY), + /* 32 */ new HeaderField("cookie", EMPTY), + /* 33 */ new HeaderField("date", EMPTY), + /* 34 */ new HeaderField("etag", EMPTY), + /* 35 */ new HeaderField("expect", EMPTY), + /* 36 */ new HeaderField("expires", EMPTY), + /* 37 */ new HeaderField("from", EMPTY), + /* 38 */ new HeaderField("host", EMPTY), + /* 39 */ new HeaderField("if-match", EMPTY), + /* 40 */ new HeaderField("if-modified-since", EMPTY), + /* 41 */ new HeaderField("if-none-match", EMPTY), + /* 42 */ new HeaderField("if-range", EMPTY), + /* 43 */ new HeaderField("if-unmodified-since", EMPTY), + /* 44 */ new HeaderField("last-modified", EMPTY), + /* 45 */ new HeaderField("link", EMPTY), + /* 46 */ new HeaderField("location", EMPTY), + /* 47 */ new HeaderField("max-forwards", EMPTY), + /* 48 */ new HeaderField("proxy-authenticate", EMPTY), + /* 49 */ new HeaderField("proxy-authorization", EMPTY), + /* 50 */ new HeaderField("range", EMPTY), + /* 51 */ new HeaderField("referer", EMPTY), + /* 52 */ new HeaderField("refresh", EMPTY), + /* 53 */ new HeaderField("retry-after", EMPTY), + /* 54 */ new HeaderField("server", EMPTY), + /* 55 */ new HeaderField("set-cookie", EMPTY), + /* 56 */ new HeaderField("strict-transport-security", EMPTY), + /* 57 */ new HeaderField("transfer-encoding", EMPTY), + /* 58 */ new HeaderField("user-agent", EMPTY), + /* 59 */ new HeaderField("vary", EMPTY), + /* 60 */ new HeaderField("via", EMPTY), + /* 61 */ new HeaderField("www-authenticate", EMPTY) + ); + + private static final Map STATIC_INDEX_BY_NAME = createMap(); + + /** + * The number of header fields in the static table. + */ + static final int length = STATIC_TABLE.size(); + + /** + * Return the header field at the given index value. + */ + static HeaderField getEntry(int index) { + return STATIC_TABLE.get(index - 1); + } + + /** + * Returns the lowest index value for the given header field name in the static table. Returns + * -1 if the header field name is not in the static table. + */ + static int getIndex(byte[] name) { + String nameString = new String(name, 0, name.length, HpackUtil.ISO_8859_1); + Integer index = STATIC_INDEX_BY_NAME.get(nameString); + if (index == null) { + return -1; + } + return index; + } + + /** + * Returns the index value for the given header field in the static table. Returns -1 if the + * header field is not in the static table. + */ + static int getIndex(byte[] name, byte[] value) { + int index = getIndex(name); + if (index == -1) { + return -1; + } + + // Note this assumes all entries for a given header field are sequential. + while (index <= length) { + HeaderField entry = getEntry(index); + if (!HpackUtil.equals(name, entry.name)) { + break; + } + if (HpackUtil.equals(value, entry.value)) { + return index; + } + index++; + } + + return -1; + } + + // create a map of header name to index value to allow quick lookup + private static Map createMap() { + int length = STATIC_TABLE.size(); + HashMap ret = new HashMap(length); + // Iterate through the static table in reverse order to + // save the smallest index for a given name in the map. + for (int index = length; index > 0; index--) { + HeaderField entry = getEntry(index); + String name = new String(entry.name, 0, entry.name.length, HpackUtil.ISO_8859_1); + ret.put(name, index); + } + return ret; + } + + // singleton + private StaticTable() { + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/package-info.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/package-info.java new file mode 100644 index 0000000000..e56f490857 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/package-info.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Copyright 2014 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * HPACK: Header Compression for HTTP/2 + */ +package io.netty.handler.codec.http2.hpack; diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoderTest.java index fd12cfe163..dbcf25d469 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoderTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoderTest.java @@ -15,9 +15,9 @@ package io.netty.handler.codec.http2; -import com.twitter.hpack.Encoder; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import io.netty.handler.codec.http2.hpack.Encoder; import io.netty.util.AsciiString; import org.junit.Before; import org.junit.Test; diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/DecoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/DecoderTest.java new file mode 100644 index 0000000000..4a3d60b535 --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/DecoderTest.java @@ -0,0 +1,319 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Copyright 2014 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.netty.handler.codec.http2.hpack; + +import static io.netty.handler.codec.http2.hpack.HpackUtil.ISO_8859_1; +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; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +public class DecoderTest { + + private static final int MAX_HEADER_SIZE = 8192; + private static final int MAX_HEADER_TABLE_SIZE = 4096; + + private Decoder decoder; + private HeaderListener mockListener; + + private static String hex(String s) { + return Hex.encodeHexString(s.getBytes()); + } + + private static byte[] getBytes(String s) { + return s.getBytes(ISO_8859_1); + } + + private void decode(String encoded) throws IOException { + byte[] b = Hex.decodeHex(encoded.toCharArray()); + decoder.decode(new ByteArrayInputStream(b), mockListener); + } + + @Before + public void setUp() { + decoder = new Decoder(MAX_HEADER_SIZE, MAX_HEADER_TABLE_SIZE); + mockListener = mock(HeaderListener.class); + } + + @Test + public void testIncompleteIndex() throws IOException { + // Verify incomplete indices are unread + byte[] compressed = Hex.decodeHex("FFF0".toCharArray()); + ByteArrayInputStream in = new ByteArrayInputStream(compressed); + decoder.decode(in, mockListener); + assertEquals(1, in.available()); + decoder.decode(in, mockListener); + assertEquals(1, in.available()); + } + + @Test(expected = IOException.class) + public void testUnusedIndex() throws IOException { + // Index 0 is not used + decode("80"); + } + + @Test(expected = IOException.class) + public void testIllegalIndex() throws IOException { + // Index larger than the header table + decode("FF00"); + } + + @Test(expected = IOException.class) + public void testInsidiousIndex() throws IOException { + // Insidious index so the last shift causes sign overflow + decode("FF8080808008"); + } + + @Test + public void testDynamicTableSizeUpdate() throws Exception { + decode("20"); + assertEquals(0, decoder.getMaxHeaderTableSize()); + decode("3FE11F"); + assertEquals(4096, decoder.getMaxHeaderTableSize()); + } + + @Test + public void testDynamicTableSizeUpdateRequired() throws Exception { + decoder.setMaxHeaderTableSize(32); + decode("3F00"); + assertEquals(31, decoder.getMaxHeaderTableSize()); + } + + @Test(expected = IOException.class) + public void testIllegalDynamicTableSizeUpdate() throws Exception { + // max header table size = MAX_HEADER_TABLE_SIZE + 1 + decode("3FE21F"); + } + + @Test(expected = IOException.class) + public void testInsidiousMaxDynamicTableSize() throws IOException { + // max header table size sign overflow + decode("3FE1FFFFFF07"); + } + + @Test + public void testReduceMaxDynamicTableSize() throws Exception { + decoder.setMaxHeaderTableSize(0); + assertEquals(0, decoder.getMaxHeaderTableSize()); + decode("2081"); + } + + @Test(expected = IOException.class) + public void testTooLargeDynamicTableSizeUpdate() throws Exception { + 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 { + decoder.setMaxHeaderTableSize(0); + assertEquals(0, decoder.getMaxHeaderTableSize()); + decode("81"); + } + + @Test(expected = IOException.class) + public void testLiteralWithIncrementalIndexingWithEmptyName() throws Exception { + decode("000005" + hex("value")); + } + + @Test + public void testLiteralWithIncrementalIndexingCompleteEviction() throws Exception { + // Verify indexed host header + decode("4004" + hex("name") + "05" + hex("value")); + verify(mockListener).addHeader(getBytes("name"), getBytes("value"), false); + verifyNoMoreInteractions(mockListener); + assertFalse(decoder.endHeaderBlock()); + + reset(mockListener); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 4096; i++) { + sb.append("a"); + } + String value = sb.toString(); + sb = new StringBuilder(); + sb.append("417F811F"); + for (int i = 0; i < 4096; i++) { + sb.append("61"); // 'a' + } + decode(sb.toString()); + verify(mockListener).addHeader(getBytes(":authority"), getBytes(value), false); + verifyNoMoreInteractions(mockListener); + assertFalse(decoder.endHeaderBlock()); + + // Verify next header is inserted at index 62 + decode("4004" + hex("name") + "05" + hex("value") + "BE"); + verify(mockListener, times(2)).addHeader(getBytes("name"), getBytes("value"), false); + verifyNoMoreInteractions(mockListener); + } + + @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(mockListener); + + // 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(mockListener, times(2)).addHeader(getBytes("name"), getBytes("value"), false); + verifyNoMoreInteractions(mockListener); + } + + @Test + public void testLiteralWithIncrementalIndexingWithLargeValue() throws Exception { + // Ignore header that exceeds max header size + StringBuilder sb = new StringBuilder(); + sb.append("4004"); + sb.append(hex("name")); + sb.append("7F813F"); + for (int i = 0; i < 8192; i++) { + sb.append("61"); // 'a' + } + decode(sb.toString()); + verifyNoMoreInteractions(mockListener); + + // 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(mockListener, times(2)).addHeader(getBytes("name"), getBytes("value"), false); + verifyNoMoreInteractions(mockListener); + } + + @Test(expected = IOException.class) + public void testLiteralWithoutIndexingWithEmptyName() throws Exception { + decode("000005" + hex("value")); + } + + @Test(expected = IOException.class) + public void testLiteralWithoutIndexingWithLargeName() throws Exception { + // Ignore header name that exceeds max header size + StringBuilder sb = new StringBuilder(); + sb.append("007F817F"); + for (int i = 0; i < 16384; i++) { + sb.append("61"); // 'a' + } + sb.append("00"); + decode(sb.toString()); + verifyNoMoreInteractions(mockListener); + + // 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 { + // Ignore header that exceeds max header size + StringBuilder sb = new StringBuilder(); + sb.append("0004"); + sb.append(hex("name")); + sb.append("7F813F"); + for (int i = 0; i < 8192; i++) { + sb.append("61"); // 'a' + } + decode(sb.toString()); + verifyNoMoreInteractions(mockListener); + + // Verify header block is reported as truncated + assertTrue(decoder.endHeaderBlock()); + + // Verify table is unmodified + decode("BE"); + } + + @Test(expected = IOException.class) + public void testLiteralNeverIndexedWithEmptyName() throws Exception { + decode("100005" + hex("value")); + } + + @Test(expected = IOException.class) + public void testLiteralNeverIndexedWithLargeName() throws Exception { + // Ignore header name that exceeds max header size + StringBuilder sb = new StringBuilder(); + sb.append("107F817F"); + for (int i = 0; i < 16384; i++) { + sb.append("61"); // 'a' + } + sb.append("00"); + decode(sb.toString()); + verifyNoMoreInteractions(mockListener); + + // 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 { + // Ignore header that exceeds max header size + StringBuilder sb = new StringBuilder(); + sb.append("1004"); + sb.append(hex("name")); + sb.append("7F813F"); + for (int i = 0; i < 8192; i++) { + sb.append("61"); // 'a' + } + decode(sb.toString()); + verifyNoMoreInteractions(mockListener); + + // Verify header block is reported as truncated + assertTrue(decoder.endHeaderBlock()); + + // Verify table is unmodified + decode("BE"); + } +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/Hex.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/Hex.java new file mode 100644 index 0000000000..f97561e163 --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/Hex.java @@ -0,0 +1,164 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.netty.handler.codec.http2.hpack; + +import java.io.IOException; + +/** + * Extracted from org/apache/commons/codec/binary/Hex.java Copyright Apache Software Foundation + */ +final class Hex { + private Hex() { + } + + /** + * Used to build output as Hex + */ + private static final char[] DIGITS_LOWER = + {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + /** + * Used to build output as Hex + */ + private static final char[] DIGITS_UPPER = + {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + + /** + * Converts an array of characters representing hexadecimal values into an array of bytes of + * those same values. The returned array will be half the length of the passed array, as it + * takes two characters to represent any given byte. An exception is thrown if the passed char + * array has an odd number of elements. + * + * @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 + */ + public static byte[] decodeHex(char[] data) throws IOException { + + int len = data.length; + + if ((len & 0x01) != 0) { + throw new IOException("Odd number of characters."); + } + + byte[] out = new byte[len >> 1]; + + // two characters form the hex value. + for (int i = 0, j = 0; j < len; i++) { + int f = toDigit(data[j], j) << 4; + j++; + f = f | toDigit(data[j], j); + j++; + out[i] = (byte) (f & 0xFF); + } + + return out; + } + + /** + * Converts an array of bytes into an array of characters representing the hexadecimal values of + * each byte in order. The returned array will be double the length of the passed array, as it + * takes two characters to represent any given byte. + * + * @param data a byte[] to convert to Hex characters + * @return A char[] containing hexadecimal characters + */ + public static char[] encodeHex(byte[] data) { + return encodeHex(data, true); + } + + /** + * Converts an array of bytes into an array of characters representing the hexadecimal values of + * each byte in order. The returned array will be double the length of the passed array, as it + * takes two characters to represent any given byte. + * + * @param data a byte[] to convert to Hex characters + * @param toLowerCase true converts to lowercase, false to uppercase + * @return A char[] containing hexadecimal characters + * @since 1.4 + */ + public static char[] encodeHex(byte[] data, boolean toLowerCase) { + return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER); + } + + /** + * Converts an array of bytes into an array of characters representing the hexadecimal values of + * each byte in order. The returned array will be double the length of the passed array, as it + * takes two characters to represent any given byte. + * + * @param data a byte[] to convert to Hex characters + * @param toDigits the output alphabet + * @return A char[] containing hexadecimal characters + * @since 1.4 + */ + protected static char[] encodeHex(byte[] data, char[] toDigits) { + int l = data.length; + char[] out = new char[l << 1]; + // two characters form the hex value. + for (int i = 0, j = 0; i < l; i++) { + out[j++] = toDigits[(0xF0 & data[i]) >>> 4]; + out[j++] = toDigits[0x0F & data[i]]; + } + return out; + } + + /** + * Converts an array of bytes into a String representing the hexadecimal values of each byte in + * order. The returned String will be double the length of the passed array, as it takes two + * characters to represent any given byte. + * + * @param data a byte[] to convert to Hex characters + * @return A String containing hexadecimal characters + * @since 1.4 + */ + public static String encodeHexString(byte[] data) { + return new String(encodeHex(data)); + } + + /** + * Converts a hexadecimal character to an integer. + * + * @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 + */ + protected static int toDigit(char ch, int index) throws IOException { + int digit = Character.digit(ch, 16); + if (digit == -1) { + throw new IOException("Illegal hexadecimal character " + ch + " at index " + index); + } + return digit; + } +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/HpackTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/HpackTest.java new file mode 100644 index 0000000000..794260eea7 --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/HpackTest.java @@ -0,0 +1,78 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Copyright 2014 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.netty.handler.codec.http2.hpack; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; + +@RunWith(Parameterized.class) +public class HpackTest { + + private static final String TEST_DIR = "/io/netty/handler/codec/http2/hpack/testdata/"; + + private final String fileName; + + public HpackTest(String fileName) { + this.fileName = fileName; + } + + @Parameters(name = "{0}") + public static Collection data() { + URL url = HpackTest.class.getResource(TEST_DIR); + File[] files = new File(url.getFile()).listFiles(); + if (files == null) { + throw new NullPointerException("files"); + } + + ArrayList data = new ArrayList(); + for (File file : files) { + data.add(new Object[]{file.getName()}); + } + return data; + } + + @Test + public void test() throws Exception { + InputStream is = HpackTest.class.getResourceAsStream(TEST_DIR + fileName); + TestCase testCase = TestCase.load(is); + testCase.testCompress(); + testCase.testDecompress(); + } +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/HuffmanTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/HuffmanTest.java new file mode 100644 index 0000000000..09a0575db5 --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/HuffmanTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Copyright 2014 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.netty.handler.codec.http2.hpack; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Random; + +public class HuffmanTest { + + @Test + public void testHuffman() throws IOException { + + String s = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (int i = 0; i < s.length(); i++) { + roundTrip(s.substring(0, i)); + } + + Random random = new Random(123456789L); + byte[] buf = new byte[4096]; + random.nextBytes(buf); + roundTrip(buf); + } + + @Test(expected = IOException.class) + public void testDecodeEOS() throws IOException { + byte[] buf = new byte[4]; + for (int i = 0; i < 4; i++) { + buf[i] = (byte) 0xFF; + } + Huffman.DECODER.decode(buf); + } + + @Test(expected = IOException.class) + public void testDecodeIllegalPadding() throws IOException { + byte[] buf = new byte[1]; + buf[0] = 0x00; // '0', invalid padding + Huffman.DECODER.decode(buf); + } + + @Test//(expected = IOException.class) TODO(jpinner) fix me + public void testDecodeExtraPadding() throws IOException { + byte[] buf = new byte[2]; + buf[0] = 0x0F; // '1', 'EOS' + buf[1] = (byte) 0xFF; // 'EOS' + Huffman.DECODER.decode(buf); + } + + private void roundTrip(String s) throws IOException { + roundTrip(Huffman.ENCODER, Huffman.DECODER, s); + } + + private static void roundTrip(HuffmanEncoder encoder, HuffmanDecoder decoder, String s) + throws IOException { + roundTrip(encoder, decoder, s.getBytes()); + } + + private void roundTrip(byte[] buf) throws IOException { + roundTrip(Huffman.ENCODER, Huffman.DECODER, buf); + } + + private static void roundTrip(HuffmanEncoder encoder, HuffmanDecoder decoder, byte[] buf) + throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + + encoder.encode(dos, buf); + + byte[] actualBytes = decoder.decode(baos.toByteArray()); + + Assert.assertTrue(Arrays.equals(buf, actualBytes)); + } +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/TestCase.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/TestCase.java new file mode 100644 index 0000000000..0177bb288a --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/TestCase.java @@ -0,0 +1,253 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Copyright 2014 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.netty.handler.codec.http2.hpack; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +final class TestCase { + + private static final Gson GSON = new GsonBuilder() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .registerTypeAdapter(HeaderField.class, new HeaderFieldDeserializer()) + .create(); + + int maxHeaderTableSize = -1; + boolean useIndexing = true; + boolean sensitiveHeaders; + boolean forceHuffmanOn; + boolean forceHuffmanOff; + + List headerBlocks; + + private TestCase() { + } + + static TestCase load(InputStream is) throws IOException { + InputStreamReader r = new InputStreamReader(is); + TestCase testCase = GSON.fromJson(r, TestCase.class); + for (HeaderBlock headerBlock : testCase.headerBlocks) { + headerBlock.encodedBytes = Hex.decodeHex(headerBlock.getEncodedStr().toCharArray()); + } + return testCase; + } + + void testCompress() throws Exception { + Encoder encoder = createEncoder(); + + for (HeaderBlock headerBlock : headerBlocks) { + + byte[] actual = + encode(encoder, headerBlock.getHeaders(), headerBlock.getMaxHeaderTableSize(), + sensitiveHeaders); + + if (!Arrays.equals(actual, headerBlock.encodedBytes)) { + throw new AssertionError( + "\nEXPECTED:\n" + headerBlock.getEncodedStr() + + "\nACTUAL:\n" + Hex.encodeHexString(actual)); + } + + List actualDynamicTable = new ArrayList(); + for (int index = 0; index < encoder.length(); index++) { + actualDynamicTable.add(encoder.getHeaderField(index)); + } + + List expectedDynamicTable = headerBlock.getDynamicTable(); + + if (!expectedDynamicTable.equals(actualDynamicTable)) { + throw new AssertionError( + "\nEXPECTED DYNAMIC TABLE:\n" + expectedDynamicTable + + "\nACTUAL DYNAMIC TABLE:\n" + actualDynamicTable); + } + + if (headerBlock.getTableSize() != encoder.size()) { + throw new AssertionError( + "\nEXPECTED TABLE SIZE: " + headerBlock.getTableSize() + + "\n ACTUAL TABLE SIZE : " + encoder.size()); + } + } + } + + void testDecompress() throws Exception { + Decoder decoder = createDecoder(); + + for (HeaderBlock headerBlock : headerBlocks) { + + List actualHeaders = decode(decoder, headerBlock.encodedBytes); + + List expectedHeaders = new ArrayList(); + for (HeaderField h : headerBlock.getHeaders()) { + expectedHeaders.add(new HeaderField(h.name, h.value)); + } + + if (!expectedHeaders.equals(actualHeaders)) { + throw new AssertionError( + "\nEXPECTED:\n" + expectedHeaders + + "\nACTUAL:\n" + actualHeaders); + } + + List actualDynamicTable = new ArrayList(); + for (int index = 0; index < decoder.length(); index++) { + actualDynamicTable.add(decoder.getHeaderField(index)); + } + + List expectedDynamicTable = headerBlock.getDynamicTable(); + + if (!expectedDynamicTable.equals(actualDynamicTable)) { + throw new AssertionError( + "\nEXPECTED DYNAMIC TABLE:\n" + expectedDynamicTable + + "\nACTUAL DYNAMIC TABLE:\n" + actualDynamicTable); + } + + if (headerBlock.getTableSize() != decoder.size()) { + throw new AssertionError( + "\nEXPECTED TABLE SIZE: " + headerBlock.getTableSize() + + "\n ACTUAL TABLE SIZE : " + decoder.size()); + } + } + } + + private Encoder createEncoder() { + int maxHeaderTableSize = this.maxHeaderTableSize; + if (maxHeaderTableSize == -1) { + maxHeaderTableSize = Integer.MAX_VALUE; + } + + return new Encoder(maxHeaderTableSize, useIndexing, forceHuffmanOn, forceHuffmanOff); + } + + private Decoder createDecoder() { + int maxHeaderTableSize = this.maxHeaderTableSize; + if (maxHeaderTableSize == -1) { + maxHeaderTableSize = Integer.MAX_VALUE; + } + + return new Decoder(8192, maxHeaderTableSize); + } + + private static byte[] encode(Encoder encoder, List headers, int maxHeaderTableSize, + boolean sensitive) + throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + if (maxHeaderTableSize != -1) { + encoder.setMaxHeaderTableSize(baos, maxHeaderTableSize); + } + + for (HeaderField e : headers) { + encoder.encodeHeader(baos, e.name, e.value, sensitive); + } + + return baos.toByteArray(); + } + + private static List decode(Decoder decoder, byte[] expected) throws IOException { + List headers = new ArrayList(); + TestHeaderListener listener = new TestHeaderListener(headers); + decoder.decode(new ByteArrayInputStream(expected), listener); + decoder.endHeaderBlock(); + return headers; + } + + private static String concat(List l) { + StringBuilder ret = new StringBuilder(); + for (String s : l) { + ret.append(s); + } + return ret.toString(); + } + + static class HeaderBlock { + private int maxHeaderTableSize = -1; + private byte[] encodedBytes; + private List encoded; + private List headers; + private List dynamicTable; + private int tableSize; + + private int getMaxHeaderTableSize() { + return maxHeaderTableSize; + } + + public String getEncodedStr() { + return concat(encoded).replaceAll(" ", ""); + } + + public List getHeaders() { + return headers; + } + + public List getDynamicTable() { + return dynamicTable; + } + + public int getTableSize() { + return tableSize; + } + } + + static class HeaderFieldDeserializer implements JsonDeserializer { + + @Override + public HeaderField deserialize(JsonElement json, Type typeOfT, + JsonDeserializationContext context) + throws JsonParseException { + JsonObject jsonObject = json.getAsJsonObject(); + Set> entrySet = jsonObject.entrySet(); + if (entrySet.size() != 1) { + throw new JsonParseException("JSON Object has multiple entries: " + entrySet); + } + Map.Entry entry = entrySet.iterator().next(); + String name = entry.getKey(); + String value = entry.getValue().getAsString(); + return new HeaderField(name, value); + } + } +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/TestHeaderListener.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/TestHeaderListener.java new file mode 100644 index 0000000000..d8bf747f2d --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/TestHeaderListener.java @@ -0,0 +1,48 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Copyright 2014 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.netty.handler.codec.http2.hpack; + +import java.util.List; + +final class TestHeaderListener implements HeaderListener { + + private final List headers; + + TestHeaderListener(List headers) { + this.headers = headers; + } + + @Override + public void addHeader(byte[] name, byte[] value, boolean sensitive) { + headers.add(new HeaderField(name, value)); + } +} diff --git a/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testDuplicateHeaders.json b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testDuplicateHeaders.json new file mode 100644 index 0000000000..7c95a71da5 --- /dev/null +++ b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testDuplicateHeaders.json @@ -0,0 +1,67 @@ +{ + "force_huffman_on": true, + "header_blocks": + [ + { + "headers": [ + { ":path": "/somepath" }, + { "x-custom": "val" } + ], + "encoded": [ + "4487 6107 a4b5 8d33 ff40 86f2 b12d 424f", + "4f83 ee3a 3f" + ], + "dynamic_table": [ + { "x-custom": "val" }, + { ":path": "/somepath" } + ], + "table_size": 89 + }, + { + "headers": [ + { ":path": "/somepath" }, + { "x-custom": "val" }, + { "x-custom": "val" } + ], + "encoded": [ + "bfbe be" + ], + "dynamic_table": [ + { "x-custom": "val" }, + { ":path": "/somepath" } + ], + "table_size": 89 + }, + { + "headers": [ + { ":path": "/somepath" }, + { "x-custom": "val" }, + { "foo": "bar" }, + { "x-custom": "val" } + ], + "encoded": [ + "bfbe 4082 94e7 838c 767f bf" + ], + "dynamic_table": [ + { "foo": "bar" }, + { "x-custom": "val" }, + { ":path": "/somepath" } + ], + "table_size": 127 + }, + { + "headers": [ + { ":path": "/somepath" } + ], + "encoded": [ + "c0" + ], + "dynamic_table": [ + { "foo": "bar" }, + { "x-custom": "val" }, + { ":path": "/somepath" } + ], + "table_size": 127 + } + ] +} diff --git a/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testEmpty.json b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testEmpty.json new file mode 100644 index 0000000000..19b46dd617 --- /dev/null +++ b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testEmpty.json @@ -0,0 +1,14 @@ +{ + "header_blocks": + [ + { + "headers": [ + ], + "encoded": [ + ], + "dynamic_table": [ + ], + "table_size": 0 + } + ] +} diff --git a/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testEviction.json b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testEviction.json new file mode 100644 index 0000000000..25693d9f10 --- /dev/null +++ b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testEviction.json @@ -0,0 +1,58 @@ +{ + "max_header_table_size": 128, + "force_huffman_on": true, + "header_blocks": + [ + { + "headers": [ + { ":path": "/somepath" }, + { "x-custom": "val1" }, + { "x-custom": "val2" }, + { "x-custom": "val3" } + ], + "encoded": [ + "4487 6107 a4b5 8d33 ff40 86f2 b12d 424f", + "4f83 ee3a 037e 83ee 3a05 7e83 ee3a 19" + ], + "dynamic_table": [ + { "x-custom": "val3" }, + { "x-custom": "val2" } + ], + "table_size": 88 + }, + { + "headers": [ + { ":path": "/somepath" }, + { "x-custom": "val4" }, + { "x-custom": "val5" }, + { "x-custom": "val6" } + ], + "encoded": [ + "4487 6107 a4b5 8d33 ff7f 0083 ee3a 1a7e", + "83ee 3a1b 7e83 ee3a 1c" + ], + "dynamic_table": [ + { "x-custom": "val6" }, + { "x-custom": "val5" } + ], + "table_size": 88 + }, + { + "headers": [ + { ":path": "/somepath" }, + { "x-custom": "val1" }, + { "x-custom": "val2" }, + { "x-custom": "val3" } + ], + "encoded": [ + "4487 6107 a4b5 8d33 ff7f 0083 ee3a 037e", + "83ee 3a05 7e83 ee3a 19" + ], + "dynamic_table": [ + { "x-custom": "val3" }, + { "x-custom": "val2" } + ], + "table_size": 88 + } + ] +} diff --git a/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testMaxHeaderTableSize.json b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testMaxHeaderTableSize.json new file mode 100644 index 0000000000..274e11c8e0 --- /dev/null +++ b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testMaxHeaderTableSize.json @@ -0,0 +1,57 @@ +{ + "max_header_table_size": 128, + "force_huffman_off": true, + "header_blocks": + [ + { + "headers": [ + { "name1": "val1" }, + { "name2": "val2" }, + { "name3": "val3" } + ], + "encoded": [ + "4005 6e61 6d65 3104 7661 6c31 4005 6e61", + "6d65 3204 7661 6c32 4005 6e61 6d65 3304", + "7661 6c33" + ], + "dynamic_table": [ + { "name3": "val3" }, + { "name2": "val2" }, + { "name1": "val1" } + ], + "table_size": 123 + }, + { + "max_header_table_size": 81, + "headers": [ + { "name3": "val3" }, + { "name2": "val2" } + ], + "encoded": [ + "3f32 be40 056e 616d 6532 0476 616c 32" + ], + "dynamic_table": [ + { "name2": "val2" } + ], + "table_size": 41 + }, + { + "max_header_table_size": 128, + "headers": [ + { "name1": "val1" }, + { "name2": "val2" }, + { "name3": "val3" } + ], + "encoded": [ + "3f61 4005 6e61 6d65 3104 7661 6c31 bf40", + "056e 616d 6533 0476 616c 33" + ], + "dynamic_table": [ + { "name3": "val3" }, + { "name1": "val1" }, + { "name2": "val2" } + ], + "table_size": 123 + } + ] +} diff --git a/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC2_1.json b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC2_1.json new file mode 100644 index 0000000000..6ea9b6f9d6 --- /dev/null +++ b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC2_1.json @@ -0,0 +1,19 @@ +{ + "force_huffman_off": true, + "header_blocks": + [ + { + "headers": [ + { "custom-key": "custom-header" } + ], + "encoded": [ + "400a 6375 7374 6f6d 2d6b 6579 0d63 7573", + "746f 6d2d 6865 6164 6572" + ], + "dynamic_table": [ + { "custom-key": "custom-header" } + ], + "table_size": 55 + } + ] +} diff --git a/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC2_2.json b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC2_2.json new file mode 100644 index 0000000000..5a222a7af6 --- /dev/null +++ b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC2_2.json @@ -0,0 +1,18 @@ +{ + "use_indexing": false, + "force_huffman_off": true, + "header_blocks": + [ + { + "headers": [ + { ":path": "/sample/path" } + ], + "encoded": [ + "040c 2f73 616d 706c 652f 7061 7468" + ], + "dynamic_table": [ + ], + "table_size": 0 + } + ] +} diff --git a/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC2_3.json b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC2_3.json new file mode 100644 index 0000000000..534e8f308b --- /dev/null +++ b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC2_3.json @@ -0,0 +1,19 @@ +{ + "sensitive_headers": true, + "force_huffman_off": true, + "header_blocks": + [ + { + "headers": [ + { "password": "secret" } + ], + "encoded": [ + "1008 7061 7373 776f 7264 0673 6563 7265", + "74" + ], + "dynamic_table": [ + ], + "table_size": 0 + } + ] +} diff --git a/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC2_4.json b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC2_4.json new file mode 100644 index 0000000000..4e8c483fdc --- /dev/null +++ b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC2_4.json @@ -0,0 +1,17 @@ +{ + "force_huffman_off": true, + "header_blocks": + [ + { + "headers": [ + { ":method": "GET" } + ], + "encoded": [ + "82" + ], + "dynamic_table": [ + ], + "table_size": 0 + } + ] +} diff --git a/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC3.json b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC3.json new file mode 100644 index 0000000000..93ece94709 --- /dev/null +++ b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC3.json @@ -0,0 +1,58 @@ +{ + "force_huffman_off": true, + "header_blocks": + [ + { + "headers": [ + { ":method": "GET" }, + { ":scheme": "http" }, + { ":path": "/" }, + { ":authority": "www.example.com" } + ], + "encoded": [ + "8286 8441 0f77 7777 2e65 7861 6d70 6c65", + "2e63 6f6d" + ], + "dynamic_table": [ + { ":authority": "www.example.com" } + ], + "table_size": 57 + }, + { + "headers": [ + { ":method": "GET" }, + { ":scheme": "http" }, + { ":path": "/" }, + { ":authority": "www.example.com" }, + { "cache-control": "no-cache" } + ], + "encoded": [ + "8286 84be 5808 6e6f 2d63 6163 6865" + ], + "dynamic_table": [ + { "cache-control": "no-cache" }, + { ":authority": "www.example.com" } + ], + "table_size": 110 + }, + { + "headers": [ + { ":method": "GET" }, + { ":scheme": "https" }, + { ":path": "/index.html" }, + { ":authority": "www.example.com" }, + { "custom-key": "custom-value" } + ], + "encoded": [ + "8287 85bf 400a 6375 7374 6f6d 2d6b 6579", + "0c63 7573 746f 6d2d 7661 6c75 65" + ], + "dynamic_table": [ + { "custom-key": "custom-value" }, + { "cache-control": "no-cache" }, + { ":authority": "www.example.com" } + ], + "table_size": 164 + } + ] +} diff --git a/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC4.json b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC4.json new file mode 100644 index 0000000000..0543f53af9 --- /dev/null +++ b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC4.json @@ -0,0 +1,58 @@ +{ + "force_huffman_on": true, + "header_blocks": + [ + { + "headers": [ + { ":method": "GET" }, + { ":scheme": "http" }, + { ":path": "/" }, + { ":authority": "www.example.com" } + ], + "encoded": [ + "8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4", + "ff" + ], + "dynamic_table": [ + { ":authority": "www.example.com" } + ], + "table_size": 57 + }, + { + "headers": [ + { ":method": "GET" }, + { ":scheme": "http" }, + { ":path": "/" }, + { ":authority": "www.example.com" }, + { "cache-control": "no-cache" } + ], + "encoded": [ + "8286 84be 5886 a8eb 1064 9cbf" + ], + "dynamic_table": [ + { "cache-control": "no-cache" }, + { ":authority": "www.example.com" } + ], + "table_size": 110 + }, + { + "headers": [ + { ":method": "GET" }, + { ":scheme": "https" }, + { ":path": "/index.html" }, + { ":authority": "www.example.com" }, + { "custom-key": "custom-value" } + ], + "encoded": [ + "8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925", + "a849 e95b b8e8 b4bf" + ], + "dynamic_table": [ + { "custom-key": "custom-value" }, + { "cache-control": "no-cache" }, + { ":authority": "www.example.com" } + ], + "table_size": 164 + } + ] +} diff --git a/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC5.json b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC5.json new file mode 100644 index 0000000000..93884b2b28 --- /dev/null +++ b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC5.json @@ -0,0 +1,72 @@ +{ + "max_header_table_size": 256, + "force_huffman_off": true, + "header_blocks": + [ + { + "headers": [ + { ":status": "302" }, + { "cache-control": "private" }, + { "date": "Mon, 21 Oct 2013 20:13:21 GMT" }, + { "location": "https://www.example.com" } + ], + "encoded": [ + "4803 3330 3258 0770 7269 7661 7465 611d", + "4d6f 6e2c 2032 3120 4f63 7420 3230 3133", + "2032 303a 3133 3a32 3120 474d 546e 1768", + "7474 7073 3a2f 2f77 7777 2e65 7861 6d70", + "6c65 2e63 6f6d" + ], + "dynamic_table": [ + { "location": "https://www.example.com" }, + { "date": "Mon, 21 Oct 2013 20:13:21 GMT" }, + { "cache-control": "private" }, + { ":status": "302" } + ], + "table_size": 222 + }, + { + "headers": [ + { ":status": "307" }, + { "cache-control": "private" }, + { "date": "Mon, 21 Oct 2013 20:13:21 GMT" }, + { "location": "https://www.example.com" } + ], + "encoded": [ + "4803 3330 37c1 c0bf" + ], + "dynamic_table": [ + { ":status": "307" }, + { "location": "https://www.example.com" }, + { "date": "Mon, 21 Oct 2013 20:13:21 GMT" }, + { "cache-control": "private" } + ], + "table_size": 222 + }, + { + "headers": [ + { ":status": "200" }, + { "cache-control": "private" }, + { "date": "Mon, 21 Oct 2013 20:13:22 GMT" }, + { "location": "https://www.example.com" }, + { "content-encoding": "gzip" }, + { "set-cookie": "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1" } + ], + "encoded": [ + "88c1 611d 4d6f 6e2c 2032 3120 4f63 7420", + "3230 3133 2032 303a 3133 3a32 3220 474d", + "54c0 5a04 677a 6970 7738 666f 6f3d 4153", + "444a 4b48 514b 425a 584f 5157 454f 5049", + "5541 5851 5745 4f49 553b 206d 6178 2d61", + "6765 3d33 3630 303b 2076 6572 7369 6f6e", + "3d31" + ], + "dynamic_table": [ + { "set-cookie": "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1" }, + { "content-encoding": "gzip" }, + { "date": "Mon, 21 Oct 2013 20:13:22 GMT" } + ], + "table_size": 215 + } + ] +} diff --git a/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC6.json b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC6.json new file mode 100644 index 0000000000..78b00e751a --- /dev/null +++ b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testSpecExampleC6.json @@ -0,0 +1,69 @@ +{ + "max_header_table_size": 256, + "force_huffman_on": true, + "header_blocks": + [ + { + "headers": [ + { ":status": "302" }, + { "cache-control": "private" }, + { "date": "Mon, 21 Oct 2013 20:13:21 GMT" }, + { "location": "https://www.example.com" } + ], + "encoded": [ + "4882 6402 5885 aec3 771a 4b61 96d0 7abe", + "9410 54d4 44a8 2005 9504 0b81 66e0 82a6", + "2d1b ff6e 919d 29ad 1718 63c7 8f0b 97c8", + "e9ae 82ae 43d3" + ], + "dynamic_table": [ + { "location": "https://www.example.com" }, + { "date": "Mon, 21 Oct 2013 20:13:21 GMT" }, + { "cache-control": "private" }, + { ":status": "302" } + ], + "table_size": 222 + }, + { + "headers": [ + { ":status": "307" }, + { "cache-control": "private" }, + { "date": "Mon, 21 Oct 2013 20:13:21 GMT" }, + { "location": "https://www.example.com" } + ], + "encoded": [ + "4883 640e ffc1 c0bf" + ], + "dynamic_table": [ + { ":status": "307" }, + { "location": "https://www.example.com" }, + { "date": "Mon, 21 Oct 2013 20:13:21 GMT" }, + { "cache-control": "private" } + ], + "table_size": 222 + }, + { + "headers": [ + { ":status": "200" }, + { "cache-control": "private" }, + { "date": "Mon, 21 Oct 2013 20:13:22 GMT" }, + { "location": "https://www.example.com" }, + { "content-encoding": "gzip" }, + { "set-cookie": "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1" } + ], + "encoded": [ + "88c1 6196 d07a be94 1054 d444 a820 0595", + "040b 8166 e084 a62d 1bff c05a 839b d9ab", + "77ad 94e7 821d d7f2 e6c7 b335 dfdf cd5b", + "3960 d5af 2708 7f36 72c1 ab27 0fb5 291f", + "9587 3160 65c0 03ed 4ee5 b106 3d50 07" + ], + "dynamic_table": [ + { "set-cookie": "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1" }, + { "content-encoding": "gzip" }, + { "date": "Mon, 21 Oct 2013 20:13:22 GMT" } + ], + "table_size": 215 + } + ] +} diff --git a/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testStaticTableEntries.json b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testStaticTableEntries.json new file mode 100644 index 0000000000..1e3e1683d8 --- /dev/null +++ b/codec-http2/src/test/resources/io/netty/handler/codec/http2/hpack/testdata/testStaticTableEntries.json @@ -0,0 +1,81 @@ +{ + "force_huffman_on": true, + "header_blocks": + [ + { + "headers": [ + { ":authority": "" }, + { ":method": "GET" }, + { ":method": "POST" }, + { ":path": "/" }, + { ":path": "/index.html" }, + { ":scheme": "http" }, + { ":scheme": "https" }, + { ":status": "200" }, + { ":status": "204" }, + { ":status": "206" }, + { ":status": "304" }, + { ":status": "400" }, + { ":status": "404" }, + { ":status": "500" }, + { "accept-charset": "" }, + { "accept-encoding": "gzip, deflate" }, + { "accept-language": "" }, + { "accept-ranges": "" }, + { "accept": "" }, + { "access-control-allow-origin": "" }, + { "age": "" }, + { "allow": "" }, + { "authorization": "" }, + { "cache-control": "" }, + { "content-disposition": "" }, + { "content-encoding": "" }, + { "content-language": "" }, + { "content-length": "" }, + { "content-location": "" }, + { "content-range": "" }, + { "content-type": "" }, + { "cookie": "" }, + { "date": "" }, + { "etag": "" }, + { "expect": "" }, + { "expires": "" }, + { "from": "" }, + { "host": "" }, + { "if-match": "" }, + { "if-modified-since": "" }, + { "if-none-match": "" }, + { "if-range": "" }, + { "if-unmodified-since": "" }, + { "last-modified": "" }, + { "link": "" }, + { "location": "" }, + { "max-forwards": "" }, + { "proxy-authenticate": "" }, + { "proxy-authorization": "" }, + { "range": "" }, + { "referer": "" }, + { "refresh": "" }, + { "retry-after": "" }, + { "server": "" }, + { "set-cookie": "" }, + { "strict-transport-security": "" }, + { "transfer-encoding": "" }, + { "user-agent": "" }, + { "vary": "" }, + { "via": "" }, + { "www-authenticate": "" } + ], + "encoded": [ + "8182 8384 8586 8788 898a 8b8c 8d8e 8f90", + "9192 9394 9596 9798 999a 9b9c 9d9e 9fa0", + "a1a2 a3a4 a5a6 a7a8 a9aa abac adae afb0", + "b1b2 b3b4 b5b6 b7b8 b9ba bbbc bd" + ], + "dynamic_table": [ + ], + "table_size": 0 + } + ] +} + diff --git a/license/LICENSE.hpack.txt b/license/LICENSE.hpack.txt new file mode 100644 index 0000000000..8dada3edaf --- /dev/null +++ b/license/LICENSE.hpack.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/microbench/src/main/java/io/netty/microbench/http2/hpack/DecoderBenchmark.java b/microbench/src/main/java/io/netty/microbench/http2/hpack/DecoderBenchmark.java new file mode 100644 index 0000000000..0ef4291d8e --- /dev/null +++ b/microbench/src/main/java/io/netty/microbench/http2/hpack/DecoderBenchmark.java @@ -0,0 +1,99 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Copyright 2015 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.netty.microbench.http2.hpack; + +import io.netty.handler.codec.http2.hpack.Decoder; +import io.netty.handler.codec.http2.hpack.Encoder; +import io.netty.handler.codec.http2.hpack.HeaderListener; +import io.netty.microbench.util.AbstractMicrobenchmark; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.infra.Blackhole; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; + +public class DecoderBenchmark extends AbstractMicrobenchmark { + + @Param + public HeadersSize size; + + @Param({ "4096" }) + public int maxTableSize; + + @Param({ "8192" }) + public int maxHeaderSize; + + @Param({ "true", "false" }) + public boolean sensitive; + + @Param({ "true", "false" }) + public boolean limitToAscii; + + private byte[] input; + + @Setup(Level.Trial) + public void setup() throws IOException { + input = getSerializedHeaders(Util.headers(size, limitToAscii), sensitive); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + public void decode(final Blackhole bh) throws IOException { + Decoder decoder = new Decoder(maxHeaderSize, maxTableSize); + decoder.decode(new ByteArrayInputStream(input), new HeaderListener() { + @Override + public void addHeader(byte[] name, byte[] value, boolean sensitive) { + bh.consume(sensitive); + } + }); + decoder.endHeaderBlock(); + } + + private byte[] getSerializedHeaders(List
headers, boolean sensitive) + throws IOException { + Encoder encoder = new Encoder(4096); + + ByteArrayOutputStream outputStream = size.newOutputStream(); + for (int i = 0; i < headers.size(); ++i) { + Header header = headers.get(i); + encoder.encodeHeader(outputStream, header.name, header.value, sensitive); + } + return outputStream.toByteArray(); + } +} diff --git a/microbench/src/main/java/io/netty/microbench/http2/hpack/EncoderBenchmark.java b/microbench/src/main/java/io/netty/microbench/http2/hpack/EncoderBenchmark.java new file mode 100644 index 0000000000..1f532fe14b --- /dev/null +++ b/microbench/src/main/java/io/netty/microbench/http2/hpack/EncoderBenchmark.java @@ -0,0 +1,93 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Copyright 2015 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.netty.microbench.http2.hpack; + +import io.netty.handler.codec.http2.hpack.Encoder; +import io.netty.microbench.util.AbstractMicrobenchmark; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.infra.Blackhole; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; + +public class EncoderBenchmark extends AbstractMicrobenchmark { + + @Param + public HeadersSize size; + + @Param({ "4096" }) + public int maxTableSize; + + @Param({ "true", "false" }) + public boolean sensitive; + + @Param({ "true", "false" }) + public boolean duplicates; + + @Param({ "true", "false" }) + public boolean limitToAscii; + + private List
headers; + private ByteArrayOutputStream outputStream; + + @Setup(Level.Trial) + public void setup() { + headers = Util.headers(size, limitToAscii); + outputStream = size.newOutputStream(); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + public void encode(Blackhole bh) throws IOException { + Encoder encoder = new Encoder(maxTableSize); + outputStream.reset(); + if (duplicates) { + // If duplicates is set, re-add the same header each time. + Header header = headers.get(0); + for (int i = 0; i < headers.size(); ++i) { + encoder.encodeHeader(outputStream, header.name, header.value, sensitive); + } + } else { + for (int i = 0; i < headers.size(); ++i) { + Header header = headers.get(i); + encoder.encodeHeader(outputStream, header.name, header.value, sensitive); + } + } + bh.consume(outputStream); + } +} diff --git a/microbench/src/main/java/io/netty/microbench/http2/hpack/Header.java b/microbench/src/main/java/io/netty/microbench/http2/hpack/Header.java new file mode 100644 index 0000000000..3150a41ed8 --- /dev/null +++ b/microbench/src/main/java/io/netty/microbench/http2/hpack/Header.java @@ -0,0 +1,79 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Copyright 2015 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.netty.microbench.http2.hpack; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * Helper class representing a single header entry. Used by the benchmarks. + */ +class Header { + private static final String ALPHABET = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"; + + final byte[] name; + final byte[] value; + + Header(byte[] name, byte[] value) { + this.name = name; + this.value = value; + } + + /** + * Creates a number of random headers with the given name/value lengths. + */ + static List
createHeaders(int numHeaders, int nameLength, int valueLength, + boolean limitToAscii) { + List
headers = new ArrayList
(numHeaders); + for (int i = 0; i < numHeaders; ++i) { + byte[] name = randomBytes(new byte[nameLength], limitToAscii); + byte[] value = randomBytes(new byte[valueLength], limitToAscii); + headers.add(new Header(name, value)); + } + return headers; + } + + private static byte[] randomBytes(byte[] bytes, boolean limitToAscii) { + Random r = new Random(); + if (limitToAscii) { + for (int index = 0; index < bytes.length; ++index) { + int charIndex = r.nextInt(ALPHABET.length()); + bytes[index] = (byte) ALPHABET.charAt(charIndex); + } + } else { + r.nextBytes(bytes); + } + return bytes; + } +} diff --git a/microbench/src/main/java/io/netty/microbench/http2/hpack/HeadersSize.java b/microbench/src/main/java/io/netty/microbench/http2/hpack/HeadersSize.java new file mode 100644 index 0000000000..0750b20304 --- /dev/null +++ b/microbench/src/main/java/io/netty/microbench/http2/hpack/HeadersSize.java @@ -0,0 +1,62 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Copyright 2015 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.netty.microbench.http2.hpack; + +import java.io.ByteArrayOutputStream; +import java.util.List; + +/** + * Enum that indicates the size of the headers to be used for the benchmark. + */ +public enum HeadersSize { + SMALL(5, 20, 40), + MEDIUM(20, 40, 80), + LARGE(100, 100, 300); + + private final int numHeaders; + private final int nameLength; + private final int valueLength; + + HeadersSize(int numHeaders, int nameLength, int valueLength) { + this.numHeaders = numHeaders; + this.nameLength = nameLength; + this.valueLength = valueLength; + } + + public List
newHeaders(boolean limitAscii) { + return Header.createHeaders(numHeaders, nameLength, valueLength, limitAscii); + } + + public ByteArrayOutputStream newOutputStream() { + return new ByteArrayOutputStream(numHeaders * (nameLength + valueLength)); + } +} diff --git a/microbench/src/main/java/io/netty/microbench/http2/hpack/Util.java b/microbench/src/main/java/io/netty/microbench/http2/hpack/Util.java new file mode 100644 index 0000000000..018f210357 --- /dev/null +++ b/microbench/src/main/java/io/netty/microbench/http2/hpack/Util.java @@ -0,0 +1,106 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Copyright 2015 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.netty.microbench.http2.hpack; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Utility methods for hpack tests. + */ +public final class Util { + private Util() { + } + + /** + * Internal key used to index a particular set of headers in the map. + */ + private static class HeadersKey { + final HeadersSize size; + final boolean limitToAscii; + + public HeadersKey(HeadersSize size, boolean limitToAscii) { + this.size = size; + this.limitToAscii = limitToAscii; + } + + List
newHeaders() { + return size.newHeaders(limitToAscii); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + HeadersKey that = (HeadersKey) o; + + if (limitToAscii != that.limitToAscii) { + return false; + } + return size == that.size; + } + + @Override + public int hashCode() { + int result = size.hashCode(); + result = 31 * result + (limitToAscii ? 1 : 0); + return result; + } + } + + private static final Map> headersMap; + + static { + HeadersSize[] sizes = HeadersSize.values(); + headersMap = new HashMap>(sizes.length * 2); + for (HeadersSize size : sizes) { + HeadersKey key = new HeadersKey(size, true); + headersMap.put(key, key.newHeaders()); + + key = new HeadersKey(size, false); + headersMap.put(key, key.newHeaders()); + } + } + + /** + * Gets headers for the given size and whether the key/values should be limited to ASCII. + */ + static List
headers(HeadersSize size, boolean limitToAscii) { + return headersMap.get(new HeadersKey(size, limitToAscii)); + } +} diff --git a/microbench/src/main/java/io/netty/microbench/http2/hpack/package-info.java b/microbench/src/main/java/io/netty/microbench/http2/hpack/package-info.java new file mode 100644 index 0000000000..1f7579b3d6 --- /dev/null +++ b/microbench/src/main/java/io/netty/microbench/http2/hpack/package-info.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Copyright 2015 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Benchmarks for {@link io.netty.handler.codec.http2.hpack}. + */ +package io.netty.microbench.http2.hpack; diff --git a/pom.xml b/pom.xml index 9247be86ec..5dee5054e9 100644 --- a/pom.xml +++ b/pom.xml @@ -676,11 +676,6 @@ - - com.twitter - hpack - v1.0.1 - org.eclipse.jetty.npn npn-api @@ -899,6 +894,14 @@ test + + + com.google.code.gson + gson + 2.3.1 + test + + org.tukaani