diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java index 7f2e6235f..9bdcd7bbc 100644 --- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java +++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java @@ -70,8 +70,7 @@ import static com.squareup.okhttp.mockwebserver.SocketPolicy.FAIL_HANDSHAKE; */ public final class MockWebServer { private static final byte[] NPN_PROTOCOLS = { - // TODO: support HTTP/2.0. - // 17, 'H', 'T', 'T', 'P', '-', 'd', 'r', 'a', 'f', 't', '-', '0', '6', '/', '2', '.', '0', + 17, 'H', 'T', 'T', 'P', '-', 'd', 'r', 'a', 'f', 't', '-', '0', '6', '/', '2', '.', '0', 6, 's', 'p', 'd', 'y', '/', '3', 8, 'h', 't', 't', 'p', '/', '1', '.', '1' }; diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Hpack.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Hpack.java index e4a06226f..c3ca8f116 100644 --- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Hpack.java +++ b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Hpack.java @@ -13,77 +13,97 @@ import java.util.List; * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03 */ final class Hpack { + + static class HeaderEntry { + private final String name; + private final String value; + + HeaderEntry(String name, String value) { + this.name = name; + this.value = value; + } + + // TODO: This needs to be the length in UTF-8 bytes, not the length in chars. + int length() { + return 32 + name.length() + value.length(); + } + } + static final int PREFIX_5_BITS = 0x1f; static final int PREFIX_6_BITS = 0x3f; static final int PREFIX_7_BITS = 0x7f; static final int PREFIX_8_BITS = 0xff; - static final List INITIAL_CLIENT_TO_SERVER_HEADER_TABLE = Arrays.asList( - ":scheme", "http", - ":scheme", "https", - ":host", "", - ":path", "/", - ":method", "GET", - "accept", "", - "accept-charset", "", - "accept-encoding", "", - "accept-language", "", - "cookie", "", - "if-modified-since", "", - "user-agent", "", - "referer", "", - "authorization", "", - "allow", "", - "cache-control", "", - "connection", "", - "content-length", "", - "content-type", "", - "date", "", - "expect", "", - "from", "", - "if-match", "", - "if-none-match", "", - "if-range", "", - "if-unmodified-since", "", - "max-forwards", "", - "proxy-authorization", "", - "range", "", - "via", "" + static final List INITIAL_CLIENT_TO_SERVER_HEADER_TABLE = Arrays.asList( + new HeaderEntry(":scheme", "http"), + new HeaderEntry(":scheme", "https"), + new HeaderEntry(":host", ""), + new HeaderEntry(":path", "/"), + new HeaderEntry(":method", "GET"), + new HeaderEntry("accept", ""), + new HeaderEntry("accept-charset", ""), + new HeaderEntry("accept-encoding", ""), + new HeaderEntry("accept-language", ""), + new HeaderEntry("cookie", ""), + new HeaderEntry("if-modified-since", ""), + new HeaderEntry("user-agent", ""), + new HeaderEntry("referer", ""), + new HeaderEntry("authorization", ""), + new HeaderEntry("allow", ""), + new HeaderEntry("cache-control", ""), + new HeaderEntry("connection", ""), + new HeaderEntry("content-length", ""), + new HeaderEntry("content-type", ""), + new HeaderEntry("date", ""), + new HeaderEntry("expect", ""), + new HeaderEntry("from", ""), + new HeaderEntry("if-match", ""), + new HeaderEntry("if-none-match", ""), + new HeaderEntry("if-range", ""), + new HeaderEntry("if-unmodified-since", ""), + new HeaderEntry("max-forwards", ""), + new HeaderEntry("proxy-authorization", ""), + new HeaderEntry("range", ""), + new HeaderEntry("via", "") ); - static final List INITIAL_SERVER_TO_CLIENT_HEADER_TABLE = Arrays.asList( - ":status", "200", - "age", "", - "cache-control", "", - "content-length", "", - "content-type", "", - "date", "", - "etag", "", - "expires", "", - "last-modified", "", - "server", "", - "set-cookie", "", - "vary", "", - "via", "", - "access-control-allow-origin", "", - "accept-ranges", "", - "allow", "", - "connection", "", - "content-disposition", "", - "content-encoding", "", - "content-language", "", - "content-location", "", - "content-range", "", - "link", "", - "location", "", - "proxy-authenticate", "", - "refresh", "", - "retry-after", "", - "strict-transport-security", "", - "transfer-encoding", "", - "www-authenticate", "" + static final List INITIAL_SERVER_TO_CLIENT_HEADER_TABLE = Arrays.asList( + new HeaderEntry(":status", "200"), + new HeaderEntry("age", ""), + new HeaderEntry("cache-control", ""), + new HeaderEntry("content-length", ""), + new HeaderEntry("content-type", ""), + new HeaderEntry("date", ""), + new HeaderEntry("etag", ""), + new HeaderEntry("expires", ""), + new HeaderEntry("last-modified", ""), + new HeaderEntry("server", ""), + new HeaderEntry("set-cookie", ""), + new HeaderEntry("vary", ""), + new HeaderEntry("via", ""), + new HeaderEntry("access-control-allow-origin", ""), + new HeaderEntry("accept-ranges", ""), + new HeaderEntry("allow", ""), + new HeaderEntry("connection", ""), + new HeaderEntry("content-disposition", ""), + new HeaderEntry("content-encoding", ""), + new HeaderEntry("content-language", ""), + new HeaderEntry("content-location", ""), + new HeaderEntry("content-range", ""), + new HeaderEntry("link", ""), + new HeaderEntry("location", ""), + new HeaderEntry("proxy-authenticate", ""), + new HeaderEntry("refresh", ""), + new HeaderEntry("retry-after", ""), + new HeaderEntry("strict-transport-security", ""), + new HeaderEntry("transfer-encoding", ""), + new HeaderEntry("www-authenticate", "") ); + // Update these when initial tables change to sum of each entry length. + static final int INITIAL_CLIENT_TO_SERVER_HEADER_TABLE_LENGTH = 1262; + static final int INITIAL_SERVER_TO_CLIENT_HEADER_TABLE_LENGTH = 1304; + private Hpack() { } @@ -92,16 +112,20 @@ final class Hpack { private final DataInputStream in; private final BitSet referenceSet = new BitSet(); - private final List headerTable; + private final List headerTable; private final List emittedHeaders = new ArrayList(); - private long bufferSize = 4096; + private long bufferSize = 0; private long bytesLeft = 0; Reader(DataInputStream in, boolean client) { this.in = in; - this.headerTable = new ArrayList(client - ? INITIAL_CLIENT_TO_SERVER_HEADER_TABLE - : INITIAL_SERVER_TO_CLIENT_HEADER_TABLE); + if (client) { // we are reading from the server + this.headerTable = new ArrayList(INITIAL_SERVER_TO_CLIENT_HEADER_TABLE); + this.bufferSize = INITIAL_SERVER_TO_CLIENT_HEADER_TABLE_LENGTH; + } else { + this.headerTable = new ArrayList(INITIAL_CLIENT_TO_SERVER_HEADER_TABLE); + this.bufferSize = INITIAL_CLIENT_TO_SERVER_HEADER_TABLE_LENGTH; + } } /** @@ -161,8 +185,6 @@ final class Hpack { referenceSet.clear(index); } else { referenceSet.set(index); - emittedHeaders.add(getName(index)); - emittedHeaders.add(getValue(index)); } } @@ -184,23 +206,17 @@ final class Hpack { private void readLiteralHeaderWithIncrementalIndexingIndexedName(int nameIndex) throws IOException { - int index = headerTable.size(); String name = getName(nameIndex); String value = readString(); - appendToHeaderTable(name, value); - emittedHeaders.add(name); - emittedHeaders.add(value); - referenceSet.set(index); + int index = headerTable.size(); // append to tail + insertIntoHeaderTable(index, new HeaderEntry(name, value)); } private void readLiteralHeaderWithIncrementalIndexingNewName() throws IOException { - int index = headerTable.size(); String name = readString(); String value = readString(); - appendToHeaderTable(name, value); - emittedHeaders.add(name); - emittedHeaders.add(value); - referenceSet.set(index); + int index = headerTable.size(); // append to tail + insertIntoHeaderTable(index, new HeaderEntry(name, value)); } private void readLiteralHeaderWithSubstitutionIndexingIndexedName(int nameIndex) @@ -208,43 +224,39 @@ final class Hpack { int index = readInt(readByte(), PREFIX_8_BITS); String name = getName(nameIndex); String value = readString(); - replaceInHeaderTable(index, name, value); - emittedHeaders.add(name); - emittedHeaders.add(value); - referenceSet.set(index); + insertIntoHeaderTable(index, new HeaderEntry(name, value)); } private void readLiteralHeaderWithSubstitutionIndexingNewName() throws IOException { String name = readString(); int index = readInt(readByte(), PREFIX_8_BITS); String value = readString(); - replaceInHeaderTable(index, name, value); - emittedHeaders.add(name); - emittedHeaders.add(value); - referenceSet.set(index); + insertIntoHeaderTable(index, new HeaderEntry(name, value)); } private String getName(int index) { - return headerTable.get(index * 2); + return headerTable.get(index).name; } private String getValue(int index) { - return headerTable.get(index * 2 + 1); + return headerTable.get(index).value; } - private void appendToHeaderTable(String name, String value) { - insertIntoHeaderTable(headerTable.size() * 2, name, value); - } + private void insertIntoHeaderTable(int index, HeaderEntry entry) { + int delta = entry.length(); + if (index != headerTable.size()) { + delta -= headerTable.get(index).length(); + } - private void replaceInHeaderTable(int index, String name, String value) { - remove(index); - insertIntoHeaderTable(index, name, value); - } - - private void insertIntoHeaderTable(int index, String name, String value) { - // TODO: This needs to be the length in UTF-8 bytes, not the length in chars. - - int delta = 32 + name.length() + value.length(); + // if the new or replacement header is too big, drop all entries. + if (delta > maxBufferSize) { + headerTable.clear(); + bufferSize = 0; + // emit the large header to the callback. + emittedHeaders.add(entry.name); + emittedHeaders.add(entry.value); + return; + } // Prune headers to the required length. while (bufferSize + delta > maxBufferSize) { @@ -252,22 +264,21 @@ final class Hpack { index--; } - if (delta > maxBufferSize) { - return; // New values won't fit in the buffer; skip 'em. + if (index < 0) { // we pruned it, so insert at beginning + index = 0; + headerTable.add(index, entry); + } else if (index == headerTable.size()) { // append to the end + headerTable.add(index, entry); + } else { // replace value at same position + headerTable.set(index, entry); } - if (index == 0) index = 0; - - headerTable.add(index * 2, name); - headerTable.add(index * 2 + 1, value); bufferSize += delta; + referenceSet.set(index); } private void remove(int index) { - String name = headerTable.remove(index * 2); - String value = headerTable.remove(index * 2); // No +1 because it's shifted by remove() above. - // TODO: This needs to be the length in UTF-8 bytes, not the length in chars. - bufferSize -= (32 + name.length() + value.length()); + bufferSize -= headerTable.remove(index).length(); } private int readByte() throws IOException { diff --git a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft06Test.java b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft06Test.java index 135bd3d14..c0e0fe65a 100644 --- a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft06Test.java +++ b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft06Test.java @@ -28,9 +28,9 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; public class Http20Draft06Test { + static final int expectedStreamId = 15; @Test public void onlyOneLiteralHeadersFrame() throws IOException { - final int expectedStreamId = 15; final List sentHeaders = Arrays.asList("name", "value"); ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -67,7 +67,6 @@ public class Http20Draft06Test { } @Test public void headersFrameThenContinuation() throws IOException { - final int expectedStreamId = 15; ByteArrayOutputStream out = new ByteArrayOutputStream(); DataOutputStream dataOut = new DataOutputStream(out); @@ -112,6 +111,122 @@ public class Http20Draft06Test { }); } + /** + * 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 { + char[] tooLarge = new char[4096]; + Arrays.fill(tooLarge, 'a'); + final List sentHeaders = Arrays.asList("foo", new String(tooLarge)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DataOutputStream dataOut = new DataOutputStream(out); + + writeOnlyHeadersFrame(literalHeaders(sentHeaders), dataOut); + + FrameReader fr = new Http20Draft06.Reader(new ByteArrayInputStream(out.toByteArray()), false); + + // Consume the large header set. + fr.nextFrame(new BaseTestHandler() { + + @Override + public void headers(boolean outFinished, boolean inFinished, int streamId, + int associatedStreamId, int priority, List nameValueBlock, + HeadersMode headersMode) { + assertEquals(sentHeaders, nameValueBlock); + } + }); + } + + @Test public void usingDraft06Examples() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DataOutputStream dataOut = new DataOutputStream(out); + + writeOnlyHeadersFrame(firstHeaderSetBytes(), dataOut); + writeOnlyHeadersFrame(secondHeaderSetBytes(), dataOut); + + FrameReader fr = new Http20Draft06.Reader(new ByteArrayInputStream(out.toByteArray()), false); + + // Consume the first header set. + fr.nextFrame(new BaseTestHandler() { + + @Override + public void headers(boolean outFinished, boolean inFinished, int streamId, + int associatedStreamId, int priority, List nameValueBlock, + HeadersMode headersMode) { + assertEquals(Arrays.asList(":path", "/my-example/index.html", "user-agent", "my-user-agent", + "mynewheader", "first"), nameValueBlock); + } + }); + + // Consume the second header set. + fr.nextFrame(new BaseTestHandler() { + + @Override + public void headers(boolean outFinished, boolean inFinished, int streamId, + int associatedStreamId, int priority, List nameValueBlock, + HeadersMode headersMode) { + assertEquals(Arrays.asList( + ":path", "/my-example/resources/script.js", + "user-agent", "my-user-agent", + "mynewheader", "second" + ), nameValueBlock); + } + }); + } + + // Deviates from draft only to fix doc bugs noted in https://github.com/igrigorik/http-2 specs. + // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#appendix-C.1 + static byte[] firstHeaderSetBytes() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + out.write(0x44); // literal header with incremental indexing, name index = 3 + out.write(0x16); // header value string length = 22 + out.write("/my-example/index.html".getBytes(), 0, 22); + + out.write(0x4C); // literal header with incremental indexing, name index = 11 + out.write(0x0D); // header value string length = 13 + out.write("my-user-agent".getBytes(), 0, 13); + + out.write(0x40); // literal header with incremental indexing, new name + out.write(0x0B); // header name string length = 11 + out.write("mynewheader".getBytes(), 0, 11); + out.write(0x05); // header value string length = 5 + out.write("first".getBytes(), 0, 5); + + return out.toByteArray(); + } + + // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#appendix-C.2 + static byte[] secondHeaderSetBytes() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + out.write(0x9e); // indexed header, index = 30: removal from reference set + out.write(0xa0); // indexed header, index = 32: removal from reference set + out.write(0x04); // literal header, substitution indexing, name index = 3 + + out.write(0x1e); // replaced entry index = 30 + out.write(0x1f); // header value string length = 31 + out.write("/my-example/resources/script.js".getBytes(), 0, 31); + + out.write(0x5f); + out.write(0x02); // literal header, incremental indexing, name index = 32 + out.write(0x06); // header value string length = 6 + out.write("second".getBytes(), 0, 6); + + return out.toByteArray(); + } + + static void writeOnlyHeadersFrame(byte[] headersSet, DataOutputStream dataOut) + throws IOException { + dataOut.writeShort(headersSet.length); + dataOut.write(Http20Draft06.TYPE_HEADERS); + dataOut.write(Http20Draft06.FLAG_END_HEADERS | Http20Draft06.FLAG_END_STREAM); + dataOut.writeInt(expectedStreamId & 0x7fffffff); // stream 15 with reserved bit set + dataOut.write(headersSet); + } + static byte[] literalHeaders(List sentHeaders) throws IOException { ByteArrayOutputStream headerBytes = new ByteArrayOutputStream(); new Hpack.Writer(new DataOutputStream(headerBytes)).writeHeaders(sentHeaders);