From b03026067b3686e45d4e11886eaccf084c36b01e Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Tue, 15 Apr 2014 11:47:04 -0400 Subject: [PATCH] Update to HTTP/2 Draft 10 and HPACK Draft 6. --- ...st.java => HttpOverHttp20Draft10Test.java} | 4 +- ...Draft05Test.java => HpackDraft06Test.java} | 92 +- .../internal/spdy/Http20Draft09Test.java | 515 --------- .../internal/spdy/Http20Draft10Test.java | 1008 +++++++++++++++++ .../okhttp/internal/spdy/HuffmanTest.java | 11 +- .../okhttp/internal/spdy/MockSpdyPeer.java | 2 +- .../okhttp/internal/spdy/SettingsTest.java | 4 +- .../internal/spdy/SpdyConnectionTest.java | 4 +- .../com/squareup/okhttp/OkHttpClient.java | 2 +- .../java/com/squareup/okhttp/Protocol.java | 2 +- .../okhttp/internal/http/SpdyTransport.java | 2 +- .../okhttp/internal/spdy/ErrorCode.java | 9 +- .../okhttp/internal/spdy/FrameReader.java | 2 +- .../okhttp/internal/spdy/FrameWriter.java | 2 +- .../squareup/okhttp/internal/spdy/Header.java | 2 +- .../{HpackDraft05.java => HpackDraft06.java} | 43 +- ...{Http20Draft09.java => Http20Draft10.java} | 118 +- .../okhttp/internal/spdy/Huffman.java | 335 +++--- .../okhttp/internal/spdy/Settings.java | 14 +- .../okhttp/internal/spdy/SpdyConnection.java | 2 +- 20 files changed, 1382 insertions(+), 791 deletions(-) rename okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/{HttpOverHttp20Draft09Test.java => HttpOverHttp20Draft10Test.java} (96%) rename okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/{HpackDraft05Test.java => HpackDraft06Test.java} (92%) delete mode 100644 okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft09Test.java create mode 100644 okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft10Test.java rename okhttp/src/main/java/com/squareup/okhttp/internal/spdy/{HpackDraft05.java => HpackDraft06.java} (91%) rename okhttp/src/main/java/com/squareup/okhttp/internal/spdy/{Http20Draft09.java => Http20Draft10.java} (83%) diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp20Draft09Test.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp20Draft10Test.java similarity index 96% rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp20Draft09Test.java rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp20Draft10Test.java index 851a9c1f9..0a3fee8e9 100644 --- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp20Draft09Test.java +++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp20Draft10Test.java @@ -24,9 +24,9 @@ import org.junit.Test; import static org.junit.Assert.assertEquals; -public class HttpOverHttp20Draft09Test extends HttpOverSpdyTest { +public class HttpOverHttp20Draft10Test extends HttpOverSpdyTest { - public HttpOverHttp20Draft09Test() { + public HttpOverHttp20Draft10Test() { super(Protocol.HTTP_2); this.hostHeader = ":authority"; } diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDraft05Test.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDraft06Test.java similarity index 92% rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDraft05Test.java rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDraft06Test.java index 492057eb9..79c36755b 100644 --- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDraft05Test.java +++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDraft06Test.java @@ -27,24 +27,25 @@ import static com.squareup.okhttp.internal.Util.headerEntries; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; -public class HpackDraft05Test { +public class HpackDraft06Test { private final Buffer bytesIn = new Buffer(); - private HpackDraft05.Reader hpackReader; + private HpackDraft06.Reader hpackReader; private Buffer bytesOut = new Buffer(); - private HpackDraft05.Writer hpackWriter; + private HpackDraft06.Writer hpackWriter; @Before public void reset() { hpackReader = newReader(bytesIn); - hpackWriter = new HpackDraft05.Writer(bytesOut); + hpackWriter = new HpackDraft06.Writer(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. * - *

http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#section-4.1.2 + *

http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-06#section-4.1.2 */ @Test public void largeHeaderValue() throws IOException { char[] value = new char[4096]; @@ -185,7 +186,7 @@ public class HpackDraft05Test { } /** - * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.1.1 + * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-06#appendix-D.1.1 */ @Test public void readLiteralHeaderFieldWithIndexing() throws IOException { Buffer out = new Buffer(); @@ -239,7 +240,7 @@ public class HpackDraft05Test { } /** - * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.1.2 + * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-06#appendix-D.1.2 */ @Test public void literalHeaderFieldWithoutIndexingIndexedName() throws IOException { List

headerBlock = headerEntries(":path", "/sample/path"); @@ -263,7 +264,7 @@ public class HpackDraft05Test { } /** - * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.1.3 + * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-06#appendix-D.1.3 */ @Test public void readIndexedHeaderField() throws IOException { bytesIn.writeByte(0x82); // == Indexed - Add == @@ -282,8 +283,69 @@ public class HpackDraft05Test { assertEquals(headerEntries(":method", "GET"), hpackReader.getAndReset()); } + // Example taken from twitter/hpack DecoderTest.testIllegalIndex + @Test public void readIndexedHeaderFieldTooLargeIndex() throws IOException { + bytesIn.writeByte(0xff); // == Indexed - Add == + bytesIn.writeByte(0x00); // 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.writeByte(0x80); // idx = -2147483521 + bytesIn.writeByte(0x80); + bytesIn.writeByte(0x80); + bytesIn.writeByte(0x80); + bytesIn.writeByte(0x08); + + try { + hpackReader.readHeaders(); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("input must be between 0 and 63: -2147483514", e.getMessage()); + } + } + + // Example taken from twitter/hpack DecoderTest.testIllegalEncodeContextUpdate + @Test public void readHeaderTableStateChangeInvalid() throws IOException { + bytesIn.writeByte(0x80); // header table state change + bytesIn.writeByte(0x81); // should be 0x80 for empty! + + try { + hpackReader.readHeaders(); + fail(); + } catch (IOException e) { + assertEquals("Invalid header table state change -127", e.getMessage()); + } + } + + // Example taken from twitter/hpack DecoderTest.testInsidiousMaxHeaderSize + @Test public void readHeaderTableStateChangeInsidiousMaxHeaderByteCount() throws IOException { + bytesIn.writeByte(0x80); // header table state change + bytesIn.writeByte(0x7F); // encoded -1879048193 + bytesIn.writeByte(0x80); + bytesIn.writeByte(0xFF); + bytesIn.writeByte(0xFF); + bytesIn.writeByte(0xFF); + bytesIn.writeByte(0x08); + + try { + hpackReader.readHeaders(); + fail(); + } catch (IOException e) { + assertEquals("Invalid header table byte count -1879048193", e.getMessage()); + } + } + /** - * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#section-3.2.1 + * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-06#section-3.2.1 */ @Test public void toggleIndex() throws IOException { // Static table entries are copied to the top of the reference set. @@ -349,7 +411,7 @@ public class HpackDraft05Test { } /** - * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.1.4 + * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-06#appendix-D.1.4 */ @Test public void readIndexedHeaderFieldFromStaticTableWithoutBuffering() throws IOException { bytesIn.writeByte(0x82); // == Indexed - Add == @@ -366,7 +428,7 @@ public class HpackDraft05Test { } /** - * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.2 + * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-06#appendix-D.2 */ @Test public void readRequestExamplesWithoutHuffman() throws IOException { Buffer out = firstRequestWithoutHuffman(); @@ -494,6 +556,7 @@ public class HpackDraft05Test { Buffer out = new Buffer(); out.writeByte(0x80); // == Empty reference set == + out.writeByte(0x80); // idx = 0, flag = 1 out.writeByte(0x85); // == Indexed - Add == // idx = 5 -> :method: GET out.writeByte(0x8c); // == Indexed - Add == @@ -568,7 +631,7 @@ public class HpackDraft05Test { } /** - * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.3 + * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-06#appendix-D.3 */ @Test public void readRequestExamplesWithHuffman() throws IOException { Buffer out = firstRequestWithHuffman(); @@ -705,6 +768,7 @@ public class HpackDraft05Test { Buffer out = new Buffer(); out.writeByte(0x80); // == Empty reference set == + out.writeByte(0x80); // idx = 0, flag = 1 out.writeByte(0x85); // == Indexed - Add == // idx = 5 -> :method: GET out.writeByte(0x8c); // == Indexed - Add == @@ -848,8 +912,8 @@ public class HpackDraft05Test { assertEquals(ByteString.EMPTY, newReader(byteStream(0)).readByteString(false)); } - private HpackDraft05.Reader newReader(Buffer source) { - return new HpackDraft05.Reader(false, 4096, source); + private HpackDraft06.Reader newReader(Buffer source) { + return new HpackDraft06.Reader(4096, source); } private Buffer byteStream(int... bytes) { diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft09Test.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft09Test.java deleted file mode 100644 index 6e7ca8a6d..000000000 --- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft09Test.java +++ /dev/null @@ -1,515 +0,0 @@ -/* - * 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 com.squareup.okhttp.internal.spdy; - -import com.squareup.okhttp.internal.Util; -import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import okio.Buffer; -import okio.BufferedSource; -import okio.ByteString; -import org.junit.Test; - -import static com.squareup.okhttp.internal.Util.headerEntries; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -public class Http20Draft09Test { - static final int expectedStreamId = 15; - - @Test public void unknownFrameTypeIgnored() throws IOException { - Buffer frame = new Buffer(); - - frame.writeShort(4); // has a 4-byte field - frame.writeByte(99); // type 99 - frame.writeByte(0); // no flags - frame.writeInt(expectedStreamId); - frame.writeInt(111111111); // custom data - - FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); - - // Consume the unknown frame. - fr.nextFrame(new BaseTestHandler()); - } - - @Test public void onlyOneLiteralHeadersFrame() throws IOException { - final List
sentHeaders = headerEntries("name", "value"); - - Buffer frame = new Buffer(); - - // Write the headers frame, specifying no more frames are expected. - { - Buffer headerBytes = literalHeaders(sentHeaders); - frame.writeShort((int) headerBytes.size()); - frame.writeByte(Http20Draft09.TYPE_HEADERS); - frame.writeByte(Http20Draft09.FLAG_END_HEADERS | Http20Draft09.FLAG_END_STREAM); - frame.writeInt(expectedStreamId & 0x7fffffff); - frame.write(headerBytes, headerBytes.size()); - } - - FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); - - // Consume the headers frame. - fr.nextFrame(new BaseTestHandler() { - - @Override - public void headers(boolean outFinished, boolean inFinished, int streamId, - int associatedStreamId, int priority, List
headerBlock, - HeadersMode headersMode) { - assertFalse(outFinished); - assertTrue(inFinished); - assertEquals(expectedStreamId, streamId); - assertEquals(-1, associatedStreamId); - assertEquals(-1, priority); - assertEquals(sentHeaders, headerBlock); - assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode); - } - }); - } - - @Test public void headersWithPriority() throws IOException { - Buffer frame = new Buffer(); - - final List
sentHeaders = headerEntries("name", "value"); - - { // Write the headers frame, specifying priority flag and value. - Buffer headerBytes = literalHeaders(sentHeaders); - frame.writeShort((int) (headerBytes.size() + 4)); - frame.writeByte(Http20Draft09.TYPE_HEADERS); - frame.writeByte(Http20Draft09.FLAG_END_HEADERS | Http20Draft09.FLAG_PRIORITY); - frame.writeInt(expectedStreamId & 0x7fffffff); - frame.writeInt(0); // Highest priority is 0. - frame.write(headerBytes, headerBytes.size()); - } - - FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); - - // Consume the headers frame. - fr.nextFrame(new BaseTestHandler() { - - @Override - public void headers(boolean outFinished, boolean inFinished, int streamId, - int associatedStreamId, int priority, List
nameValueBlock, - HeadersMode headersMode) { - assertFalse(outFinished); - assertFalse(inFinished); - assertEquals(expectedStreamId, streamId); - assertEquals(-1, associatedStreamId); - assertEquals(0, priority); - assertEquals(sentHeaders, nameValueBlock); - assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode); - } - }); - } - - /** Headers are compressed, then framed. */ - @Test public void headersFrameThenContinuation() throws IOException { - - Buffer frame = new Buffer(); - - // Decoding the first header will cross frame boundaries. - Buffer headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux")); - { // Write the first headers frame. - frame.writeShort((int) (headerBlock.size() / 2)); - frame.writeByte(Http20Draft09.TYPE_HEADERS); - frame.writeByte(0); // no flags - frame.writeInt(expectedStreamId & 0x7fffffff); - frame.write(headerBlock, headerBlock.size() / 2); - } - - { // Write the continuation frame, specifying no more frames are expected. - frame.writeShort((int) headerBlock.size()); - frame.writeByte(Http20Draft09.TYPE_CONTINUATION); - frame.writeByte(Http20Draft09.FLAG_END_HEADERS); - frame.writeInt(expectedStreamId & 0x7fffffff); - frame.write(headerBlock, headerBlock.size()); - } - - FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); - - // Reading the above frames should result in a concatenated headerBlock. - fr.nextFrame(new BaseTestHandler() { - - @Override - public void headers(boolean outFinished, boolean inFinished, int streamId, - int associatedStreamId, int priority, List
headerBlock, - HeadersMode headersMode) { - assertFalse(outFinished); - assertFalse(inFinished); - assertEquals(expectedStreamId, streamId); - assertEquals(-1, associatedStreamId); - assertEquals(-1, priority); - assertEquals(headerEntries("foo", "barrr", "baz", "qux"), headerBlock); - assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode); - } - }); - } - - @Test public void pushPromise() throws IOException { - Buffer frame = new Buffer(); - - final int expectedPromisedStreamId = 11; - - final List
pushPromise = Arrays.asList( - new Header(Header.TARGET_METHOD, "GET"), - new Header(Header.TARGET_SCHEME, "https"), - new Header(Header.TARGET_AUTHORITY, "squareup.com"), - new Header(Header.TARGET_PATH, "/") - ); - - { // Write the push promise frame, specifying the associated stream ID. - Buffer headerBytes = literalHeaders(pushPromise); - frame.writeShort((int) (headerBytes.size() + 4)); - frame.writeByte(Http20Draft09.TYPE_PUSH_PROMISE); - frame.writeByte(Http20Draft09.FLAG_END_PUSH_PROMISE); - frame.writeInt(expectedStreamId & 0x7fffffff); - frame.writeInt(expectedPromisedStreamId & 0x7fffffff); - frame.write(headerBytes, headerBytes.size()); - } - - FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); - - // Consume the headers frame. - fr.nextFrame(new BaseTestHandler() { - @Override - public void pushPromise(int streamId, int promisedStreamId, List
headerBlock) { - assertEquals(expectedStreamId, streamId); - assertEquals(expectedPromisedStreamId, promisedStreamId); - assertEquals(pushPromise, headerBlock); - } - }); - } - - /** Headers are compressed, then framed. */ - @Test public void pushPromiseThenContinuation() throws IOException { - Buffer frame = new Buffer(); - - final int expectedPromisedStreamId = 11; - - final List
pushPromise = Arrays.asList( - new Header(Header.TARGET_METHOD, "GET"), - new Header(Header.TARGET_SCHEME, "https"), - new Header(Header.TARGET_AUTHORITY, "squareup.com"), - new Header(Header.TARGET_PATH, "/") - ); - - // Decoding the first header will cross frame boundaries. - Buffer headerBlock = literalHeaders(pushPromise); - int firstFrameLength = (int) (headerBlock.size() - 1); - { // Write the first headers frame. - frame.writeShort(firstFrameLength + 4); - frame.writeByte(Http20Draft09.TYPE_PUSH_PROMISE); - frame.writeByte(0); // no flags - frame.writeInt(expectedStreamId & 0x7fffffff); - frame.writeInt(expectedPromisedStreamId & 0x7fffffff); - frame.write(headerBlock, firstFrameLength); - } - - { // Write the continuation frame, specifying no more frames are expected. - frame.writeShort(1); - frame.writeByte(Http20Draft09.TYPE_CONTINUATION); - frame.writeByte(Http20Draft09.FLAG_END_HEADERS); - frame.writeInt(expectedStreamId & 0x7fffffff); - frame.write(headerBlock, 1); - } - - FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); - - // Reading the above frames should result in a concatenated headerBlock. - fr.nextFrame(new BaseTestHandler() { - @Override - public void pushPromise(int streamId, int promisedStreamId, List
headerBlock) { - assertEquals(expectedStreamId, streamId); - assertEquals(expectedPromisedStreamId, promisedStreamId); - assertEquals(pushPromise, headerBlock); - } - }); - } - - @Test public void readRstStreamFrame() throws IOException { - Buffer frame = new Buffer(); - - frame.writeShort(4); - frame.writeByte(Http20Draft09.TYPE_RST_STREAM); - frame.writeByte(0); // No flags - frame.writeInt(expectedStreamId & 0x7fffffff); - frame.writeInt(ErrorCode.COMPRESSION_ERROR.httpCode); - - FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); - - // Consume the reset frame. - fr.nextFrame(new BaseTestHandler() { - @Override public void rstStream(int streamId, ErrorCode errorCode) { - assertEquals(expectedStreamId, streamId); - assertEquals(ErrorCode.COMPRESSION_ERROR, errorCode); - } - }); - } - - @Test public void readSettingsFrame() throws IOException { - Buffer frame = new Buffer(); - - final int reducedTableSizeBytes = 16; - - frame.writeShort(16); // 2 settings * 4 bytes for the code and 4 for the value. - frame.writeByte(Http20Draft09.TYPE_SETTINGS); - frame.writeByte(0); // No flags - frame.writeInt(0 & 0x7fffffff); // Settings are always on the connection stream 0. - frame.writeInt(Settings.HEADER_TABLE_SIZE & 0xffffff); - frame.writeInt(reducedTableSizeBytes); - frame.writeInt(Settings.ENABLE_PUSH & 0xffffff); - frame.writeInt(0); - - final Http20Draft09.Reader fr = new Http20Draft09.Reader(frame, 4096, false); - - // Consume the settings frame. - fr.nextFrame(new BaseTestHandler() { - @Override public void settings(boolean clearPrevious, Settings settings) { - assertFalse(clearPrevious); // No clearPrevious in http/2. - assertEquals(reducedTableSizeBytes, settings.getHeaderTableSize()); - assertEquals(false, settings.getEnablePush(true)); - } - }); - } - - @Test public void pingRoundTrip() throws IOException { - Buffer frame = new Buffer(); - - final int expectedPayload1 = 7; - final int expectedPayload2 = 8; - - // Compose the expected PING frame. - frame.writeShort(8); // length - frame.writeByte(Http20Draft09.TYPE_PING); - frame.writeByte(Http20Draft09.FLAG_ACK); - frame.writeInt(0); // connection-level - frame.writeInt(expectedPayload1); - frame.writeInt(expectedPayload2); - - // Check writer sends the same bytes. - assertEquals(frame, sendPingFrame(true, expectedPayload1, expectedPayload2)); - - FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); - - fr.nextFrame(new BaseTestHandler() { // Consume the ping frame. - @Override public void ping(boolean ack, int payload1, int payload2) { - assertTrue(ack); - assertEquals(expectedPayload1, payload1); - assertEquals(expectedPayload2, payload2); - } - }); - } - - @Test public void maxLengthDataFrame() throws IOException { - Buffer frame = new Buffer(); - - final byte[] expectedData = new byte[16383]; - Arrays.fill(expectedData, (byte) 2); - - // Write the data frame. - frame.writeShort(expectedData.length); - frame.writeByte(Http20Draft09.TYPE_DATA); - frame.writeByte(0); // no flags - frame.writeInt(expectedStreamId & 0x7fffffff); - frame.write(expectedData); - - // Check writer sends the same bytes. - assertEquals(frame, sendDataFrame(new Buffer().write(expectedData))); - - FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); - - fr.nextFrame(new BaseTestHandler() { - @Override public void data( - boolean inFinished, int streamId, BufferedSource source, int length) throws IOException { - assertFalse(inFinished); - assertEquals(expectedStreamId, streamId); - assertEquals(16383, length); - ByteString data = source.readByteString(length); - for (byte b : data.toByteArray()){ - assertEquals(2, b); - } - } - }); - } - - @Test public void tooLargeDataFrame() throws IOException { - try { - sendDataFrame(new Buffer().write(new byte[0x1000000])); - fail(); - } catch (IllegalArgumentException e) { - assertEquals("FRAME_SIZE_ERROR length > 16383: 16777216", e.getMessage()); - } - } - - @Test public void windowUpdateRoundTrip() throws IOException { - Buffer frame = new Buffer(); - - final long expectedWindowSizeIncrement = 0x7fffffff; - - // Compose the expected window update frame. - frame.writeShort(4); // length - frame.writeByte(Http20Draft09.TYPE_WINDOW_UPDATE); - frame.writeByte(0); // No flags. - frame.writeInt(expectedStreamId); - frame.writeInt((int) expectedWindowSizeIncrement); - - // Check writer sends the same bytes. - assertEquals(frame, windowUpdate(expectedWindowSizeIncrement)); - - FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); - - fr.nextFrame(new BaseTestHandler() { // Consume the window update frame. - @Override public void windowUpdate(int streamId, long windowSizeIncrement) { - assertEquals(expectedStreamId, streamId); - assertEquals(expectedWindowSizeIncrement, windowSizeIncrement); - } - }); - } - - @Test public void badWindowSizeIncrement() throws IOException { - try { - windowUpdate(0); - fail(); - } catch (IllegalArgumentException e) { - assertEquals("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: 0", - e.getMessage()); - } - try { - windowUpdate(0x80000000L); - fail(); - } catch (IllegalArgumentException e) { - assertEquals("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: 2147483648", - e.getMessage()); - } - } - - @Test public void goAwayWithoutDebugDataRoundTrip() throws IOException { - Buffer frame = new Buffer(); - - final ErrorCode expectedError = ErrorCode.PROTOCOL_ERROR; - - // Compose the expected GOAWAY frame without debug data. - frame.writeShort(8); // Without debug data there's only 2 32-bit fields. - frame.writeByte(Http20Draft09.TYPE_GOAWAY); - frame.writeByte(0); // no flags. - frame.writeInt(0); // connection-scope - frame.writeInt(expectedStreamId); // last good stream. - frame.writeInt(expectedError.httpCode); - - // Check writer sends the same bytes. - assertEquals(frame, sendGoAway(expectedStreamId, expectedError, Util.EMPTY_BYTE_ARRAY)); - - FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); - - fr.nextFrame(new BaseTestHandler() { // Consume the go away frame. - @Override public void goAway( - int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) { - assertEquals(expectedStreamId, lastGoodStreamId); - assertEquals(expectedError, errorCode); - assertEquals(0, debugData.size()); - } - }); - } - - @Test public void goAwayWithDebugDataRoundTrip() throws IOException { - Buffer frame = new Buffer(); - - final ErrorCode expectedError = ErrorCode.PROTOCOL_ERROR; - final ByteString expectedData = ByteString.encodeUtf8("abcdefgh"); - - // Compose the expected GOAWAY frame without debug data. - frame.writeShort(8 + expectedData.size()); - frame.writeByte(Http20Draft09.TYPE_GOAWAY); - frame.writeByte(0); // no flags. - frame.writeInt(0); // connection-scope - frame.writeInt(0); // never read any stream! - frame.writeInt(expectedError.httpCode); - frame.write(expectedData.toByteArray()); - - // Check writer sends the same bytes. - assertEquals(frame, sendGoAway(0, expectedError, expectedData.toByteArray())); - - FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); - - fr.nextFrame(new BaseTestHandler() { // Consume the go away frame. - @Override public void goAway( - int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) { - assertEquals(0, lastGoodStreamId); - assertEquals(expectedError, errorCode); - assertEquals(expectedData, debugData); - } - }); - } - - @Test public void frameSizeError() throws IOException { - Http20Draft09.Writer writer = new Http20Draft09.Writer(new Buffer(), true); - - try { - writer.frameHeader(16384, Http20Draft09.TYPE_DATA, Http20Draft09.FLAG_NONE, 0); - fail(); - } catch (IllegalArgumentException e) { - assertEquals("FRAME_SIZE_ERROR length > 16383: 16384", e.getMessage()); - } - } - - @Test public void streamIdHasReservedBit() throws IOException { - Http20Draft09.Writer writer = new Http20Draft09.Writer(new Buffer(), true); - - try { - int streamId = 3; - streamId |= 1L << 31; // set reserved bit - writer.frameHeader(16383, Http20Draft09.TYPE_DATA, Http20Draft09.FLAG_NONE, streamId); - fail(); - } catch (IllegalArgumentException e) { - assertEquals("reserved bit set: -2147483645", e.getMessage()); - } - } - - private Buffer literalHeaders(List
sentHeaders) throws IOException { - Buffer out = new Buffer(); - new HpackDraft05.Writer(out).writeHeaders(sentHeaders); - return out; - } - - private Buffer sendPingFrame(boolean ack, int payload1, int payload2) throws IOException { - Buffer out = new Buffer(); - new Http20Draft09.Writer(out, true).ping(ack, payload1, payload2); - return out; - } - - private Buffer sendGoAway(int lastGoodStreamId, ErrorCode errorCode, byte[] debugData) - throws IOException { - Buffer out = new Buffer(); - new Http20Draft09.Writer(out, true).goAway(lastGoodStreamId, errorCode, debugData); - return out; - } - - private Buffer sendDataFrame(Buffer data) throws IOException { - Buffer out = new Buffer(); - new Http20Draft09.Writer(out, true).dataFrame(expectedStreamId, Http20Draft09.FLAG_NONE, data, - (int) data.size()); - return out; - } - - private Buffer windowUpdate(long windowSizeIncrement) throws IOException { - Buffer out = new Buffer(); - new Http20Draft09.Writer(out, true).windowUpdate(expectedStreamId, windowSizeIncrement); - return out; - } -} diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft10Test.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft10Test.java new file mode 100644 index 000000000..1f489a8b0 --- /dev/null +++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft10Test.java @@ -0,0 +1,1008 @@ +/* + * 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 com.squareup.okhttp.internal.spdy; + +import com.squareup.okhttp.internal.Util; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import okio.BufferedSource; +import okio.ByteString; +import okio.Buffer; +import org.junit.Test; + +import static com.squareup.okhttp.internal.Util.headerEntries; +import static com.squareup.okhttp.internal.spdy.Http20Draft10.FLAG_END_HEADERS; +import static com.squareup.okhttp.internal.spdy.Http20Draft10.FLAG_END_STREAM; +import static com.squareup.okhttp.internal.spdy.Http20Draft10.FLAG_NONE; +import static com.squareup.okhttp.internal.spdy.Http20Draft10.FLAG_PAD_HIGH; +import static com.squareup.okhttp.internal.spdy.Http20Draft10.FLAG_PAD_LOW; +import static com.squareup.okhttp.internal.spdy.Http20Draft10.FLAG_PRIORITY; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class Http20Draft10Test { + static final int expectedStreamId = 15; + + @Test public void unknownFrameTypeProtocolError() throws IOException { + Buffer frame = new Buffer(); + + frame.writeShort(4); // has a 4-byte field + frame.writeByte(99); // type 99 + frame.writeByte(0); // no flags + frame.writeInt(expectedStreamId); + frame.writeInt(111111111); // custom data + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + + // Consume the unknown frame. + try { + fr.nextFrame(new BaseTestHandler()); + fail(); + } catch (IOException e) { + assertEquals("PROTOCOL_ERROR: unknown frame type 99", e.getMessage()); + } + } + + @Test public void onlyOneLiteralHeadersFrame() throws IOException { + final List
sentHeaders = headerEntries("name", "value"); + + Buffer frame = new Buffer(); + + // Write the headers frame, specifying no more frames are expected. + { + Buffer headerBytes = literalHeaders(sentHeaders); + frame.writeShort((int) headerBytes.size()); + frame.writeByte(Http20Draft10.TYPE_HEADERS); + frame.writeByte(FLAG_END_HEADERS | FLAG_END_STREAM); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.write(headerBytes, headerBytes.size()); + } + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + + // Consume the headers frame. + fr.nextFrame(new BaseTestHandler() { + + @Override + public void headers(boolean outFinished, boolean inFinished, int streamId, + int associatedStreamId, int priority, List
headerBlock, + HeadersMode headersMode) { + assertFalse(outFinished); + assertTrue(inFinished); + assertEquals(expectedStreamId, streamId); + assertEquals(-1, associatedStreamId); + assertEquals(-1, priority); + assertEquals(sentHeaders, headerBlock); + assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode); + } + }); + } + + @Test public void headersWithPriority() throws IOException { + Buffer frame = new Buffer(); + + final List
sentHeaders = headerEntries("name", "value"); + + { // Write the headers frame, specifying priority flag and value. + Buffer headerBytes = literalHeaders(sentHeaders); + frame.writeShort((int) (headerBytes.size() + 4)); + frame.writeByte(Http20Draft10.TYPE_HEADERS); + frame.writeByte(FLAG_END_HEADERS | FLAG_PRIORITY); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.writeInt(0); // Highest priority is 0. + frame.write(headerBytes, headerBytes.size()); + } + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + + // Consume the headers frame. + fr.nextFrame(new BaseTestHandler() { + + @Override + public void headers(boolean outFinished, boolean inFinished, int streamId, + int associatedStreamId, int priority, List
nameValueBlock, + HeadersMode headersMode) { + assertFalse(outFinished); + assertFalse(inFinished); + assertEquals(expectedStreamId, streamId); + assertEquals(-1, associatedStreamId); + assertEquals(0, priority); + assertEquals(sentHeaders, nameValueBlock); + assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode); + } + }); + } + + /** Headers are compressed, then framed. */ + @Test public void headersFrameThenContinuation() throws IOException { + + Buffer frame = new Buffer(); + + // Decoding the first header will cross frame boundaries. + Buffer headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux")); + { // Write the first headers frame. + frame.writeShort((int) (headerBlock.size() / 2)); + frame.writeByte(Http20Draft10.TYPE_HEADERS); + frame.writeByte(0); // no flags + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.write(headerBlock, headerBlock.size() / 2); + } + + { // Write the continuation frame, specifying no more frames are expected. + frame.writeShort((int) headerBlock.size()); + frame.writeByte(Http20Draft10.TYPE_CONTINUATION); + frame.writeByte(FLAG_END_HEADERS); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.write(headerBlock, headerBlock.size()); + } + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + + // Reading the above frames should result in a concatenated headerBlock. + fr.nextFrame(new BaseTestHandler() { + + @Override + public void headers(boolean outFinished, boolean inFinished, int streamId, + int associatedStreamId, int priority, List
headerBlock, + HeadersMode headersMode) { + assertFalse(outFinished); + assertFalse(inFinished); + assertEquals(expectedStreamId, streamId); + assertEquals(-1, associatedStreamId); + assertEquals(-1, priority); + assertEquals(headerEntries("foo", "barrr", "baz", "qux"), headerBlock); + assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode); + } + }); + } + + @Test public void pushPromise() throws IOException { + Buffer frame = new Buffer(); + + final int expectedPromisedStreamId = 11; + + final List
pushPromise = Arrays.asList( + new Header(Header.TARGET_METHOD, "GET"), + new Header(Header.TARGET_SCHEME, "https"), + new Header(Header.TARGET_AUTHORITY, "squareup.com"), + new Header(Header.TARGET_PATH, "/") + ); + + { // Write the push promise frame, specifying the associated stream ID. + Buffer headerBytes = literalHeaders(pushPromise); + frame.writeShort((int) (headerBytes.size() + 4)); + frame.writeByte(Http20Draft10.TYPE_PUSH_PROMISE); + frame.writeByte(Http20Draft10.FLAG_END_PUSH_PROMISE); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.writeInt(expectedPromisedStreamId & 0x7fffffff); + frame.write(headerBytes, headerBytes.size()); + } + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + + // Consume the headers frame. + fr.nextFrame(new BaseTestHandler() { + @Override + public void pushPromise(int streamId, int promisedStreamId, List
headerBlock) { + assertEquals(expectedStreamId, streamId); + assertEquals(expectedPromisedStreamId, promisedStreamId); + assertEquals(pushPromise, headerBlock); + } + }); + } + + /** Headers are compressed, then framed. */ + @Test public void pushPromiseThenContinuation() throws IOException { + Buffer frame = new Buffer(); + + final int expectedPromisedStreamId = 11; + + final List
pushPromise = Arrays.asList( + new Header(Header.TARGET_METHOD, "GET"), + new Header(Header.TARGET_SCHEME, "https"), + new Header(Header.TARGET_AUTHORITY, "squareup.com"), + new Header(Header.TARGET_PATH, "/") + ); + + // Decoding the first header will cross frame boundaries. + Buffer headerBlock = literalHeaders(pushPromise); + int firstFrameLength = (int) (headerBlock.size() - 1); + { // Write the first headers frame. + frame.writeShort(firstFrameLength + 4); + frame.writeByte(Http20Draft10.TYPE_PUSH_PROMISE); + frame.writeByte(0); // no flags + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.writeInt(expectedPromisedStreamId & 0x7fffffff); + frame.write(headerBlock, firstFrameLength); + } + + { // Write the continuation frame, specifying no more frames are expected. + frame.writeShort(1); + frame.writeByte(Http20Draft10.TYPE_CONTINUATION); + frame.writeByte(FLAG_END_HEADERS); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.write(headerBlock, 1); + } + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + + // Reading the above frames should result in a concatenated headerBlock. + fr.nextFrame(new BaseTestHandler() { + @Override + public void pushPromise(int streamId, int promisedStreamId, List
headerBlock) { + assertEquals(expectedStreamId, streamId); + assertEquals(expectedPromisedStreamId, promisedStreamId); + assertEquals(pushPromise, headerBlock); + } + }); + } + + @Test public void readRstStreamFrame() throws IOException { + Buffer frame = new Buffer(); + + frame.writeShort(4); + frame.writeByte(Http20Draft10.TYPE_RST_STREAM); + frame.writeByte(0); // No flags + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.writeInt(ErrorCode.COMPRESSION_ERROR.httpCode); + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + + // Consume the reset frame. + fr.nextFrame(new BaseTestHandler() { + @Override public void rstStream(int streamId, ErrorCode errorCode) { + assertEquals(expectedStreamId, streamId); + assertEquals(ErrorCode.COMPRESSION_ERROR, errorCode); + } + }); + } + + @Test public void readSettingsFrame() throws IOException { + Buffer frame = new Buffer(); + + final int reducedTableSizeBytes = 16; + + frame.writeShort(10); // 2 settings * 1 bytes for the code and 4 for the value. + frame.writeByte(Http20Draft10.TYPE_SETTINGS); + frame.writeByte(0); // No flags + frame.writeInt(0); // Settings are always on the connection stream 0. + frame.writeByte(1); // SETTINGS_HEADER_TABLE_SIZE + frame.writeInt(reducedTableSizeBytes); + frame.writeByte(2); // SETTINGS_ENABLE_PUSH + frame.writeInt(0); + + final Http20Draft10.Reader fr = new Http20Draft10.Reader(frame, 4096, false); + + // Consume the settings frame. + fr.nextFrame(new BaseTestHandler() { + @Override public void settings(boolean clearPrevious, Settings settings) { + assertFalse(clearPrevious); // No clearPrevious in HTTP/2. + assertEquals(reducedTableSizeBytes, settings.getHeaderTableSize()); + assertEquals(false, settings.getEnablePush(true)); + } + }); + } + + @Test public void readSettingsFrameInvalidPushValue() throws IOException { + Buffer frame = new Buffer(); + + frame.writeShort(5); // 1 settings * 1 bytes for the code and 4 for the value. + frame.writeByte(Http20Draft10.TYPE_SETTINGS); + frame.writeByte(0); // No flags + frame.writeInt(0); // Settings are always on the connection stream 0. + frame.writeByte(2); + frame.writeInt(2); + + final Http20Draft10.Reader fr = new Http20Draft10.Reader(frame, 4096, false); + + try { + fr.nextFrame(new BaseTestHandler()); + fail(); + } catch (IOException e) { + assertEquals("PROTOCOL_ERROR SETTINGS_ENABLE_PUSH != 0 or 1", e.getMessage()); + } + } + + @Test public void readSettingsFrameInvalidSettingId() throws IOException { + Buffer frame = new Buffer(); + + frame.writeShort(5); // 1 settings * 1 bytes for the code and 4 for the value. + frame.writeByte(Http20Draft10.TYPE_SETTINGS); + frame.writeByte(0); // No flags + frame.writeInt(0); // Settings are always on the connection stream 0. + frame.writeByte(7); // old number for SETTINGS_INITIAL_WINDOW_SIZE + frame.writeInt(1); + + final Http20Draft10.Reader fr = new Http20Draft10.Reader(frame, 4096, false); + + try { + fr.nextFrame(new BaseTestHandler()); + fail(); + } catch (IOException e) { + assertEquals("PROTOCOL_ERROR invalid settings id: 7", e.getMessage()); + } + } + + @Test public void readSettingsFrameNegativeWindowSize() throws IOException { + Buffer frame = new Buffer(); + + frame.writeShort(5); // 1 settings * 1 bytes for the code and 4 for the value. + frame.writeByte(Http20Draft10.TYPE_SETTINGS); + frame.writeByte(0); // No flags + frame.writeInt(0); // Settings are always on the connection stream 0. + frame.writeByte(4); // SETTINGS_INITIAL_WINDOW_SIZE + frame.writeInt(Integer.MIN_VALUE); + + final Http20Draft10.Reader fr = new Http20Draft10.Reader(frame, 4096, false); + + try { + fr.nextFrame(new BaseTestHandler()); + fail(); + } catch (IOException e) { + assertEquals("PROTOCOL_ERROR SETTINGS_INITIAL_WINDOW_SIZE > 2^31 - 1", e.getMessage()); + } + } + + @Test public void pingRoundTrip() throws IOException { + Buffer frame = new Buffer(); + + final int expectedPayload1 = 7; + final int expectedPayload2 = 8; + + // Compose the expected PING frame. + frame.writeShort(8); // length + frame.writeByte(Http20Draft10.TYPE_PING); + frame.writeByte(Http20Draft10.FLAG_ACK); + frame.writeInt(0); // connection-level + frame.writeInt(expectedPayload1); + frame.writeInt(expectedPayload2); + + // Check writer sends the same bytes. + assertEquals(frame, sendPingFrame(true, expectedPayload1, expectedPayload2)); + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + + fr.nextFrame(new BaseTestHandler() { // Consume the ping frame. + @Override public void ping(boolean ack, int payload1, int payload2) { + assertTrue(ack); + assertEquals(expectedPayload1, payload1); + assertEquals(expectedPayload2, payload2); + } + }); + } + + @Test public void maxLengthDataFrame() throws IOException { + Buffer frame = new Buffer(); + + final byte[] expectedData = new byte[16383]; + Arrays.fill(expectedData, (byte) 2); + + // Write the data frame. + frame.writeShort(expectedData.length); + frame.writeByte(Http20Draft10.TYPE_DATA); + frame.writeByte(0); // no flags + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.write(expectedData); + + // Check writer sends the same bytes. + assertEquals(frame, sendDataFrame(new Buffer().write(expectedData))); + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + + fr.nextFrame(new BaseTestHandler() { + @Override public void data( + boolean inFinished, int streamId, BufferedSource source, int length) throws IOException { + assertFalse(inFinished); + assertEquals(expectedStreamId, streamId); + assertEquals(16383, length); + ByteString data = source.readByteString(length); + for (byte b : data.toByteArray()) { + assertEquals(2, b); + } + } + }); + } + + @Test public void readPaddedDataFrame() throws IOException { + final Buffer frame = new Buffer(); + + final int dataLength = 1123; + final byte[] expectedData = new byte[dataLength]; + Arrays.fill(expectedData, (byte) 2); + + final int paddingLength = 257; + final byte[] padding = new byte[paddingLength]; + Arrays.fill(padding, (byte) 0); + + // Write the data frame. + frame.writeShort(dataLength + paddingLength + 2); // 2 for PAD_HIGH,LOW. + frame.writeByte(Http20Draft10.TYPE_DATA); + frame.writeByte(FLAG_PAD_HIGH | FLAG_PAD_LOW); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.writeShort(paddingLength); + frame.write(expectedData); + frame.write(padding); + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + fr.nextFrame(assertData()); + assertTrue(frame.exhausted()); // Padding was skipped. + } + + @Test public void readPaddedDataFrameZeroPaddingHigh() throws IOException { + final Buffer frame = new Buffer(); + + final int dataLength = 1123; + final byte[] expectedData = new byte[dataLength]; + Arrays.fill(expectedData, (byte) 2); + + // Write the data frame. + frame.writeShort(dataLength + 2); // 2 for PAD_HIGH,LOW. + frame.writeByte(Http20Draft10.TYPE_DATA); + frame.writeByte(FLAG_PAD_HIGH | FLAG_PAD_LOW); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.writeShort(0); + frame.write(expectedData); + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + fr.nextFrame(assertData()); + } + + @Test public void readPaddedDataFrameZeroPaddingLow() throws IOException { + final Buffer frame = new Buffer(); + + final int dataLength = 1123; + final byte[] expectedData = new byte[dataLength]; + Arrays.fill(expectedData, (byte) 2); + + // Write the data frame. + frame.writeShort(dataLength + 1); + frame.writeByte(Http20Draft10.TYPE_DATA); + frame.writeByte(FLAG_PAD_LOW); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.writeByte(0); + frame.write(expectedData); + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + fr.nextFrame(assertData()); + } + + @Test public void readPaddedDataFrameMissingLowFlag() throws IOException { + final Buffer frame = new Buffer(); + + final int dataLength = 1123; + final byte[] expectedData = new byte[dataLength]; + Arrays.fill(expectedData, (byte) 2); + + final int paddingLength = 257; + final byte[] padding = new byte[paddingLength]; + Arrays.fill(padding, (byte) 0); + + // Write the data frame. + frame.writeShort(dataLength + paddingLength + 2); // 2 for PAD_HIGH,LOW. + frame.writeByte(Http20Draft10.TYPE_DATA); + frame.writeByte(FLAG_PAD_HIGH); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.writeShort(paddingLength); + frame.write(expectedData); + frame.write(padding); + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + + try { + fr.nextFrame(new BaseTestHandler()); + fail(); + } catch (IOException e) { + assertEquals("PROTOCOL_ERROR FLAG_PAD_HIGH set without FLAG_PAD_LOW", e.getMessage()); + } + } + + /** + * Padding is encoded over 2 bytes, so maximum value is 65535, but maximum frame size is 16383. + */ + @Test public void readPaddedDataFrameWithTooMuchPadding() throws IOException { + final Buffer frame = new Buffer(); + + final int dataLength = 1123; + final byte[] expectedData = new byte[dataLength]; + Arrays.fill(expectedData, (byte) 2); + + final byte[] padding = new byte[0xffff]; + Arrays.fill(padding, (byte) 0); + + // Write the data frame. + frame.writeShort(dataLength + 2); // 2 for PAD_HIGH,LOW. + frame.writeByte(Http20Draft10.TYPE_HEADERS); + frame.writeByte(FLAG_PAD_HIGH | FLAG_PAD_LOW); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.writeShort(0xffff); + frame.write(expectedData); + frame.write(padding); + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + + try { + fr.nextFrame(new BaseTestHandler()); + fail(); + } catch (IOException e) { + assertEquals("PROTOCOL_ERROR padding > 16383: 65535", e.getMessage()); + } + } + + @Test public void readPaddedHeadersFrame() throws IOException { + final Buffer frame = new Buffer(); + + final int paddingLength = 257; + final byte[] padding = new byte[paddingLength]; + Arrays.fill(padding, (byte) 0); + + Buffer headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux")); + frame.writeShort((int) headerBlock.size() + paddingLength + 2); // 2 for PAD_HIGH,LOW. + frame.writeByte(Http20Draft10.TYPE_HEADERS); + frame.writeByte(FLAG_END_HEADERS | FLAG_PAD_HIGH | FLAG_PAD_LOW); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.writeShort(paddingLength); + frame.write(headerBlock, headerBlock.size()); + frame.write(padding); + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + fr.nextFrame(assertHeaderBlock()); + assertTrue(frame.exhausted()); // Padding was skipped. + } + + @Test public void readPaddedHeadersFrameZeroPaddingHigh() throws IOException { + final Buffer frame = new Buffer(); + + Buffer headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux")); + frame.writeShort((int) headerBlock.size() + 2); // 2 for PAD_HIGH,LOW. + frame.writeByte(Http20Draft10.TYPE_HEADERS); + frame.writeByte(FLAG_END_HEADERS | FLAG_PAD_HIGH | FLAG_PAD_LOW); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.writeShort(0); + frame.write(headerBlock, headerBlock.size()); + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + fr.nextFrame(assertHeaderBlock()); + } + + @Test public void readPaddedHeadersFrameZeroPaddingLow() throws IOException { + final Buffer frame = new Buffer(); + + Buffer headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux")); + frame.writeShort((int) headerBlock.size() + 2); // 2 for PAD_HIGH,LOW. + frame.writeByte(Http20Draft10.TYPE_HEADERS); + frame.writeByte(FLAG_END_HEADERS | FLAG_PAD_LOW); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.writeByte(0); + frame.write(headerBlock, headerBlock.size()); + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + fr.nextFrame(assertHeaderBlock()); + } + + @Test public void readPaddedHeadersFrameMissingLowFlag() throws IOException { + final Buffer frame = new Buffer(); + + final int paddingLength = 257; + final byte[] padding = new byte[paddingLength]; + Arrays.fill(padding, (byte) 0); + + Buffer headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux")); + frame.writeShort((int) headerBlock.size() + 1); + frame.writeByte(Http20Draft10.TYPE_HEADERS); + frame.writeByte(FLAG_PAD_HIGH); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.writeShort(paddingLength); + frame.write(headerBlock, headerBlock.size()); + frame.write(padding); + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + + try { + fr.nextFrame(new BaseTestHandler()); + fail(); + } catch (IOException e) { + assertEquals("PROTOCOL_ERROR FLAG_PAD_HIGH set without FLAG_PAD_LOW", e.getMessage()); + } + } + + /** + * Padding is encoded over 2 bytes, so maximum value is 65535, but maximum frame size is 16383. + */ + @Test public void readPaddedHeadersFrameWithTooMuchPadding() throws IOException { + final Buffer frame = new Buffer(); + + final byte[] padding = new byte[0xffff]; + Arrays.fill(padding, (byte) 0); + + Buffer headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux")); + frame.writeShort((int) headerBlock.size() + 2); // 2 for PAD_HIGH,LOW. + frame.writeByte(Http20Draft10.TYPE_HEADERS); + frame.writeByte(FLAG_PAD_HIGH | FLAG_PAD_LOW); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.writeShort(0xffff); + frame.write(headerBlock, headerBlock.size()); + frame.write(padding); + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + + try { + fr.nextFrame(new BaseTestHandler()); + fail(); + } catch (IOException e) { + assertEquals("PROTOCOL_ERROR padding > 16383: 65535", e.getMessage()); + } + } + + /** Headers are compressed, then framed. */ + @Test public void readPaddedHeadersFrameThenContinuation() throws IOException { + + Buffer frame = new Buffer(); + + final int paddingLength = 257; + final byte[] padding = new byte[paddingLength]; + Arrays.fill(padding, (byte) 0); + + // Decoding the first header will cross frame boundaries. + Buffer headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux")); + { // Write the first headers frame. + frame.writeShort((int) (headerBlock.size() / 2) + paddingLength + 2); // 2 for PAD_HIGH,LOW. + frame.writeByte(Http20Draft10.TYPE_HEADERS); + frame.writeByte(FLAG_PAD_HIGH | FLAG_PAD_LOW); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.writeShort(paddingLength); + frame.write(headerBlock, headerBlock.size() / 2); + frame.write(padding); + } + + { // Write the continuation frame, specifying no more frames are expected. + frame.writeShort((int) headerBlock.size() + paddingLength + 2); + frame.writeByte(Http20Draft10.TYPE_CONTINUATION); // 2 for PAD_HIGH,LOW. + frame.writeByte(FLAG_END_HEADERS | FLAG_PAD_HIGH | FLAG_PAD_LOW); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.writeShort(paddingLength); + frame.write(headerBlock, headerBlock.size()); + frame.write(padding); + } + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + fr.nextFrame(assertHeaderBlock()); + assertTrue(frame.exhausted()); // Padding was skipped. + } + + @Test public void readPaddedContinuationFrameZeroPaddingHigh() throws IOException { + final Buffer frame = new Buffer(); + + Buffer headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux")); + { // Write the first headers frame. + frame.writeShort((int) (headerBlock.size() / 2)); + frame.writeByte(Http20Draft10.TYPE_HEADERS); + frame.writeByte(0); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.write(headerBlock, headerBlock.size() / 2); + } + + { // Write the continuation frame, specifying no more frames are expected. + frame.writeShort((int) headerBlock.size() + 2); // 2 for PAD_HIGH,LOW. + frame.writeByte(Http20Draft10.TYPE_CONTINUATION); + frame.writeByte(FLAG_END_HEADERS | FLAG_PAD_HIGH | FLAG_PAD_LOW); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.writeShort(0); + frame.write(headerBlock, headerBlock.size()); + } + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + fr.nextFrame(assertHeaderBlock()); + } + + @Test public void readPaddedContinuationFrameZeroPaddingLow() throws IOException { + final Buffer frame = new Buffer(); + + Buffer headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux")); + { // Write the first headers frame. + frame.writeShort((int) (headerBlock.size() / 2)); + frame.writeByte(Http20Draft10.TYPE_HEADERS); + frame.writeByte(0); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.write(headerBlock, headerBlock.size() / 2); + } + + { // Write the continuation frame, specifying no more frames are expected. + frame.writeShort((int) headerBlock.size() + 2); // 2 for PAD_HIGH,LOW. + frame.writeByte(Http20Draft10.TYPE_CONTINUATION); + frame.writeByte(FLAG_END_HEADERS | FLAG_PAD_LOW); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.writeByte(0); + frame.write(headerBlock, headerBlock.size()); + } + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + fr.nextFrame(assertHeaderBlock()); + } + + @Test public void readPaddedContinuationFrameMissingLowFlag() throws IOException { + final Buffer frame = new Buffer(); + + final int paddingLength = 257; + final byte[] padding = new byte[paddingLength]; + Arrays.fill(padding, (byte) 0); + + Buffer headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux")); + { // Write the first headers frame. + frame.writeShort((int) (headerBlock.size() / 2)); + frame.writeByte(Http20Draft10.TYPE_HEADERS); + frame.writeByte(0); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.write(headerBlock, headerBlock.size() / 2); + } + + { // Write the continuation frame, specifying no more frames are expected. + frame.writeShort((int) headerBlock.size() + 1); + frame.writeByte(Http20Draft10.TYPE_CONTINUATION); + frame.writeByte(FLAG_PAD_HIGH); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.writeShort(paddingLength); + frame.write(headerBlock, headerBlock.size()); + frame.write(padding); + } + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + + try { + fr.nextFrame(new BaseTestHandler()); + fail(); + } catch (IOException e) { + assertEquals("PROTOCOL_ERROR FLAG_PAD_HIGH set without FLAG_PAD_LOW", e.getMessage()); + } + } + + /** + * Padding is encoded over 2 bytes, so maximum value is 65535, but maximum frame size is 16383. + */ + @Test public void readPaddedContinuationFrameWithTooMuchPadding() throws IOException { + final Buffer frame = new Buffer(); + + final byte[] padding = new byte[0xffff]; + Arrays.fill(padding, (byte) 0); + + Buffer headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux")); + { // Write the first headers frame. + frame.writeShort((int) (headerBlock.size() / 2)); + frame.writeByte(Http20Draft10.TYPE_HEADERS); + frame.writeByte(0); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.write(headerBlock, headerBlock.size() / 2); + } + + { // Write the continuation frame, specifying no more frames are expected. + frame.writeShort((int) (headerBlock.size() / 2) + 2); // 2 for PAD_HIGH,LOW. + frame.writeByte(Http20Draft10.TYPE_CONTINUATION); + frame.writeByte(FLAG_PAD_HIGH | FLAG_PAD_LOW); + frame.writeInt(expectedStreamId & 0x7fffffff); + frame.writeShort(0xffff); + frame.write(headerBlock, (headerBlock.size() / 2)); + frame.write(padding); + } + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + + try { + fr.nextFrame(new BaseTestHandler()); + fail(); + } catch (IOException e) { + assertEquals("PROTOCOL_ERROR padding > 16383: 65535", e.getMessage()); + } + } + + @Test public void tooLargeDataFrame() throws IOException { + try { + sendDataFrame(new Buffer().write(new byte[0x1000000])); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("FRAME_SIZE_ERROR length > 16383: 16777216", e.getMessage()); + } + } + + @Test public void windowUpdateRoundTrip() throws IOException { + Buffer frame = new Buffer(); + + final long expectedWindowSizeIncrement = 0x7fffffff; + + // Compose the expected window update frame. + frame.writeShort(4); // length + frame.writeByte(Http20Draft10.TYPE_WINDOW_UPDATE); + frame.writeByte(0); // No flags. + frame.writeInt(expectedStreamId); + frame.writeInt((int) expectedWindowSizeIncrement); + + // Check writer sends the same bytes. + assertEquals(frame, windowUpdate(expectedWindowSizeIncrement)); + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + + fr.nextFrame(new BaseTestHandler() { // Consume the window update frame. + @Override public void windowUpdate(int streamId, long windowSizeIncrement) { + assertEquals(expectedStreamId, streamId); + assertEquals(expectedWindowSizeIncrement, windowSizeIncrement); + } + }); + } + + @Test public void badWindowSizeIncrement() throws IOException { + try { + windowUpdate(0); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: 0", + e.getMessage()); + } + try { + windowUpdate(0x80000000L); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: 2147483648", + e.getMessage()); + } + } + + @Test public void goAwayWithoutDebugDataRoundTrip() throws IOException { + Buffer frame = new Buffer(); + + final ErrorCode expectedError = ErrorCode.PROTOCOL_ERROR; + + // Compose the expected GOAWAY frame without debug data. + frame.writeShort(8); // Without debug data there's only 2 32-bit fields. + frame.writeByte(Http20Draft10.TYPE_GOAWAY); + frame.writeByte(0); // no flags. + frame.writeInt(0); // connection-scope + frame.writeInt(expectedStreamId); // last good stream. + frame.writeInt(expectedError.httpCode); + + // Check writer sends the same bytes. + assertEquals(frame, sendGoAway(expectedStreamId, expectedError, Util.EMPTY_BYTE_ARRAY)); + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + + fr.nextFrame(new BaseTestHandler() { // Consume the go away frame. + @Override public void goAway( + int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) { + assertEquals(expectedStreamId, lastGoodStreamId); + assertEquals(expectedError, errorCode); + assertEquals(0, debugData.size()); + } + }); + } + + @Test public void goAwayWithDebugDataRoundTrip() throws IOException { + Buffer frame = new Buffer(); + + final ErrorCode expectedError = ErrorCode.PROTOCOL_ERROR; + final ByteString expectedData = ByteString.encodeUtf8("abcdefgh"); + + // Compose the expected GOAWAY frame without debug data. + frame.writeShort(8 + expectedData.size()); + frame.writeByte(Http20Draft10.TYPE_GOAWAY); + frame.writeByte(0); // no flags. + frame.writeInt(0); // connection-scope + frame.writeInt(0); // never read any stream! + frame.writeInt(expectedError.httpCode); + frame.write(expectedData.toByteArray()); + + // Check writer sends the same bytes. + assertEquals(frame, sendGoAway(0, expectedError, expectedData.toByteArray())); + + FrameReader fr = new Http20Draft10.Reader(frame, 4096, false); + + fr.nextFrame(new BaseTestHandler() { // Consume the go away frame. + @Override public void goAway( + int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) { + assertEquals(0, lastGoodStreamId); + assertEquals(expectedError, errorCode); + assertEquals(expectedData, debugData); + } + }); + } + + @Test public void frameSizeError() throws IOException { + Http20Draft10.Writer writer = new Http20Draft10.Writer(new Buffer(), true); + + try { + writer.frameHeader(16384, Http20Draft10.TYPE_DATA, FLAG_NONE, 0); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("FRAME_SIZE_ERROR length > 16383: 16384", e.getMessage()); + } + } + + @Test public void streamIdHasReservedBit() throws IOException { + Http20Draft10.Writer writer = new Http20Draft10.Writer(new Buffer(), true); + + try { + int streamId = 3; + streamId |= 1L << 31; // set reserved bit + writer.frameHeader(16383, Http20Draft10.TYPE_DATA, FLAG_NONE, streamId); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("reserved bit set: -2147483645", e.getMessage()); + } + } + + private Buffer literalHeaders(List
sentHeaders) throws IOException { + Buffer out = new Buffer(); + new HpackDraft06.Writer(out).writeHeaders(sentHeaders); + return out; + } + + private Buffer sendPingFrame(boolean ack, int payload1, int payload2) throws IOException { + Buffer out = new Buffer(); + new Http20Draft10.Writer(out, true).ping(ack, payload1, payload2); + return out; + } + + private Buffer sendGoAway(int lastGoodStreamId, ErrorCode errorCode, byte[] debugData) + throws IOException { + Buffer out = new Buffer(); + new Http20Draft10.Writer(out, true).goAway(lastGoodStreamId, errorCode, debugData); + return out; + } + + private Buffer sendDataFrame(Buffer data) throws IOException { + Buffer out = new Buffer(); + new Http20Draft10.Writer(out, true).dataFrame(expectedStreamId, FLAG_NONE, data, + (int) data.size()); + return out; + } + + private Buffer windowUpdate(long windowSizeIncrement) throws IOException { + Buffer out = new Buffer(); + new Http20Draft10.Writer(out, true).windowUpdate(expectedStreamId, windowSizeIncrement); + return out; + } + + private FrameReader.Handler assertHeaderBlock() { + return new BaseTestHandler() { + @Override + public void headers(boolean outFinished, boolean inFinished, int streamId, + int associatedStreamId, int priority, List
headerBlock, + HeadersMode headersMode) { + assertFalse(outFinished); + assertFalse(inFinished); + assertEquals(expectedStreamId, streamId); + assertEquals(-1, associatedStreamId); + assertEquals(-1, priority); + assertEquals(headerEntries("foo", "barrr", "baz", "qux"), headerBlock); + assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode); + } + }; + } + + private FrameReader.Handler assertData() { + return new BaseTestHandler() { + @Override public void data( + boolean inFinished, int streamId, BufferedSource source, int length) throws IOException { + assertFalse(inFinished); + assertEquals(expectedStreamId, streamId); + assertEquals(1123, length); + ByteString data = source.readByteString(length); + for (byte b : data.toByteArray()) { + assertEquals(2, b); + } + } + }; + } +} diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HuffmanTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HuffmanTest.java index 6206b7efb..222d23e15 100644 --- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HuffmanTest.java +++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HuffmanTest.java @@ -43,18 +43,13 @@ public class HuffmanTest { } private void assertRoundTrip(byte[] buf) throws IOException { - assertRoundTrip(Huffman.Codec.REQUEST, buf); - assertRoundTrip(Huffman.Codec.RESPONSE, buf); - } - - private static void assertRoundTrip(Huffman.Codec codec, byte[] buf) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos); - codec.encode(buf, dos); - assertEquals(baos.size(), codec.encodedLength(buf)); + Huffman.get().encode(buf, dos); + assertEquals(baos.size(), Huffman.get().encodedLength(buf)); - byte[] decodedBytes = codec.decode(baos.toByteArray()); + byte[] decodedBytes = Huffman.get().decode(baos.toByteArray()); assertTrue(Arrays.equals(buf, decodedBytes)); } } diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java index f43efa9ce..5507c2d06 100644 --- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java +++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java @@ -280,7 +280,7 @@ public final class MockSpdyPeer implements Closeable { @Override public void pushPromise(int streamId, int associatedStreamId, List
headerBlock) { - this.type = Http20Draft09.TYPE_PUSH_PROMISE; + this.type = Http20Draft10.TYPE_PUSH_PROMISE; this.streamId = streamId; this.associatedStreamId = associatedStreamId; this.headerBlock = headerBlock; diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java index 294684f1b..e92ba1895 100644 --- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java +++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java @@ -37,7 +37,7 @@ public final class SettingsTest { @Test public void setFields() { Settings settings = new Settings(); - // WARNING: clash on flags between spdy/3 and http/2! + // WARNING: clash on flags between spdy/3 and HTTP/2! assertEquals(-3, settings.getUploadBandwidth(-3)); assertEquals(-1, settings.getHeaderTableSize()); settings.set(UPLOAD_BANDWIDTH, 0, 42); @@ -45,7 +45,7 @@ public final class SettingsTest { settings.set(Settings.HEADER_TABLE_SIZE, 0, 8096); assertEquals(8096, settings.getHeaderTableSize()); - // WARNING: clash on flags between spdy/3 and http/2! + // WARNING: clash on flags between spdy/3 and HTTP/2! assertEquals(-3, settings.getDownloadBandwidth(-3)); assertEquals(true, settings.getEnablePush(true)); settings.set(DOWNLOAD_BANDWIDTH, 0, 53); diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java index 0ea345c8e..1a20f8e4e 100644 --- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java +++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java @@ -55,7 +55,7 @@ import static org.junit.Assert.fail; public final class SpdyConnectionTest { private static final Variant SPDY3 = new Spdy3(); - private static final Variant HTTP_20_DRAFT_09 = new Http20Draft09(); + private static final Variant HTTP_20_DRAFT_09 = new Http20Draft10(); private final MockSpdyPeer peer = new MockSpdyPeer(); @After public void tearDown() throws Exception { @@ -336,7 +336,7 @@ public final class SpdyConnectionTest { // verify the peer's settings were read and applied. synchronized (connection) { assertEquals(0, connection.peerSettings.getHeaderTableSize()); - Http20Draft09.Reader frameReader = (Http20Draft09.Reader) connection.frameReader; + Http20Draft10.Reader frameReader = (Http20Draft10.Reader) connection.frameReader; assertEquals(0, frameReader.hpackReader.maxHeaderTableByteCount()); // TODO: when supported, check the frameWriter's compression table is unaffected. } diff --git a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java index f832612a3..70e1693a3 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java +++ b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java @@ -340,7 +340,7 @@ public final class OkHttpClient implements URLStreamHandlerFactory, Cloneable { * *

This is an evolving set. Future releases may drop * support for transitional protocols (like spdy/3.1), in favor of their - * successors (spdy/4 or http/2.0). The http/1.1 transport will never be + * successors (spdy/4 or hTTP/2). The http/1.1 transport will never be * dropped. * *

If multiple protocols are specified, emittedHeaders = new ArrayList

(); private final BufferedSource source; @@ -119,8 +118,7 @@ final class HpackDraft05 { BitArray emittedReferencedHeaders = new BitArray.FixedCapacity(); int headerTableByteCount = 0; - Reader(boolean client, int maxHeaderTableByteCount, Source source) { - this.huffmanCodec = client ? Huffman.Codec.RESPONSE : Huffman.Codec.REQUEST; + Reader(int maxHeaderTableByteCount, Source source) { this.maxHeaderTableByteCount = maxHeaderTableByteCount; this.source = Okio.buffer(source); } @@ -181,7 +179,22 @@ final class HpackDraft05 { while (!source.exhausted()) { int b = source.readByte() & 0xff; if (b == 0x80) { // 10000000 - clearReferenceSet(); + b = source.readByte(); + if ((b & 0x80) == 0x80) { // 1NNNNNNN + if ((b & PREFIX_7_BITS) == 0) { + clearReferenceSet(); + } else { + throw new IOException("Invalid header table state change " + b); + } + } else { + int headerTableByteCount = b != PREFIX_7_BITS ? b : readInt(b, PREFIX_7_BITS); + if (headerTableByteCount < 0) { + throw new IOException("Invalid header table byte count " + headerTableByteCount); + } + maxHeaderTableByteCount = Math.min(headerTableByteCount, maxHeaderTableByteCount); + int toEvict = headerTableByteCount - maxHeaderTableByteCount; + if (toEvict > 0) evictToRecoverBytes(toEvict); + } } else if ((b & 0x80) == 0x80) { // 1NNNNNNN int index = readInt(b, PREFIX_7_BITS); readIndexedHeader(index - 1); @@ -228,9 +241,13 @@ final class HpackDraft05 { return result; } - private void readIndexedHeader(int index) { + private void readIndexedHeader(int index) throws IOException { if (isStaticHeader(index)) { - Header staticEntry = STATIC_HEADER_TABLE[index - headerCount]; + index -= headerCount; + if (index > STATIC_HEADER_TABLE.length - 1) { + throw new IOException("Header index too large " + (index + 1)); + } + Header staticEntry = STATIC_HEADER_TABLE[index]; if (maxHeaderTableByteCount == 0) { emittedHeaders.add(staticEntry); } else { @@ -371,7 +388,7 @@ final class HpackDraft05 { ByteString byteString = source.readByteString(length); if (huffmanDecode) { - byteString = huffmanCodec.decode(byteString); // TODO: streaming Huffman! + byteString = Huffman.get().decode(byteString); // TODO: streaming Huffman! } if (asciiLowercase) { @@ -419,7 +436,7 @@ final class HpackDraft05 { } } - // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#section-4.1.1 + // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-06#section-4.1.1 void writeInt(int value, int prefixMask, int bits) throws IOException { // Write the raw value for a single byte value. if (value < prefixMask) { diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft09.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft10.java similarity index 83% rename from okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft09.java rename to okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft10.java index 1c3e7f39b..dd4498983 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft09.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft10.java @@ -26,10 +26,10 @@ import okio.Source; import okio.Timeout; /** - * Read and write http/2 v09 frames. - * http://tools.ietf.org/html/draft-ietf-httpbis-http2-09 + * Read and write HTTP/2 v10 frames. + * http://tools.ietf.org/html/draft-ietf-httpbis-http2-10 */ -public final class Http20Draft09 implements Variant { +public final class Http20Draft10 implements Variant { @Override public Protocol getProtocol() { return Protocol.HTTP_2; @@ -46,15 +46,18 @@ public final class Http20Draft09 implements Variant { static final byte TYPE_PUSH_PROMISE = 0x5; static final byte TYPE_PING = 0x6; static final byte TYPE_GOAWAY = 0x7; - static final byte TYPE_WINDOW_UPDATE = 0x9; - static final byte TYPE_CONTINUATION = 0xa; + static final byte TYPE_WINDOW_UPDATE = 0x8; + static final byte TYPE_CONTINUATION = 0x9; static final byte FLAG_NONE = 0x0; static final byte FLAG_ACK = 0x1; static final byte FLAG_END_STREAM = 0x1; + static final byte FLAG_END_SEGMENT = 0x2; static final byte FLAG_END_HEADERS = 0x4; // Used for headers and continuation. static final byte FLAG_END_PUSH_PROMISE = 0x4; static final byte FLAG_PRIORITY = 0x8; + static final byte FLAG_PAD_LOW = 0x10; + static final byte FLAG_PAD_HIGH = 0x20; @Override public FrameReader newReader(BufferedSource source, boolean client) { return new Reader(source, 4096, client); @@ -74,13 +77,13 @@ public final class Http20Draft09 implements Variant { private final boolean client; // Visible for testing. - final HpackDraft05.Reader hpackReader; + final HpackDraft06.Reader hpackReader; Reader(BufferedSource source, int headerTableSize, boolean client) { this.source = source; this.client = client; this.continuation = new ContinuationSource(this.source); - this.hpackReader = new HpackDraft05.Reader(client, headerTableSize, continuation); + this.hpackReader = new HpackDraft06.Reader(headerTableSize, continuation); } @Override public void readConnectionHeader() throws IOException { @@ -146,8 +149,7 @@ public final class Http20Draft09 implements Variant { break; default: - // Implementations MUST ignore frames of unsupported or unrecognized types. - source.skip(length); + throw ioException("PROTOCOL_ERROR: unknown frame type %s", type); } return true; } @@ -158,21 +160,27 @@ public final class Http20Draft09 implements Variant { boolean endStream = (flags & FLAG_END_STREAM) != 0; + short padding = readPadding(source, flags); + int priority = -1; if ((flags & FLAG_PRIORITY) != 0) { priority = source.readInt() & 0x7fffffff; length -= 4; // account for above read. } - List
headerBlock = readHeaderBlock(length, flags, streamId); + length = lengthWithoutPadding(length, flags, padding); + + List
headerBlock = readHeaderBlock(length, padding, flags, streamId); handler.headers(false, endStream, streamId, -1, priority, headerBlock, HeadersMode.HTTP_20_HEADERS); } - private List
readHeaderBlock(short length, byte flags, int streamId) + private List
readHeaderBlock(short length, short padding, byte flags, int streamId) throws IOException { + continuation.length = continuation.left = length; + continuation.padding = padding; continuation.flags = flags; continuation.streamId = streamId; @@ -187,7 +195,12 @@ public final class Http20Draft09 implements Variant { throws IOException { boolean inFinished = (flags & FLAG_END_STREAM) != 0; // TODO: checkState open or half-closed (local) or raise STREAM_CLOSED + + short padding = readPadding(source, flags); + length = lengthWithoutPadding(length, flags, padding); + handler.data(inFinished, streamId, source, length); + source.skip(padding); } private void readPriority(Handler handler, short length, byte flags, int streamId) @@ -221,13 +234,32 @@ public final class Http20Draft09 implements Variant { return; } - if (length % 8 != 0) throw ioException("TYPE_SETTINGS length %% 8 != 0: %s", length); + if (length % 5 != 0) throw ioException("TYPE_SETTINGS length %% 5 != 0: %s", length); Settings settings = new Settings(); - for (int i = 0; i < length; i += 8) { - int w1 = source.readInt(); + for (int i = 0; i < length; i += 5) { + int id = source.readByte(); int value = source.readInt(); - // int r = (w1 & 0xff000000) >>> 24; // Reserved. - int id = w1 & 0xffffff; + + switch (id) { + case 1: // SETTINGS_HEADER_TABLE_SIZE + break; + case 2: // SETTINGS_ENABLE_PUSH + if (value != 0 && value != 1) { + throw ioException("PROTOCOL_ERROR SETTINGS_ENABLE_PUSH != 0 or 1"); + } + break; + case 3: // SETTINGS_MAX_CONCURRENT_STREAMS + id = 4; // Renumbered in draft 10. + break; + case 4: // SETTINGS_INITIAL_WINDOW_SIZE + id = 7; // Renumbered in draft 10. + if (value < 0) { + throw ioException("PROTOCOL_ERROR SETTINGS_INITIAL_WINDOW_SIZE > 2^31 - 1"); + } + break; + default: + throw ioException("PROTOCOL_ERROR invalid settings id: %s", id); + } settings.set(id, 0, value); } handler.settings(false, settings); @@ -243,7 +275,8 @@ public final class Http20Draft09 implements Variant { } int promisedStreamId = source.readInt() & 0x7fffffff; length -= 4; // account for above read. - List
headerBlock = readHeaderBlock(length, flags, streamId); + short padding = 0; // no padding for push promise. + List
headerBlock = readHeaderBlock(length, padding, flags, streamId); handler.pushPromise(streamId, promisedStreamId, headerBlock); } @@ -292,14 +325,14 @@ public final class Http20Draft09 implements Variant { private final BufferedSink sink; private final boolean client; private final Buffer hpackBuffer; - private final HpackDraft05.Writer hpackWriter; + private final HpackDraft06.Writer hpackWriter; private boolean closed; Writer(BufferedSink sink, boolean client) { this.sink = sink; this.client = client; this.hpackBuffer = new Buffer(); - this.hpackWriter = new HpackDraft05.Writer(hpackBuffer); + this.hpackWriter = new HpackDraft06.Writer(hpackBuffer); } @Override public synchronized void flush() throws IOException { @@ -411,14 +444,17 @@ public final class Http20Draft09 implements Variant { @Override public synchronized void settings(Settings settings) throws IOException { if (closed) throw new IOException("closed"); - int length = settings.size() * 8; + int length = settings.size() * 5; byte type = TYPE_SETTINGS; byte flags = FLAG_NONE; int streamId = 0; frameHeader(length, type, flags, streamId); for (int i = 0; i < Settings.COUNT; i++) { if (!settings.isSet(i)) continue; - sink.writeInt(i & 0xffffff); + int id = i; + if (id == 4) id = 3; // SETTINGS_MAX_CONCURRENT_STREAMS renumbered. + else if (id == 7) id = 4; // SETTINGS_INITIAL_WINDOW_SIZE renumbered. + sink.writeByte(id); sink.writeInt(settings.get(i)); } sink.flush(); @@ -493,16 +529,17 @@ public final class Http20Draft09 implements Variant { /** * Decompression of the header block occurs above the framing layer. This * class lazily reads continuation frames as they are needed by {@link - * HpackDraft05.Reader#readHeaders()}. + * HpackDraft06.Reader#readHeaders()}. */ static final class ContinuationSource implements Source { private final BufferedSource source; - int length; + short length; byte flags; int streamId; - int left; + short left; + short padding; public ContinuationSource(BufferedSource source) { this.source = source; @@ -510,6 +547,8 @@ public final class Http20Draft09 implements Variant { @Override public long read(Buffer sink, long byteCount) throws IOException { while (left == 0) { + source.skip(padding); + padding = 0; if ((flags & FLAG_END_HEADERS) != 0) return -1; readContinuationHeader(); // TODO: test case for empty continuation header? @@ -532,12 +571,41 @@ public final class Http20Draft09 implements Variant { int previousStreamId = streamId; int w1 = source.readInt(); int w2 = source.readInt(); - length = left = (short) ((w1 & 0x3fff0000) >> 16); + length = (short) ((w1 & 0x3fff0000) >> 16); byte type = (byte) ((w1 & 0xff00) >> 8); flags = (byte) (w1 & 0xff); + padding = readPadding(source, flags); + length = left = lengthWithoutPadding(length, flags, padding); streamId = (w2 & 0x7fffffff); if (type != TYPE_CONTINUATION) throw ioException("%s != TYPE_CONTINUATION", type); if (streamId != previousStreamId) throw ioException("TYPE_CONTINUATION streamId changed"); } } + + private static short readPadding(BufferedSource source, byte flags) throws IOException { + if ((flags & FLAG_PAD_HIGH) != 0 && (flags & FLAG_PAD_LOW) == 0) { + throw ioException("PROTOCOL_ERROR FLAG_PAD_HIGH set without FLAG_PAD_LOW"); + } + int padding = 0; + if ((flags & FLAG_PAD_HIGH) != 0) { + padding = source.readShort() & 0xffff; + } else if ((flags & FLAG_PAD_LOW) != 0) { + padding = source.readByte() & 0xff; + } + if (padding > 16383) throw ioException("PROTOCOL_ERROR padding > 16383: %s", padding); + return (short) padding; + } + + private static short lengthWithoutPadding(short length, byte flags, short padding) + throws IOException { + if ((flags & FLAG_PAD_HIGH) != 0) { // account for reading the padding length. + length -= 2; + } else if ((flags & FLAG_PAD_LOW) != 0) { + length--; + } + if (padding > length) { + throw ioException("PROTOCOL_ERROR padding %s > remaining length %s", padding, length); + } + return (short) (length - padding); + } } diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Huffman.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Huffman.java index 45d882f6e..5b9c22f5c 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Huffman.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Huffman.java @@ -30,132 +30,172 @@ import okio.ByteString; * */ class Huffman { - enum Codec { - REQUEST(REQUEST_CODES, REQUEST_CODE_LENGTHS), - RESPONSE(RESPONSE_CODES, RESPONSE_CODE_LENGTHS); - private final Node root = new Node(); - private final int[] codes; - private final byte[] lengths; + // Appendix C: Huffman Codes + // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-06#appendix-C + private static final int[] CODES = { + 0x7ffffba, 0x7ffffbb, 0x7ffffbc, 0x7ffffbd, 0x7ffffbe, 0x7ffffbf, 0x7ffffc0, 0x7ffffc1, + 0x7ffffc2, 0x7ffffc3, 0x7ffffc4, 0x7ffffc5, 0x7ffffc6, 0x7ffffc7, 0x7ffffc8, 0x7ffffc9, + 0x7ffffca, 0x7ffffcb, 0x7ffffcc, 0x7ffffcd, 0x7ffffce, 0x7ffffcf, 0x7ffffd0, 0x7ffffd1, + 0x7ffffd2, 0x7ffffd3, 0x7ffffd4, 0x7ffffd5, 0x7ffffd6, 0x7ffffd7, 0x7ffffd8, 0x7ffffd9, 0xe8, + 0xffc, 0x3ffa, 0x7ffc, 0x7ffd, 0x24, 0x6e, 0x7ffe, 0x7fa, 0x7fb, 0x3fa, 0x7fc, 0xe9, 0x25, + 0x4, 0x0, 0x5, 0x6, 0x7, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x1ec, 0xea, 0x3fffe, 0x2d, + 0x1fffc, 0x1ed, 0x3ffb, 0x6f, 0xeb, 0xec, 0xed, 0xee, 0x70, 0x1ee, 0x1ef, 0x1f0, 0x1f1, 0x3fb, + 0x1f2, 0xef, 0x1f3, 0x1f4, 0x1f5, 0x1f6, 0x1f7, 0xf0, 0xf1, 0x1f8, 0x1f9, 0x1fa, 0x1fb, 0x1fc, + 0x3fc, 0x3ffc, 0x7ffffda, 0x1ffc, 0x3ffd, 0x2e, 0x7fffe, 0x8, 0x2f, 0x9, 0x30, 0x1, 0x31, + 0x32, 0x33, 0xa, 0x71, 0x72, 0xb, 0x34, 0xc, 0xd, 0xe, 0xf2, 0xf, 0x10, 0x11, 0x35, 0x73, + 0x36, 0xf3, 0xf4, 0xf5, 0x1fffd, 0x7fd, 0x1fffe, 0xffd, 0x7ffffdb, 0x7ffffdc, 0x7ffffdd, + 0x7ffffde, 0x7ffffdf, 0x7ffffe0, 0x7ffffe1, 0x7ffffe2, 0x7ffffe3, 0x7ffffe4, 0x7ffffe5, + 0x7ffffe6, 0x7ffffe7, 0x7ffffe8, 0x7ffffe9, 0x7ffffea, 0x7ffffeb, 0x7ffffec, 0x7ffffed, + 0x7ffffee, 0x7ffffef, 0x7fffff0, 0x7fffff1, 0x7fffff2, 0x7fffff3, 0x7fffff4, 0x7fffff5, + 0x7fffff6, 0x7fffff7, 0x7fffff8, 0x7fffff9, 0x7fffffa, 0x7fffffb, 0x7fffffc, 0x7fffffd, + 0x7fffffe, 0x7ffffff, 0x3ffff80, 0x3ffff81, 0x3ffff82, 0x3ffff83, 0x3ffff84, 0x3ffff85, + 0x3ffff86, 0x3ffff87, 0x3ffff88, 0x3ffff89, 0x3ffff8a, 0x3ffff8b, 0x3ffff8c, 0x3ffff8d, + 0x3ffff8e, 0x3ffff8f, 0x3ffff90, 0x3ffff91, 0x3ffff92, 0x3ffff93, 0x3ffff94, 0x3ffff95, + 0x3ffff96, 0x3ffff97, 0x3ffff98, 0x3ffff99, 0x3ffff9a, 0x3ffff9b, 0x3ffff9c, 0x3ffff9d, + 0x3ffff9e, 0x3ffff9f, 0x3ffffa0, 0x3ffffa1, 0x3ffffa2, 0x3ffffa3, 0x3ffffa4, 0x3ffffa5, + 0x3ffffa6, 0x3ffffa7, 0x3ffffa8, 0x3ffffa9, 0x3ffffaa, 0x3ffffab, 0x3ffffac, 0x3ffffad, + 0x3ffffae, 0x3ffffaf, 0x3ffffb0, 0x3ffffb1, 0x3ffffb2, 0x3ffffb3, 0x3ffffb4, 0x3ffffb5, + 0x3ffffb6, 0x3ffffb7, 0x3ffffb8, 0x3ffffb9, 0x3ffffba, 0x3ffffbb, 0x3ffffbc, 0x3ffffbd, + 0x3ffffbe, 0x3ffffbf, 0x3ffffc0, 0x3ffffc1, 0x3ffffc2, 0x3ffffc3, 0x3ffffc4, 0x3ffffc5, + 0x3ffffc6, 0x3ffffc7, 0x3ffffc8, 0x3ffffc9, 0x3ffffca, 0x3ffffcb, 0x3ffffcc, 0x3ffffcd, + 0x3ffffce, 0x3ffffcf, 0x3ffffd0, 0x3ffffd1, 0x3ffffd2, 0x3ffffd3, 0x3ffffd4, 0x3ffffd5, + 0x3ffffd6, 0x3ffffd7, 0x3ffffd8, 0x3ffffd9, 0x3ffffda, 0x3ffffdb + }; - /** - * @param codes Index designates the symbol this code represents. - * @param lengths Index designates the symbol this code represents. - */ - Codec(int[] codes, byte[] lengths) { - buildTree(codes, lengths); - this.codes = codes; - this.lengths = lengths; - } + private static final byte[] CODE_LENGTHS = { + 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, + 27, 27, 27, 27, 27, 27, 27, 27, 27, 8, 12, 14, 15, 15, 6, 7, 15, 11, 11, 10, 11, 8, 6, 5, 4, + 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 9, 8, 18, 6, 17, 9, 14, 7, 8, 8, 8, 8, 7, 9, 9, 9, 9, 10, 9, 8, + 9, 9, 9, 9, 9, 8, 8, 9, 9, 9, 9, 9, 10, 14, 27, 13, 14, 6, 19, 5, 6, 5, 6, 4, 6, 6, 6, 5, 7, + 7, 5, 6, 5, 5, 5, 8, 5, 5, 5, 6, 7, 6, 8, 8, 8, 17, 11, 17, 12, 27, 27, 27, 27, 27, 27, 27, + 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, + 27, 27, 27, 27, 27, 27, 27, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26 + }; - void encode(byte[] data, OutputStream out) throws IOException { - long current = 0; - int n = 0; + private static final Huffman INSTANCE = new Huffman(); - for (int i = 0; i < data.length; i++) { - int b = data[i] & 0xFF; - int code = codes[b]; - int nbits = lengths[b]; + public static Huffman get() { + return INSTANCE; + } - current <<= nbits; - current |= code; - n += nbits; + private final Node root = new Node(); - while (n >= 8) { - n -= 8; - out.write(((int) (current >> n))); - } - } + private Huffman() { + buildTree(); + } - if (n > 0) { - current <<= (8 - n); - current |= (0xFF >>> n); - out.write((int) current); + void encode(byte[] data, OutputStream out) throws IOException { + long current = 0; + int n = 0; + + for (int i = 0; i < data.length; i++) { + int b = data[i] & 0xFF; + int code = CODES[b]; + int nbits = CODE_LENGTHS[b]; + + current <<= nbits; + current |= code; + n += nbits; + + while (n >= 8) { + n -= 8; + out.write(((int) (current >> n))); } } - int encodedLength(byte[] bytes) { - long len = 0; + if (n > 0) { + current <<= (8 - n); + current |= (0xFF >>> n); + out.write((int) current); + } + } - for (int i = 0; i < bytes.length; i++) { - int b = bytes[i] & 0xFF; - len += lengths[b]; - } + int encodedLength(byte[] bytes) { + long len = 0; - return (int) ((len + 7) >> 3); + for (int i = 0; i < bytes.length; i++) { + int b = bytes[i] & 0xFF; + len += CODE_LENGTHS[b]; } - ByteString decode(ByteString buf) throws IOException { - return ByteString.of(decode(buf.toByteArray())); - } + return (int) ((len + 7) >> 3); + } - byte[] decode(byte[] buf) throws IOException { - // FIXME - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - Node node = root; - int current = 0; - int nbits = 0; - for (int i = 0; i < buf.length; i++) { - int b = buf[i] & 0xFF; - current = (current << 8) | b; - nbits += 8; - while (nbits >= 8) { - int c = (current >>> (nbits - 8)) & 0xFF; - node = node.children[c]; - if (node.children == null) { - // terminal node - baos.write(node.symbol); - nbits -= node.terminalBits; - node = root; - } else { - // non-terminal node - nbits -= 8; - } - } - } + ByteString decode(ByteString buf) throws IOException { + return ByteString.of(decode(buf.toByteArray())); + } - while (nbits > 0) { - int c = (current << (8 - nbits)) & 0xFF; + byte[] decode(byte[] buf) throws IOException { + // FIXME + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Node node = root; + int current = 0; + int nbits = 0; + for (int i = 0; i < buf.length; i++) { + int b = buf[i] & 0xFF; + current = (current << 8) | b; + nbits += 8; + while (nbits >= 8) { + int c = (current >>> (nbits - 8)) & 0xFF; node = node.children[c]; - if (node.children != null || node.terminalBits > nbits) { - break; + if (node.children == null) { + // terminal node + baos.write(node.symbol); + nbits -= node.terminalBits; + node = root; + } else { + // non-terminal node + nbits -= 8; } - baos.write(node.symbol); - nbits -= node.terminalBits; - node = root; - } - - return baos.toByteArray(); - } - - private void buildTree(int[] codes, byte[] lengths) { - for (int i = 0; i < lengths.length; i++) { - addCode(i, codes[i], lengths[i]); } } - private void addCode(int sym, int code, byte len) { - Node terminal = new Node(sym, len); - - Node current = root; - while (len > 8) { - len -= 8; - int i = ((code >>> len) & 0xFF); - if (current.children == null) { - throw new IllegalStateException("invalid dictionary: prefix not unique"); - } - if (current.children[i] == null) { - current.children[i] = new Node(); - } - current = current.children[i]; + while (nbits > 0) { + int c = (current << (8 - nbits)) & 0xFF; + node = node.children[c]; + if (node.children != null || node.terminalBits > nbits) { + break; } + baos.write(node.symbol); + nbits -= node.terminalBits; + node = root; + } - int shift = 8 - len; - int start = (code << shift) & 0xFF; - int end = 1 << shift; - for (int i = start; i < start + end; i++) { - current.children[i] = terminal; + return baos.toByteArray(); + } + + private void buildTree() { + for (int i = 0; i < CODE_LENGTHS.length; i++) { + addCode(i, CODES[i], CODE_LENGTHS[i]); + } + } + + private void addCode(int sym, int code, byte len) { + Node terminal = new Node(sym, len); + + Node current = root; + while (len > 8) { + len -= 8; + int i = ((code >>> len) & 0xFF); + if (current.children == null) { + throw new IllegalStateException("invalid dictionary: prefix not unique"); } + if (current.children[i] == null) { + current.children[i] = new Node(); + } + current = current.children[i]; + } + + int shift = 8 - len; + int start = (code << shift) & 0xFF; + int end = 1 << shift; + for (int i = start; i < start + end; i++) { + current.children[i] = terminal; } } @@ -190,95 +230,4 @@ class Huffman { this.terminalBits = b == 0 ? 8 : b; } } - - // Appendix C: Huffman Codes For Requests - // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-C - private static final int[] REQUEST_CODES = { - 0x7ffffba, 0x7ffffbb, 0x7ffffbc, 0x7ffffbd, 0x7ffffbe, 0x7ffffbf, 0x7ffffc0, 0x7ffffc1, - 0x7ffffc2, 0x7ffffc3, 0x7ffffc4, 0x7ffffc5, 0x7ffffc6, 0x7ffffc7, 0x7ffffc8, 0x7ffffc9, - 0x7ffffca, 0x7ffffcb, 0x7ffffcc, 0x7ffffcd, 0x7ffffce, 0x7ffffcf, 0x7ffffd0, 0x7ffffd1, - 0x7ffffd2, 0x7ffffd3, 0x7ffffd4, 0x7ffffd5, 0x7ffffd6, 0x7ffffd7, 0x7ffffd8, 0x7ffffd9, 0xe8, - 0xffc, 0x3ffa, 0x7ffc, 0x7ffd, 0x24, 0x6e, 0x7ffe, 0x7fa, 0x7fb, 0x3fa, 0x7fc, 0xe9, 0x25, - 0x4, 0x0, 0x5, 0x6, 0x7, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x1ec, 0xea, 0x3fffe, 0x2d, - 0x1fffc, 0x1ed, 0x3ffb, 0x6f, 0xeb, 0xec, 0xed, 0xee, 0x70, 0x1ee, 0x1ef, 0x1f0, 0x1f1, 0x3fb, - 0x1f2, 0xef, 0x1f3, 0x1f4, 0x1f5, 0x1f6, 0x1f7, 0xf0, 0xf1, 0x1f8, 0x1f9, 0x1fa, 0x1fb, 0x1fc, - 0x3fc, 0x3ffc, 0x7ffffda, 0x1ffc, 0x3ffd, 0x2e, 0x7fffe, 0x8, 0x2f, 0x9, 0x30, 0x1, 0x31, - 0x32, 0x33, 0xa, 0x71, 0x72, 0xb, 0x34, 0xc, 0xd, 0xe, 0xf2, 0xf, 0x10, 0x11, 0x35, 0x73, - 0x36, 0xf3, 0xf4, 0xf5, 0x1fffd, 0x7fd, 0x1fffe, 0xffd, 0x7ffffdb, 0x7ffffdc, 0x7ffffdd, - 0x7ffffde, 0x7ffffdf, 0x7ffffe0, 0x7ffffe1, 0x7ffffe2, 0x7ffffe3, 0x7ffffe4, 0x7ffffe5, - 0x7ffffe6, 0x7ffffe7, 0x7ffffe8, 0x7ffffe9, 0x7ffffea, 0x7ffffeb, 0x7ffffec, 0x7ffffed, - 0x7ffffee, 0x7ffffef, 0x7fffff0, 0x7fffff1, 0x7fffff2, 0x7fffff3, 0x7fffff4, 0x7fffff5, - 0x7fffff6, 0x7fffff7, 0x7fffff8, 0x7fffff9, 0x7fffffa, 0x7fffffb, 0x7fffffc, 0x7fffffd, - 0x7fffffe, 0x7ffffff, 0x3ffff80, 0x3ffff81, 0x3ffff82, 0x3ffff83, 0x3ffff84, 0x3ffff85, - 0x3ffff86, 0x3ffff87, 0x3ffff88, 0x3ffff89, 0x3ffff8a, 0x3ffff8b, 0x3ffff8c, 0x3ffff8d, - 0x3ffff8e, 0x3ffff8f, 0x3ffff90, 0x3ffff91, 0x3ffff92, 0x3ffff93, 0x3ffff94, 0x3ffff95, - 0x3ffff96, 0x3ffff97, 0x3ffff98, 0x3ffff99, 0x3ffff9a, 0x3ffff9b, 0x3ffff9c, 0x3ffff9d, - 0x3ffff9e, 0x3ffff9f, 0x3ffffa0, 0x3ffffa1, 0x3ffffa2, 0x3ffffa3, 0x3ffffa4, 0x3ffffa5, - 0x3ffffa6, 0x3ffffa7, 0x3ffffa8, 0x3ffffa9, 0x3ffffaa, 0x3ffffab, 0x3ffffac, 0x3ffffad, - 0x3ffffae, 0x3ffffaf, 0x3ffffb0, 0x3ffffb1, 0x3ffffb2, 0x3ffffb3, 0x3ffffb4, 0x3ffffb5, - 0x3ffffb6, 0x3ffffb7, 0x3ffffb8, 0x3ffffb9, 0x3ffffba, 0x3ffffbb, 0x3ffffbc, 0x3ffffbd, - 0x3ffffbe, 0x3ffffbf, 0x3ffffc0, 0x3ffffc1, 0x3ffffc2, 0x3ffffc3, 0x3ffffc4, 0x3ffffc5, - 0x3ffffc6, 0x3ffffc7, 0x3ffffc8, 0x3ffffc9, 0x3ffffca, 0x3ffffcb, 0x3ffffcc, 0x3ffffcd, - 0x3ffffce, 0x3ffffcf, 0x3ffffd0, 0x3ffffd1, 0x3ffffd2, 0x3ffffd3, 0x3ffffd4, 0x3ffffd5, - 0x3ffffd6, 0x3ffffd7, 0x3ffffd8, 0x3ffffd9, 0x3ffffda, 0x3ffffdb - }; - - private static final byte[] REQUEST_CODE_LENGTHS = { - 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, - 27, 27, 27, 27, 27, 27, 27, 27, 27, 8, 12, 14, 15, 15, 6, 7, 15, 11, 11, 10, 11, 8, 6, 5, 4, - 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 9, 8, 18, 6, 17, 9, 14, 7, 8, 8, 8, 8, 7, 9, 9, 9, 9, 10, 9, 8, - 9, 9, 9, 9, 9, 8, 8, 9, 9, 9, 9, 9, 10, 14, 27, 13, 14, 6, 19, 5, 6, 5, 6, 4, 6, 6, 6, 5, 7, - 7, 5, 6, 5, 5, 5, 8, 5, 5, 5, 6, 7, 6, 8, 8, 8, 17, 11, 17, 12, 27, 27, 27, 27, 27, 27, 27, - 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, - 27, 27, 27, 27, 27, 27, 27, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, - 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, - 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, - 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, - 26, 26, 26, 26, 26, 26, 26 - }; - - // Appendix D: Huffman Codes For Responses - // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-D - private static final int[] RESPONSE_CODES = { - 0x1ffffbc, 0x1ffffbd, 0x1ffffbe, 0x1ffffbf, 0x1ffffc0, 0x1ffffc1, 0x1ffffc2, 0x1ffffc3, - 0x1ffffc4, 0x1ffffc5, 0x1ffffc6, 0x1ffffc7, 0x1ffffc8, 0x1ffffc9, 0x1ffffca, 0x1ffffcb, - 0x1ffffcc, 0x1ffffcd, 0x1ffffce, 0x1ffffcf, 0x1ffffd0, 0x1ffffd1, 0x1ffffd2, 0x1ffffd3, - 0x1ffffd4, 0x1ffffd5, 0x1ffffd6, 0x1ffffd7, 0x1ffffd8, 0x1ffffd9, 0x1ffffda, 0x1ffffdb, 0x0, - 0xffa, 0x6a, 0x1ffa, 0x3ffc, 0x1ec, 0x3f8, 0x1ffb, 0x1ed, 0x1ee, 0xffb, 0x7fa, 0x22, 0x23, - 0x24, 0x6b, 0x1, 0x2, 0x3, 0x8, 0x9, 0xa, 0x25, 0x26, 0xb, 0xc, 0xd, 0x1ef, 0xfffa, 0x6c, - 0x1ffc, 0xffc, 0xfffb, 0x6d, 0xea, 0xeb, 0xec, 0xed, 0xee, 0x27, 0x1f0, 0xef, 0xf0, 0x3f9, - 0x1f1, 0x28, 0xf1, 0xf2, 0x1f2, 0x3fa, 0x1f3, 0x29, 0xe, 0x1f4, 0x1f5, 0xf3, 0x3fb, 0x1f6, - 0x3fc, 0x7fb, 0x1ffd, 0x7fc, 0x7ffc, 0x1f7, 0x1fffe, 0xf, 0x6e, 0x2a, 0x2b, 0x10, 0x6f, 0x70, - 0x71, 0x2c, 0x1f8, 0x1f9, 0x72, 0x2d, 0x2e, 0x2f, 0x30, 0x1fa, 0x31, 0x32, 0x33, 0x34, 0x73, - 0xf4, 0x74, 0xf5, 0x1fb, 0xfffc, 0x3ffd, 0xfffd, 0xfffe, 0x1ffffdc, 0x1ffffdd, 0x1ffffde, - 0x1ffffdf, 0x1ffffe0, 0x1ffffe1, 0x1ffffe2, 0x1ffffe3, 0x1ffffe4, 0x1ffffe5, 0x1ffffe6, - 0x1ffffe7, 0x1ffffe8, 0x1ffffe9, 0x1ffffea, 0x1ffffeb, 0x1ffffec, 0x1ffffed, 0x1ffffee, - 0x1ffffef, 0x1fffff0, 0x1fffff1, 0x1fffff2, 0x1fffff3, 0x1fffff4, 0x1fffff5, 0x1fffff6, - 0x1fffff7, 0x1fffff8, 0x1fffff9, 0x1fffffa, 0x1fffffb, 0x1fffffc, 0x1fffffd, 0x1fffffe, - 0x1ffffff, 0xffff80, 0xffff81, 0xffff82, 0xffff83, 0xffff84, 0xffff85, 0xffff86, 0xffff87, - 0xffff88, 0xffff89, 0xffff8a, 0xffff8b, 0xffff8c, 0xffff8d, 0xffff8e, 0xffff8f, 0xffff90, - 0xffff91, 0xffff92, 0xffff93, 0xffff94, 0xffff95, 0xffff96, 0xffff97, 0xffff98, 0xffff99, - 0xffff9a, 0xffff9b, 0xffff9c, 0xffff9d, 0xffff9e, 0xffff9f, 0xffffa0, 0xffffa1, 0xffffa2, - 0xffffa3, 0xffffa4, 0xffffa5, 0xffffa6, 0xffffa7, 0xffffa8, 0xffffa9, 0xffffaa, 0xffffab, - 0xffffac, 0xffffad, 0xffffae, 0xffffaf, 0xffffb0, 0xffffb1, 0xffffb2, 0xffffb3, 0xffffb4, - 0xffffb5, 0xffffb6, 0xffffb7, 0xffffb8, 0xffffb9, 0xffffba, 0xffffbb, 0xffffbc, 0xffffbd, - 0xffffbe, 0xffffbf, 0xffffc0, 0xffffc1, 0xffffc2, 0xffffc3, 0xffffc4, 0xffffc5, 0xffffc6, - 0xffffc7, 0xffffc8, 0xffffc9, 0xffffca, 0xffffcb, 0xffffcc, 0xffffcd, 0xffffce, 0xffffcf, - 0xffffd0, 0xffffd1, 0xffffd2, 0xffffd3, 0xffffd4, 0xffffd5, 0xffffd6, 0xffffd7, 0xffffd8, - 0xffffd9, 0xffffda, 0xffffdb, 0xffffdc - }; - - private static final byte[] RESPONSE_CODE_LENGTHS = { - 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, - 25, 25, 25, 25, 25, 25, 25, 25, 25, 4, 12, 7, 13, 14, 9, 10, 13, 9, 9, 12, 11, 6, 6, 6, 7, 4, - 4, 4, 5, 5, 5, 6, 6, 5, 5, 5, 9, 16, 7, 13, 12, 16, 7, 8, 8, 8, 8, 8, 6, 9, 8, 8, 10, 9, 6, 8, - 8, 9, 10, 9, 6, 5, 9, 9, 8, 10, 9, 10, 11, 13, 11, 15, 9, 17, 5, 7, 6, 6, 5, 7, 7, 7, 6, 9, 9, - 7, 6, 6, 6, 6, 9, 6, 6, 6, 6, 7, 8, 7, 8, 9, 16, 14, 16, 16, 25, 25, 25, 25, 25, 25, 25, 25, - 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, - 25, 25, 25, 25, 25, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, - 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, - 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, - 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, - 24, 24, 24, 24, 24, 24 - }; } diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java index bf430882e..d42b9eebc 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java @@ -38,11 +38,11 @@ final class Settings { /** spdy/3: Sender's estimate of max incoming kbps. */ static final int UPLOAD_BANDWIDTH = 1; - /** http/2: Size in bytes of the table used to decode the sender's header blocks. */ + /** HTTP/2: Size in bytes of the table used to decode the sender's header blocks. */ static final int HEADER_TABLE_SIZE = 1; /** spdy/3: Sender's estimate of max outgoing kbps. */ static final int DOWNLOAD_BANDWIDTH = 2; - /** http/2: An endpoint must not send a PUSH_PROMISE frame when this is 0. */ + /** HTTP/2: An endpoint must not send a PUSH_PROMISE frame when this is 0. */ static final int ENABLE_PUSH = 2; /** spdy/3: Sender's estimate of millis between sending a request and receiving a response. */ static final int ROUND_TRIP_TIME = 3; @@ -134,7 +134,7 @@ final class Settings { return (bit & set) != 0 ? values[UPLOAD_BANDWIDTH] : defaultValue; } - /** http/2 only. Returns -1 if unset. */ + /** HTTP/2 only. Returns -1 if unset. */ int getHeaderTableSize() { int bit = 1 << HEADER_TABLE_SIZE; return (bit & set) != 0 ? values[HEADER_TABLE_SIZE] : -1; @@ -146,8 +146,8 @@ final class Settings { return (bit & set) != 0 ? values[DOWNLOAD_BANDWIDTH] : defaultValue; } - /** http/2 only. */ - // TODO: honor this setting in http/2. + /** HTTP/2 only. */ + // TODO: honor this setting in HTTP/2. boolean getEnablePush(boolean defaultValue) { int bit = 1 << ENABLE_PUSH; return ((bit & set) != 0 ? values[ENABLE_PUSH] : defaultValue ? 1 : 0) == 1; @@ -159,7 +159,7 @@ final class Settings { return (bit & set) != 0 ? values[ROUND_TRIP_TIME] : defaultValue; } - // TODO: honor this setting in spdy/3 and http/2. + // TODO: honor this setting in spdy/3 and HTTP/2. int getMaxConcurrentStreams(int defaultValue) { int bit = 1 << MAX_CONCURRENT_STREAMS; return (bit & set) != 0 ? values[MAX_CONCURRENT_STREAMS] : defaultValue; @@ -188,7 +188,7 @@ final class Settings { return (bit & set) != 0 ? values[CLIENT_CERTIFICATE_VECTOR_SIZE] : defaultValue; } - // TODO: honor this setting in spdy/3 and http/2. + // TODO: honor this setting in spdy/3 and HTTP/2. boolean isFlowControlDisabled() { int bit = 1 << FLOW_CONTROL_OPTIONS; int value = (bit & set) != 0 ? values[FLOW_CONTROL_OPTIONS] : 0; diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java index 20ecc7cf5..cfc0526ae 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java @@ -143,7 +143,7 @@ public final class SpdyConnection implements Closeable { Variant variant; if (protocol == Protocol.HTTP_2) { - variant = new Http20Draft09(); + variant = new Http20Draft10(); } else if (protocol == Protocol.SPDY_3) { variant = new Spdy3(); } else {