/* * Copyright (C) 2013 Square, 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 okhttp3.internal.http2; import java.io.IOException; import java.util.Arrays; import java.util.List; import okio.Buffer; import okio.ByteString; import org.junit.Before; import org.junit.Test; import static okhttp3.TestUtil.headerEntries; import static okio.ByteString.decodeHex; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; public final class HpackTest { private final Buffer bytesIn = new Buffer(); private Hpack.Reader hpackReader; private Buffer bytesOut = new Buffer(); private Hpack.Writer hpackWriter; @Before public void reset() { hpackReader = newReader(bytesIn); hpackWriter = new Hpack.Writer(4096, false, bytesOut); } /** * Variable-length quantity special cases strings which are longer than 127 bytes. Values such as * cookies can be 4KiB, and should be possible to send. * * <p> http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#section-5.2 */ @Test public void largeHeaderValue() throws IOException { char[] value = new char[4096]; Arrays.fill(value, '!'); List<Header> headerBlock = headerEntries("cookie", new String(value)); hpackWriter.writeHeaders(headerBlock); bytesIn.writeAll(bytesOut); hpackReader.readHeaders(); assertEquals(0, hpackReader.headerCount); assertEquals(headerBlock, hpackReader.getAndResetHeaderList()); } /** * HPACK has a max header table size, which can be smaller than the max header message. Ensure the * larger header content is not lost. */ @Test public void tooLargeToHPackIsStillEmitted() throws IOException { bytesIn.writeByte(0x21); // Dynamic table size update (size = 1). bytesIn.writeByte(0x00); // Literal indexed bytesIn.writeByte(0x0a); // Literal name (len = 10) bytesIn.writeUtf8("custom-key"); bytesIn.writeByte(0x0d); // Literal value (len = 13) bytesIn.writeUtf8("custom-header"); hpackReader.readHeaders(); assertEquals(0, hpackReader.headerCount); assertEquals(headerEntries("custom-key", "custom-header"), hpackReader.getAndResetHeaderList()); } /** Oldest entries are evicted to support newer ones. */ @Test public void writerEviction() throws IOException { List<Header> headerBlock = headerEntries( "custom-foo", "custom-header", "custom-bar", "custom-header", "custom-baz", "custom-header"); bytesIn.writeByte(0x40); // Literal indexed bytesIn.writeByte(0x0a); // Literal name (len = 10) bytesIn.writeUtf8("custom-foo"); bytesIn.writeByte(0x0d); // Literal value (len = 13) bytesIn.writeUtf8("custom-header"); bytesIn.writeByte(0x40); // Literal indexed bytesIn.writeByte(0x0a); // Literal name (len = 10) bytesIn.writeUtf8("custom-bar"); bytesIn.writeByte(0x0d); // Literal value (len = 13) bytesIn.writeUtf8("custom-header"); bytesIn.writeByte(0x40); // Literal indexed bytesIn.writeByte(0x0a); // Literal name (len = 10) bytesIn.writeUtf8("custom-baz"); bytesIn.writeByte(0x0d); // Literal value (len = 13) bytesIn.writeUtf8("custom-header"); // Set to only support 110 bytes (enough for 2 headers). // Use a new Writer because we don't support change the dynamic table // size after Writer constructed. Hpack.Writer writer = new Hpack.Writer(110, false, bytesOut); writer.writeHeaders(headerBlock); assertEquals(bytesIn, bytesOut); assertEquals(2, writer.headerCount); int tableLength = writer.dynamicTable.length; Header entry = writer.dynamicTable[tableLength - 1]; checkEntry(entry, "custom-bar", "custom-header", 55); entry = writer.dynamicTable[tableLength - 2]; checkEntry(entry, "custom-baz", "custom-header", 55); } @Test public void readerEviction() throws IOException { List<Header> headerBlock = headerEntries( "custom-foo", "custom-header", "custom-bar", "custom-header", "custom-baz", "custom-header"); // Set to only support 110 bytes (enough for 2 headers). bytesIn.writeByte(0x3F); // Dynamic table size update (size = 110). bytesIn.writeByte(0x4F); bytesIn.writeByte(0x40); // Literal indexed bytesIn.writeByte(0x0a); // Literal name (len = 10) bytesIn.writeUtf8("custom-foo"); bytesIn.writeByte(0x0d); // Literal value (len = 13) bytesIn.writeUtf8("custom-header"); bytesIn.writeByte(0x40); // Literal indexed bytesIn.writeByte(0x0a); // Literal name (len = 10) bytesIn.writeUtf8("custom-bar"); bytesIn.writeByte(0x0d); // Literal value (len = 13) bytesIn.writeUtf8("custom-header"); bytesIn.writeByte(0x40); // Literal indexed bytesIn.writeByte(0x0a); // Literal name (len = 10) bytesIn.writeUtf8("custom-baz"); bytesIn.writeByte(0x0d); // Literal value (len = 13) bytesIn.writeUtf8("custom-header"); hpackReader.readHeaders(); assertEquals(2, hpackReader.headerCount); Header entry1 = hpackReader.dynamicTable[readerHeaderTableLength() - 1]; checkEntry(entry1, "custom-bar", "custom-header", 55); Header entry2 = hpackReader.dynamicTable[readerHeaderTableLength() - 2]; checkEntry(entry2, "custom-baz", "custom-header", 55); // Once a header field is decoded and added to the reconstructed header // list, it cannot be removed from it. Hence, foo is here. assertEquals(headerBlock, hpackReader.getAndResetHeaderList()); // Simulate receiving a small dynamic table size update, that implies eviction. bytesIn.writeByte(0x3F); // Dynamic table size update (size = 55). bytesIn.writeByte(0x18); hpackReader.readHeaders(); assertEquals(1, hpackReader.headerCount); } /** Header table backing array is initially 8 long, let's ensure it grows. */ @Test public void dynamicallyGrowsBeyond64Entries() throws IOException { // Lots of headers need more room! hpackReader = new Hpack.Reader(16384, 4096, bytesIn); bytesIn.writeByte(0x3F); // Dynamic table size update (size = 16384). bytesIn.writeByte(0xE1); bytesIn.writeByte(0x7F); for (int i = 0; i < 256; i++) { bytesIn.writeByte(0x40); // Literal indexed bytesIn.writeByte(0x0a); // Literal name (len = 10) bytesIn.writeUtf8("custom-foo"); bytesIn.writeByte(0x0d); // Literal value (len = 13) bytesIn.writeUtf8("custom-header"); } hpackReader.readHeaders(); assertEquals(256, hpackReader.headerCount); } @Test public void huffmanDecodingSupported() throws IOException { bytesIn.writeByte(0x44); // == Literal indexed == // Indexed name (idx = 4) -> :path bytesIn.writeByte(0x8c); // Literal value Huffman encoded 12 bytes // decodes to www.example.com which is length 15 bytesIn.write(decodeHex("f1e3c2e5f23a6ba0ab90f4ff")); hpackReader.readHeaders(); assertEquals(1, hpackReader.headerCount); assertEquals(52, hpackReader.dynamicTableByteCount); Header entry = hpackReader.dynamicTable[readerHeaderTableLength() - 1]; checkEntry(entry, ":path", "www.example.com", 52); } /** * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C.2.1 */ @Test public void readLiteralHeaderFieldWithIndexing() throws IOException { bytesIn.writeByte(0x40); // Literal indexed bytesIn.writeByte(0x0a); // Literal name (len = 10) bytesIn.writeUtf8("custom-key"); bytesIn.writeByte(0x0d); // Literal value (len = 13) bytesIn.writeUtf8("custom-header"); hpackReader.readHeaders(); assertEquals(1, hpackReader.headerCount); assertEquals(55, hpackReader.dynamicTableByteCount); Header entry = hpackReader.dynamicTable[readerHeaderTableLength() - 1]; checkEntry(entry, "custom-key", "custom-header", 55); assertEquals(headerEntries("custom-key", "custom-header"), hpackReader.getAndResetHeaderList()); } /** * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C.2.2 */ @Test public void literalHeaderFieldWithoutIndexingIndexedName() throws IOException { List<Header> headerBlock = headerEntries(":path", "/sample/path"); bytesIn.writeByte(0x04); // == Literal not indexed == // Indexed name (idx = 4) -> :path bytesIn.writeByte(0x0c); // Literal value (len = 12) bytesIn.writeUtf8("/sample/path"); hpackWriter.writeHeaders(headerBlock); assertEquals(bytesIn, bytesOut); hpackReader.readHeaders(); assertEquals(0, hpackReader.headerCount); assertEquals(headerBlock, hpackReader.getAndResetHeaderList()); } @Test public void literalHeaderFieldWithoutIndexingNewName() throws IOException { List<Header> headerBlock = headerEntries("custom-key", "custom-header"); bytesIn.writeByte(0x00); // Not indexed bytesIn.writeByte(0x0a); // Literal name (len = 10) bytesIn.writeUtf8("custom-key"); bytesIn.writeByte(0x0d); // Literal value (len = 13) bytesIn.writeUtf8("custom-header"); hpackReader.readHeaders(); assertEquals(0, hpackReader.headerCount); assertEquals(headerBlock, hpackReader.getAndResetHeaderList()); } @Test public void literalHeaderFieldNeverIndexedIndexedName() throws IOException { bytesIn.writeByte(0x14); // == Literal never indexed == // Indexed name (idx = 4) -> :path bytesIn.writeByte(0x0c); // Literal value (len = 12) bytesIn.writeUtf8("/sample/path"); hpackReader.readHeaders(); assertEquals(0, hpackReader.headerCount); assertEquals(headerEntries(":path", "/sample/path"), hpackReader.getAndResetHeaderList()); } @Test public void literalHeaderFieldNeverIndexedNewName() throws IOException { List<Header> headerBlock = headerEntries("custom-key", "custom-header"); bytesIn.writeByte(0x10); // Never indexed bytesIn.writeByte(0x0a); // Literal name (len = 10) bytesIn.writeUtf8("custom-key"); bytesIn.writeByte(0x0d); // Literal value (len = 13) bytesIn.writeUtf8("custom-header"); hpackReader.readHeaders(); assertEquals(0, hpackReader.headerCount); assertEquals(headerBlock, hpackReader.getAndResetHeaderList()); } @Test public void literalHeaderFieldWithIncrementalIndexingIndexedName() throws IOException { List<Header> headerBlock = headerEntries(":path", "/sample/path"); bytesIn.writeByte(0x44); // Indexed name (idx = 4) -> :path bytesIn.writeByte(0x0c); // Literal value (len = 12) bytesIn.writeUtf8("/sample/path"); hpackReader.readHeaders(); assertEquals(1, hpackReader.headerCount); assertEquals(headerBlock, hpackReader.getAndResetHeaderList()); } @Test public void literalHeaderFieldWithIncrementalIndexingNewName() throws IOException { List<Header> headerBlock = headerEntries("custom-key", "custom-header"); bytesIn.writeByte(0x40); // Never indexed bytesIn.writeByte(0x0a); // Literal name (len = 10) bytesIn.writeUtf8("custom-key"); bytesIn.writeByte(0x0d); // Literal value (len = 13) bytesIn.writeUtf8("custom-header"); hpackWriter.writeHeaders(headerBlock); assertEquals(bytesIn, bytesOut); assertEquals(1, hpackWriter.headerCount); Header entry = hpackWriter.dynamicTable[hpackWriter.dynamicTable.length - 1]; checkEntry(entry, "custom-key", "custom-header", 55); hpackReader.readHeaders(); assertEquals(1, hpackReader.headerCount); assertEquals(headerBlock, hpackReader.getAndResetHeaderList()); } @Test public void theSameHeaderAfterOneIncrementalIndexed() throws IOException { List<Header> headerBlock = headerEntries( "custom-key", "custom-header", "custom-key", "custom-header"); bytesIn.writeByte(0x40); // Never indexed bytesIn.writeByte(0x0a); // Literal name (len = 10) bytesIn.writeUtf8("custom-key"); bytesIn.writeByte(0x0d); // Literal value (len = 13) bytesIn.writeUtf8("custom-header"); bytesIn.writeByte(0xbe); // Indexed name and value (idx = 63) hpackWriter.writeHeaders(headerBlock); assertEquals(bytesIn, bytesOut); assertEquals(1, hpackWriter.headerCount); Header entry = hpackWriter.dynamicTable[hpackWriter.dynamicTable.length - 1]; checkEntry(entry, "custom-key", "custom-header", 55); hpackReader.readHeaders(); assertEquals(1, hpackReader.headerCount); assertEquals(headerBlock, hpackReader.getAndResetHeaderList()); } @Test public void staticHeaderIsNotCopiedIntoTheIndexedTable() throws IOException { bytesIn.writeByte(0x82); // == Indexed - Add == // idx = 2 -> :method: GET hpackReader.readHeaders(); assertEquals(0, hpackReader.headerCount); assertEquals(0, hpackReader.dynamicTableByteCount); assertEquals(null, hpackReader.dynamicTable[readerHeaderTableLength() - 1]); assertEquals(headerEntries(":method", "GET"), hpackReader.getAndResetHeaderList()); } // Example taken from twitter/hpack DecoderTest.testUnusedIndex @Test public void readIndexedHeaderFieldIndex0() throws IOException { bytesIn.writeByte(0x80); // == Indexed - Add idx = 0 try { hpackReader.readHeaders(); fail(); } catch (IOException e) { assertEquals("index == 0", e.getMessage()); } } // Example taken from twitter/hpack DecoderTest.testIllegalIndex @Test public void readIndexedHeaderFieldTooLargeIndex() throws IOException { bytesIn.writeShort(0xff00); // == Indexed - Add idx = 127 try { hpackReader.readHeaders(); fail(); } catch (IOException e) { assertEquals("Header index too large 127", e.getMessage()); } } // Example taken from twitter/hpack DecoderTest.testInsidiousIndex @Test public void readIndexedHeaderFieldInsidiousIndex() throws IOException { bytesIn.writeByte(0xff); // == Indexed - Add == bytesIn.write(decodeHex("8080808008")); // idx = -2147483521 try { hpackReader.readHeaders(); fail(); } catch (IOException e) { assertEquals("Header index too large -2147483521", e.getMessage()); } } // Example taken from twitter/hpack DecoderTest.testHeaderTableSizeUpdate @Test public void minMaxHeaderTableSize() throws IOException { bytesIn.writeByte(0x20); hpackReader.readHeaders(); assertEquals(0, hpackReader.maxDynamicTableByteCount()); bytesIn.writeByte(0x3f); // encode size 4096 bytesIn.writeByte(0xe1); bytesIn.writeByte(0x1f); hpackReader.readHeaders(); assertEquals(4096, hpackReader.maxDynamicTableByteCount()); } // Example taken from twitter/hpack DecoderTest.testIllegalHeaderTableSizeUpdate @Test public void cannotSetTableSizeLargerThanSettingsValue() throws IOException { bytesIn.writeByte(0x3f); // encode size 4097 bytesIn.writeByte(0xe2); bytesIn.writeByte(0x1f); try { hpackReader.readHeaders(); fail(); } catch (IOException e) { assertEquals("Invalid dynamic table size update 4097", e.getMessage()); } } // Example taken from twitter/hpack DecoderTest.testInsidiousMaxHeaderSize @Test public void readHeaderTableStateChangeInsidiousMaxHeaderByteCount() throws IOException { bytesIn.writeByte(0x3f); bytesIn.write(decodeHex("e1ffffff07")); // count = -2147483648 try { hpackReader.readHeaders(); fail(); } catch (IOException e) { assertEquals("Invalid dynamic table size update -2147483648", e.getMessage()); } } /** * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C.2.4 */ @Test public void readIndexedHeaderFieldFromStaticTableWithoutBuffering() throws IOException { bytesIn.writeByte(0x20); // Dynamic table size update (size = 0). bytesIn.writeByte(0x82); // == Indexed - Add == // idx = 2 -> :method: GET hpackReader.readHeaders(); // Not buffered in header table. assertEquals(0, hpackReader.headerCount); assertEquals(headerEntries(":method", "GET"), hpackReader.getAndResetHeaderList()); } /** * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C.2 */ @Test public void readRequestExamplesWithoutHuffman() throws IOException { firstRequestWithoutHuffman(); hpackReader.readHeaders(); checkReadFirstRequestWithoutHuffman(); secondRequestWithoutHuffman(); hpackReader.readHeaders(); checkReadSecondRequestWithoutHuffman(); thirdRequestWithoutHuffman(); hpackReader.readHeaders(); checkReadThirdRequestWithoutHuffman(); } private void firstRequestWithoutHuffman() { bytesIn.writeByte(0x82); // == Indexed - Add == // idx = 2 -> :method: GET bytesIn.writeByte(0x86); // == Indexed - Add == // idx = 7 -> :scheme: http bytesIn.writeByte(0x84); // == Indexed - Add == // idx = 6 -> :path: / bytesIn.writeByte(0x41); // == Literal indexed == // Indexed name (idx = 4) -> :authority bytesIn.writeByte(0x0f); // Literal value (len = 15) bytesIn.writeUtf8("www.example.com"); } private void checkReadFirstRequestWithoutHuffman() { assertEquals(1, hpackReader.headerCount); // [ 1] (s = 57) :authority: www.example.com Header entry = hpackReader.dynamicTable[readerHeaderTableLength() - 1]; checkEntry(entry, ":authority", "www.example.com", 57); // Table size: 57 assertEquals(57, hpackReader.dynamicTableByteCount); // Decoded header list: assertEquals(headerEntries( ":method", "GET", ":scheme", "http", ":path", "/", ":authority", "www.example.com"), hpackReader.getAndResetHeaderList()); } private void secondRequestWithoutHuffman() { bytesIn.writeByte(0x82); // == Indexed - Add == // idx = 2 -> :method: GET bytesIn.writeByte(0x86); // == Indexed - Add == // idx = 7 -> :scheme: http bytesIn.writeByte(0x84); // == Indexed - Add == // idx = 6 -> :path: / bytesIn.writeByte(0xbe); // == Indexed - Add == // Indexed name (idx = 62) -> :authority: www.example.com bytesIn.writeByte(0x58); // == Literal indexed == // Indexed name (idx = 24) -> cache-control bytesIn.writeByte(0x08); // Literal value (len = 8) bytesIn.writeUtf8("no-cache"); } private void checkReadSecondRequestWithoutHuffman() { assertEquals(2, hpackReader.headerCount); // [ 1] (s = 53) cache-control: no-cache Header entry = hpackReader.dynamicTable[readerHeaderTableLength() - 2]; checkEntry(entry, "cache-control", "no-cache", 53); // [ 2] (s = 57) :authority: www.example.com entry = hpackReader.dynamicTable[readerHeaderTableLength() - 1]; checkEntry(entry, ":authority", "www.example.com", 57); // Table size: 110 assertEquals(110, hpackReader.dynamicTableByteCount); // Decoded header list: assertEquals(headerEntries( ":method", "GET", ":scheme", "http", ":path", "/", ":authority", "www.example.com", "cache-control", "no-cache"), hpackReader.getAndResetHeaderList()); } private void thirdRequestWithoutHuffman() { bytesIn.writeByte(0x82); // == Indexed - Add == // idx = 2 -> :method: GET bytesIn.writeByte(0x87); // == Indexed - Add == // idx = 7 -> :scheme: http bytesIn.writeByte(0x85); // == Indexed - Add == // idx = 5 -> :path: /index.html bytesIn.writeByte(0xbf); // == Indexed - Add == // Indexed name (idx = 63) -> :authority: www.example.com bytesIn.writeByte(0x40); // Literal indexed bytesIn.writeByte(0x0a); // Literal name (len = 10) bytesIn.writeUtf8("custom-key"); bytesIn.writeByte(0x0c); // Literal value (len = 12) bytesIn.writeUtf8("custom-value"); } private void checkReadThirdRequestWithoutHuffman() { assertEquals(3, hpackReader.headerCount); // [ 1] (s = 54) custom-key: custom-value Header entry = hpackReader.dynamicTable[readerHeaderTableLength() - 3]; checkEntry(entry, "custom-key", "custom-value", 54); // [ 2] (s = 53) cache-control: no-cache entry = hpackReader.dynamicTable[readerHeaderTableLength() - 2]; checkEntry(entry, "cache-control", "no-cache", 53); // [ 3] (s = 57) :authority: www.example.com entry = hpackReader.dynamicTable[readerHeaderTableLength() - 1]; checkEntry(entry, ":authority", "www.example.com", 57); // Table size: 164 assertEquals(164, hpackReader.dynamicTableByteCount); // Decoded header list: assertEquals(headerEntries( ":method", "GET", ":scheme", "https", ":path", "/index.html", ":authority", "www.example.com", "custom-key", "custom-value"), hpackReader.getAndResetHeaderList()); } /** * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C.4 */ @Test public void readRequestExamplesWithHuffman() throws IOException { firstRequestWithHuffman(); hpackReader.readHeaders(); checkReadFirstRequestWithHuffman(); secondRequestWithHuffman(); hpackReader.readHeaders(); checkReadSecondRequestWithHuffman(); thirdRequestWithHuffman(); hpackReader.readHeaders(); checkReadThirdRequestWithHuffman(); } private void firstRequestWithHuffman() { bytesIn.writeByte(0x82); // == Indexed - Add == // idx = 2 -> :method: GET bytesIn.writeByte(0x86); // == Indexed - Add == // idx = 6 -> :scheme: http bytesIn.writeByte(0x84); // == Indexed - Add == // idx = 4 -> :path: / bytesIn.writeByte(0x41); // == Literal indexed == // Indexed name (idx = 1) -> :authority bytesIn.writeByte(0x8c); // Literal value Huffman encoded 12 bytes // decodes to www.example.com which is length 15 bytesIn.write(decodeHex("f1e3c2e5f23a6ba0ab90f4ff")); } private void checkReadFirstRequestWithHuffman() { assertEquals(1, hpackReader.headerCount); // [ 1] (s = 57) :authority: www.example.com Header entry = hpackReader.dynamicTable[readerHeaderTableLength() - 1]; checkEntry(entry, ":authority", "www.example.com", 57); // Table size: 57 assertEquals(57, hpackReader.dynamicTableByteCount); // Decoded header list: assertEquals(headerEntries( ":method", "GET", ":scheme", "http", ":path", "/", ":authority", "www.example.com"), hpackReader.getAndResetHeaderList()); } private void secondRequestWithHuffman() { bytesIn.writeByte(0x82); // == Indexed - Add == // idx = 2 -> :method: GET bytesIn.writeByte(0x86); // == Indexed - Add == // idx = 6 -> :scheme: http bytesIn.writeByte(0x84); // == Indexed - Add == // idx = 4 -> :path: / bytesIn.writeByte(0xbe); // == Indexed - Add == // idx = 62 -> :authority: www.example.com bytesIn.writeByte(0x58); // == Literal indexed == // Indexed name (idx = 24) -> cache-control bytesIn.writeByte(0x86); // Literal value Huffman encoded 6 bytes // decodes to no-cache which is length 8 bytesIn.write(decodeHex("a8eb10649cbf")); } private void checkReadSecondRequestWithHuffman() { assertEquals(2, hpackReader.headerCount); // [ 1] (s = 53) cache-control: no-cache Header entry = hpackReader.dynamicTable[readerHeaderTableLength() - 2]; checkEntry(entry, "cache-control", "no-cache", 53); // [ 2] (s = 57) :authority: www.example.com entry = hpackReader.dynamicTable[readerHeaderTableLength() - 1]; checkEntry(entry, ":authority", "www.example.com", 57); // Table size: 110 assertEquals(110, hpackReader.dynamicTableByteCount); // Decoded header list: assertEquals(headerEntries( ":method", "GET", ":scheme", "http", ":path", "/", ":authority", "www.example.com", "cache-control", "no-cache"), hpackReader.getAndResetHeaderList()); } private void thirdRequestWithHuffman() { bytesIn.writeByte(0x82); // == Indexed - Add == // idx = 2 -> :method: GET bytesIn.writeByte(0x87); // == Indexed - Add == // idx = 7 -> :scheme: https bytesIn.writeByte(0x85); // == Indexed - Add == // idx = 5 -> :path: /index.html bytesIn.writeByte(0xbf); // == Indexed - Add == // idx = 63 -> :authority: www.example.com bytesIn.writeByte(0x40); // Literal indexed bytesIn.writeByte(0x88); // Literal name Huffman encoded 8 bytes // decodes to custom-key which is length 10 bytesIn.write(decodeHex("25a849e95ba97d7f")); bytesIn.writeByte(0x89); // Literal value Huffman encoded 9 bytes // decodes to custom-value which is length 12 bytesIn.write(decodeHex("25a849e95bb8e8b4bf")); } private void checkReadThirdRequestWithHuffman() { assertEquals(3, hpackReader.headerCount); // [ 1] (s = 54) custom-key: custom-value Header entry = hpackReader.dynamicTable[readerHeaderTableLength() - 3]; checkEntry(entry, "custom-key", "custom-value", 54); // [ 2] (s = 53) cache-control: no-cache entry = hpackReader.dynamicTable[readerHeaderTableLength() - 2]; checkEntry(entry, "cache-control", "no-cache", 53); // [ 3] (s = 57) :authority: www.example.com entry = hpackReader.dynamicTable[readerHeaderTableLength() - 1]; checkEntry(entry, ":authority", "www.example.com", 57); // Table size: 164 assertEquals(164, hpackReader.dynamicTableByteCount); // Decoded header list: assertEquals(headerEntries( ":method", "GET", ":scheme", "https", ":path", "/index.html", ":authority", "www.example.com", "custom-key", "custom-value"), hpackReader.getAndResetHeaderList()); } @Test public void readSingleByteInt() throws IOException { assertEquals(10, newReader(byteStream()).readInt(10, 31)); assertEquals(10, newReader(byteStream()).readInt(0xe0 | 10, 31)); } @Test public void readMultibyteInt() throws IOException { assertEquals(1337, newReader(byteStream(154, 10)).readInt(31, 31)); } @Test public void writeSingleByteInt() throws IOException { hpackWriter.writeInt(10, 31, 0); assertBytes(10); hpackWriter.writeInt(10, 31, 0xe0); assertBytes(0xe0 | 10); } @Test public void writeMultibyteInt() throws IOException { hpackWriter.writeInt(1337, 31, 0); assertBytes(31, 154, 10); hpackWriter.writeInt(1337, 31, 0xe0); assertBytes(0xe0 | 31, 154, 10); } @Test public void max31BitValue() throws IOException { hpackWriter.writeInt(0x7fffffff, 31, 0); assertBytes(31, 224, 255, 255, 255, 7); assertEquals(0x7fffffff, newReader(byteStream(224, 255, 255, 255, 7)).readInt(31, 31)); } @Test public void prefixMask() throws IOException { hpackWriter.writeInt(31, 31, 0); assertBytes(31, 0); assertEquals(31, newReader(byteStream(0)).readInt(31, 31)); } @Test public void prefixMaskMinusOne() throws IOException { hpackWriter.writeInt(30, 31, 0); assertBytes(30); assertEquals(31, newReader(byteStream(0)).readInt(31, 31)); } @Test public void zero() throws IOException { hpackWriter.writeInt(0, 31, 0); assertBytes(0); assertEquals(0, newReader(byteStream()).readInt(0, 31)); } @Test public void lowercaseHeaderNameBeforeEmit() throws IOException { hpackWriter.writeHeaders(Arrays.asList(new Header("FoO", "BaR"))); assertBytes(0x40, 3, 'f', 'o', 'o', 3, 'B', 'a', 'R'); } @Test public void mixedCaseHeaderNameIsMalformed() throws IOException { try { newReader(byteStream(0, 3, 'F', 'o', 'o', 3, 'B', 'a', 'R')).readHeaders(); fail(); } catch (IOException e) { assertEquals("PROTOCOL_ERROR response malformed: mixed case name: Foo", e.getMessage()); } } @Test public void emptyHeaderName() throws IOException { hpackWriter.writeByteString(ByteString.encodeUtf8("")); assertBytes(0); assertEquals(ByteString.EMPTY, newReader(byteStream(0)).readByteString()); } @Test public void emitsDynamicTableSizeUpdate() throws IOException { hpackWriter.setHeaderTableSizeSetting(2048); hpackWriter.writeHeaders(Arrays.asList(new Header("foo", "bar"))); assertBytes( 0x3F, 0xE1, 0xF, // Dynamic table size update (size = 2048). 0x40, 3, 'f', 'o', 'o', 3, 'b', 'a', 'r'); hpackWriter.setHeaderTableSizeSetting(8192); hpackWriter.writeHeaders(Arrays.asList(new Header("bar", "foo"))); assertBytes( 0x3F, 0xE1, 0x3F, // Dynamic table size update (size = 8192). 0x40, 3, 'b', 'a', 'r', 3, 'f', 'o', 'o'); // No more dynamic table updates should be emitted. hpackWriter.writeHeaders(Arrays.asList(new Header("far", "boo"))); assertBytes(0x40, 3, 'f', 'a', 'r', 3, 'b', 'o', 'o'); } @Test public void noDynamicTableSizeUpdateWhenSizeIsEqual() throws IOException { int currentSize = hpackWriter.headerTableSizeSetting; hpackWriter.setHeaderTableSizeSetting(currentSize); hpackWriter.writeHeaders(Arrays.asList(new Header("foo", "bar"))); assertBytes(0x40, 3, 'f', 'o', 'o', 3, 'b', 'a', 'r'); } @Test public void growDynamicTableSize() throws IOException { hpackWriter.setHeaderTableSizeSetting(8192); hpackWriter.setHeaderTableSizeSetting(16384); hpackWriter.writeHeaders(Arrays.asList(new Header("foo", "bar"))); assertBytes( 0x3F, 0xE1, 0x7F, // Dynamic table size update (size = 16384). 0x40, 3, 'f', 'o', 'o', 3, 'b', 'a', 'r'); } @Test public void shrinkDynamicTableSize() throws IOException { hpackWriter.setHeaderTableSizeSetting(2048); hpackWriter.setHeaderTableSizeSetting(0); hpackWriter.writeHeaders(Arrays.asList(new Header("foo", "bar"))); assertBytes( 0x20, // Dynamic size update (size = 0). 0x40, 3, 'f', 'o', 'o', 3, 'b', 'a', 'r'); } @Test public void manyDynamicTableSizeChanges() throws IOException { hpackWriter.setHeaderTableSizeSetting(16384); hpackWriter.setHeaderTableSizeSetting(8096); hpackWriter.setHeaderTableSizeSetting(0); hpackWriter.setHeaderTableSizeSetting(4096); hpackWriter.setHeaderTableSizeSetting(2048); hpackWriter.writeHeaders(Arrays.asList(new Header("foo", "bar"))); assertBytes( 0x20, // Dynamic size update (size = 0). 0x3F, 0xE1, 0xF, // Dynamic size update (size = 2048). 0x40, 3, 'f', 'o', 'o', 3, 'b', 'a', 'r'); } @Test public void dynamicTableEvictionWhenSizeLowered() throws IOException { List<Header> headerBlock = headerEntries( "custom-key1", "custom-header", "custom-key2", "custom-header"); hpackWriter.writeHeaders(headerBlock); assertEquals(2, hpackWriter.headerCount); hpackWriter.setHeaderTableSizeSetting(56); assertEquals(1, hpackWriter.headerCount); hpackWriter.setHeaderTableSizeSetting(0); assertEquals(0, hpackWriter.headerCount); } @Test public void noEvictionOnDynamicTableSizeIncrease() throws IOException { List<Header> headerBlock = headerEntries( "custom-key1", "custom-header", "custom-key2", "custom-header"); hpackWriter.writeHeaders(headerBlock); assertEquals(2, hpackWriter.headerCount); hpackWriter.setHeaderTableSizeSetting(8192); assertEquals(2, hpackWriter.headerCount); } @Test public void dynamicTableSizeHasAnUpperBound() { hpackWriter.setHeaderTableSizeSetting(1048576); assertEquals(16384, hpackWriter.maxDynamicTableByteCount); } @Test public void huffmanEncode() throws IOException { hpackWriter = new Hpack.Writer(4096, true, bytesOut); hpackWriter.writeHeaders(headerEntries("foo", "bar")); ByteString expected = new Buffer() .writeByte(0x40) // Literal header, new name. .writeByte(0x82) // String literal is Huffman encoded (len = 2). .writeByte(0x94) // 'foo' Huffman encoded. .writeByte(0xE7) .writeByte(3) // String literal not Huffman encoded (len = 3). .writeByte('b') .writeByte('a') .writeByte('r') .readByteString(); ByteString actual = bytesOut.readByteString(); assertEquals(expected, actual); } @Test public void staticTableIndexedHeaders() throws IOException { hpackWriter.writeHeaders(headerEntries(":method", "GET")); assertBytes(0x82); assertEquals(0, hpackWriter.headerCount); hpackWriter.writeHeaders(headerEntries(":method", "POST")); assertBytes(0x83); assertEquals(0, hpackWriter.headerCount); hpackWriter.writeHeaders(headerEntries(":path", "/")); assertBytes(0x84); assertEquals(0, hpackWriter.headerCount); hpackWriter.writeHeaders(headerEntries(":path", "/index.html")); assertBytes(0x85); assertEquals(0, hpackWriter.headerCount); hpackWriter.writeHeaders(headerEntries(":scheme", "http")); assertBytes(0x86); assertEquals(0, hpackWriter.headerCount); hpackWriter.writeHeaders(headerEntries(":scheme", "https")); assertBytes(0x87); assertEquals(0, hpackWriter.headerCount); } @Test public void dynamicTableIndexedHeader() throws IOException { hpackWriter.writeHeaders(headerEntries("custom-key", "custom-header")); assertBytes(0x40, 10, 'c', 'u', 's', 't', 'o', 'm', '-', 'k', 'e', 'y', 13, 'c', 'u', 's', 't', 'o', 'm', '-', 'h', 'e', 'a', 'd', 'e', 'r'); assertEquals(1, hpackWriter.headerCount); hpackWriter.writeHeaders(headerEntries("custom-key", "custom-header")); assertBytes(0xbe); assertEquals(1, hpackWriter.headerCount); } @Test public void doNotIndexPseudoHeaders() throws IOException { hpackWriter.writeHeaders(headerEntries(":method", "PUT")); assertBytes(0x02, 3, 'P', 'U', 'T'); assertEquals(0, hpackWriter.headerCount); hpackWriter.writeHeaders(headerEntries(":path", "/okhttp")); assertBytes(0x04, 7, '/', 'o', 'k', 'h', 't', 't', 'p'); assertEquals(0, hpackWriter.headerCount); } @Test public void incrementalIndexingWithAuthorityPseudoHeader() throws IOException { hpackWriter.writeHeaders(headerEntries(":authority", "foo.com")); assertBytes(0x41, 7, 'f', 'o', 'o', '.', 'c', 'o', 'm'); assertEquals(1, hpackWriter.headerCount); hpackWriter.writeHeaders(headerEntries(":authority", "foo.com")); assertBytes(0xbe); assertEquals(1, hpackWriter.headerCount); // If the :authority header somehow changes, it should be re-added to the dynamic table. hpackWriter.writeHeaders(headerEntries(":authority", "bar.com")); assertBytes(0x41, 7, 'b', 'a', 'r', '.', 'c', 'o', 'm'); assertEquals(2, hpackWriter.headerCount); hpackWriter.writeHeaders(headerEntries(":authority", "bar.com")); assertBytes(0xbe); assertEquals(2, hpackWriter.headerCount); } @Test public void incrementalIndexingWithStaticTableIndexedName() throws IOException { hpackWriter.writeHeaders(headerEntries("accept-encoding", "gzip")); assertBytes(0x50, 4, 'g', 'z', 'i', 'p'); assertEquals(1, hpackWriter.headerCount); hpackWriter.writeHeaders(headerEntries("accept-encoding", "gzip")); assertBytes(0xbe); assertEquals(1, hpackWriter.headerCount); } @Test public void incrementalIndexingWithDynamcTableIndexedName() throws IOException { hpackWriter.writeHeaders(headerEntries("foo", "bar")); assertBytes(0x40, 3, 'f', 'o', 'o', 3, 'b', 'a', 'r'); assertEquals(1, hpackWriter.headerCount); hpackWriter.writeHeaders(headerEntries("foo", "bar1")); assertBytes(0x7e, 4, 'b', 'a', 'r', '1'); assertEquals(2, hpackWriter.headerCount); hpackWriter.writeHeaders(headerEntries("foo", "bar1")); assertBytes(0xbe); assertEquals(2, hpackWriter.headerCount); } private Hpack.Reader newReader(Buffer source) { return new Hpack.Reader(4096, source); } private Buffer byteStream(int... bytes) { return new Buffer().write(intArrayToByteArray(bytes)); } private void checkEntry(Header entry, String name, String value, int size) { assertEquals(name, entry.name.utf8()); assertEquals(value, entry.value.utf8()); assertEquals(size, entry.hpackSize); } private void assertBytes(int... bytes) throws IOException { ByteString expected = intArrayToByteArray(bytes); ByteString actual = bytesOut.readByteString(); assertEquals(expected, actual); } private ByteString intArrayToByteArray(int[] bytes) { byte[] data = new byte[bytes.length]; for (int i = 0; i < bytes.length; i++) { data[i] = (byte) bytes[i]; } return ByteString.of(data); } private int readerHeaderTableLength() { return hpackReader.dynamicTable.length; } }