diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java index 51ad1dfc7..29a98ec61 100644 --- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java +++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java @@ -74,12 +74,12 @@ public final class MockWebServer { 6, 's', 'p', 'd', 'y', '/', '3', 8, 'h', 't', 't', 'p', '/', '1', '.', '1' }; + private static final byte[] HTTP_20_DRAFT_09 = new byte[] { + 'H', 'T', 'T', 'P', '-', 'd', 'r', 'a', 'f', 't', '-', '0', '9', '/', '2', '.', '0' + }; private static final byte[] SPDY3 = new byte[] { 's', 'p', 'd', 'y', '/', '3' }; - private static final byte[] HTTP_20_DRAFT_06 = new byte[] { - 'H', 'T', 'T', 'P', '-', 'd', 'r', 'a', 'f', 't', '-', '0', '6', '/', '2', '.', '0' - }; private static final byte[] HTTP_11 = new byte[] { 'h', 't', 't', 'p', '/', '1', '.', '1' }; @@ -326,8 +326,8 @@ public final class MockWebServer { byte[] selectedProtocol = Platform.get().getNpnSelectedProtocol(sslSocket); if (selectedProtocol == null || Arrays.equals(selectedProtocol, HTTP_11)) { transport = Transport.HTTP_11; - } else if (Arrays.equals(selectedProtocol, HTTP_20_DRAFT_06)) { - transport = Transport.HTTP_20_DRAFT_06; + } else if (Arrays.equals(selectedProtocol, HTTP_20_DRAFT_09)) { + transport = Transport.HTTP_20_DRAFT_09; } else if (Arrays.equals(selectedProtocol, SPDY3)) { transport = Transport.SPDY_3; } else { @@ -340,14 +340,14 @@ public final class MockWebServer { socket = raw; } - if (transport == Transport.SPDY_3 || transport == Transport.HTTP_20_DRAFT_06) { + if (transport == Transport.HTTP_20_DRAFT_09 || transport == Transport.SPDY_3) { SpdySocketHandler spdySocketHandler = new SpdySocketHandler(socket, transport); SpdyConnection.Builder builder = new SpdyConnection.Builder(false, socket) .handler(spdySocketHandler); - if (transport == Transport.SPDY_3) { - builder.spdy3(); + if (transport == Transport.HTTP_20_DRAFT_09) { + builder.http20Draft09(); } else { - builder.http20Draft06(); + builder.spdy3(); } SpdyConnection spdyConnection = builder.build(); openSpdyConnections.put(spdyConnection, Boolean.TRUE); @@ -721,6 +721,6 @@ public final class MockWebServer { } enum Transport { - HTTP_11, SPDY_3, HTTP_20_DRAFT_06 + HTTP_11, SPDY_3, HTTP_20_DRAFT_09 } } diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Hpack.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/HpackDraft05.java similarity index 66% rename from okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Hpack.java rename to okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/HpackDraft05.java index c3ca8f116..c3f8d8e14 100644 --- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Hpack.java +++ b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/HpackDraft05.java @@ -7,25 +7,41 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; import java.util.List; +import java.util.ListIterator; /** - * Read and write HPACK v03. - * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03 + * Read and write HPACK v05. + * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05 */ -final class Hpack { +final class HpackDraft05 { - static class HeaderEntry { - private final String name; - private final String value; + // Visible for testing. + static class HeaderEntry implements Cloneable { + final String name; + final String value; + final int size; + boolean referenced = true; HeaderEntry(String name, String value) { this.name = name; this.value = value; + // TODO: This needs to be the size in bytes, not the length in chars. + this.size = 32 + name.length() + value.length(); } - // TODO: This needs to be the length in UTF-8 bytes, not the length in chars. - int length() { - return 32 + name.length() + value.length(); + /** Adds name and value, if this entry is referenced. */ + void addTo(List out) { + if (!referenced) return; + out.add(name); + out.add(value); + } + + @Override public HeaderEntry clone() { + try { + return (HeaderEntry) super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError(); + } } } @@ -34,98 +50,97 @@ final class Hpack { static final int PREFIX_7_BITS = 0x7f; static final int PREFIX_8_BITS = 0xff; - static final List INITIAL_CLIENT_TO_SERVER_HEADER_TABLE = Arrays.asList( + static final List STATIC_HEADER_TABLE = Arrays.asList( + new HeaderEntry(":authority", ""), + new HeaderEntry(":method", "GET"), + new HeaderEntry(":method", "POST"), + new HeaderEntry(":path", "/"), + new HeaderEntry(":path", "/index.html"), new HeaderEntry(":scheme", "http"), new HeaderEntry(":scheme", "https"), - new HeaderEntry(":host", ""), - new HeaderEntry(":path", "/"), - new HeaderEntry(":method", "GET"), - new HeaderEntry("accept", ""), + new HeaderEntry(":status", "200"), + new HeaderEntry(":status", "500"), + new HeaderEntry(":status", "404"), + new HeaderEntry(":status", "403"), + new HeaderEntry(":status", "400"), + new HeaderEntry(":status", "401"), new HeaderEntry("accept-charset", ""), new HeaderEntry("accept-encoding", ""), new HeaderEntry("accept-language", ""), - new HeaderEntry("cookie", ""), - new HeaderEntry("if-modified-since", ""), - new HeaderEntry("user-agent", ""), - new HeaderEntry("referer", ""), - new HeaderEntry("authorization", ""), - new HeaderEntry("allow", ""), - new HeaderEntry("cache-control", ""), - new HeaderEntry("connection", ""), - new HeaderEntry("content-length", ""), - new HeaderEntry("content-type", ""), - new HeaderEntry("date", ""), - new HeaderEntry("expect", ""), - new HeaderEntry("from", ""), - new HeaderEntry("if-match", ""), - new HeaderEntry("if-none-match", ""), - new HeaderEntry("if-range", ""), - new HeaderEntry("if-unmodified-since", ""), - new HeaderEntry("max-forwards", ""), - new HeaderEntry("proxy-authorization", ""), - new HeaderEntry("range", ""), - new HeaderEntry("via", "") - ); - - static final List INITIAL_SERVER_TO_CLIENT_HEADER_TABLE = Arrays.asList( - new HeaderEntry(":status", "200"), - new HeaderEntry("age", ""), - new HeaderEntry("cache-control", ""), - new HeaderEntry("content-length", ""), - new HeaderEntry("content-type", ""), - new HeaderEntry("date", ""), - new HeaderEntry("etag", ""), - new HeaderEntry("expires", ""), - new HeaderEntry("last-modified", ""), - new HeaderEntry("server", ""), - new HeaderEntry("set-cookie", ""), - new HeaderEntry("vary", ""), - new HeaderEntry("via", ""), - new HeaderEntry("access-control-allow-origin", ""), new HeaderEntry("accept-ranges", ""), + new HeaderEntry("accept", ""), + new HeaderEntry("access-control-allow-origin", ""), + new HeaderEntry("age", ""), new HeaderEntry("allow", ""), - new HeaderEntry("connection", ""), + new HeaderEntry("authorization", ""), + new HeaderEntry("cache-control", ""), new HeaderEntry("content-disposition", ""), new HeaderEntry("content-encoding", ""), new HeaderEntry("content-language", ""), + new HeaderEntry("content-length", ""), new HeaderEntry("content-location", ""), new HeaderEntry("content-range", ""), + new HeaderEntry("content-type", ""), + new HeaderEntry("cookie", ""), + new HeaderEntry("date", ""), + new HeaderEntry("etag", ""), + new HeaderEntry("expect", ""), + new HeaderEntry("expires", ""), + new HeaderEntry("from", ""), + new HeaderEntry("host", ""), + new HeaderEntry("if-match", ""), + new HeaderEntry("if-modified-since", ""), + new HeaderEntry("if-none-match", ""), + new HeaderEntry("if-range", ""), + new HeaderEntry("if-unmodified-since", ""), + new HeaderEntry("last-modified", ""), new HeaderEntry("link", ""), new HeaderEntry("location", ""), + new HeaderEntry("max-forwards", ""), new HeaderEntry("proxy-authenticate", ""), + new HeaderEntry("proxy-authorization", ""), + new HeaderEntry("range", ""), + new HeaderEntry("referer", ""), new HeaderEntry("refresh", ""), new HeaderEntry("retry-after", ""), + new HeaderEntry("server", ""), + new HeaderEntry("set-cookie", ""), new HeaderEntry("strict-transport-security", ""), new HeaderEntry("transfer-encoding", ""), + new HeaderEntry("user-agent", ""), + new HeaderEntry("vary", ""), + new HeaderEntry("via", ""), new HeaderEntry("www-authenticate", "") ); - // Update these when initial tables change to sum of each entry length. - static final int INITIAL_CLIENT_TO_SERVER_HEADER_TABLE_LENGTH = 1262; - static final int INITIAL_SERVER_TO_CLIENT_HEADER_TABLE_LENGTH = 1304; - - private Hpack() { + private HpackDraft05() { } + // TODO: huffman encoding! + // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#section-4.1.2 static class Reader { - private final long maxBufferSize = 4096; // TODO: needs to come from settings. private final DataInputStream in; - - private final BitSet referenceSet = new BitSet(); - private final List headerTable; private final List emittedHeaders = new ArrayList(); - private long bufferSize = 0; private long bytesLeft = 0; - Reader(DataInputStream in, boolean client) { + // Visible for testing. + final List headerTable = new ArrayList(); // TODO: default capacity? + final BitSet staticReferenceSet = new BitSet(); + long headerTableSize = 0; + long maxHeaderTableSize = 4096; // TODO: needs to come from SETTINGS_HEADER_TABLE_SIZE. + + Reader(DataInputStream in) { this.in = in; - if (client) { // we are reading from the server - this.headerTable = new ArrayList(INITIAL_SERVER_TO_CLIENT_HEADER_TABLE); - this.bufferSize = INITIAL_SERVER_TO_CLIENT_HEADER_TABLE_LENGTH; - } else { - this.headerTable = new ArrayList(INITIAL_CLIENT_TO_SERVER_HEADER_TABLE); - this.bufferSize = INITIAL_CLIENT_TO_SERVER_HEADER_TABLE_LENGTH; - } + } + + // Visible for testing. + void reset() { + bytesLeft = 0; + headerTableSize = 0; + maxHeaderTableSize = 4096; + staticReferenceSet.clear(); + headerTable.clear(); + emittedHeaders.clear(); } /** @@ -141,32 +156,42 @@ final class Hpack { if ((b & 0x80) != 0) { int index = readInt(b, PREFIX_7_BITS); - readIndexedHeader(index); - } else if (b == 0x60) { - readLiteralHeaderWithoutIndexingNewName(); - } else if ((b & 0xe0) == 0x60) { - int index = readInt(b, PREFIX_5_BITS); - readLiteralHeaderWithoutIndexingIndexedName(index - 1); + if (index == 0) { + emptyReferenceSet(); + } else { + readIndexedHeader(index - 1); + } } else if (b == 0x40) { - readLiteralHeaderWithIncrementalIndexingNewName(); + readLiteralHeaderWithoutIndexingNewName(); } else if ((b & 0xe0) == 0x40) { - int index = readInt(b, PREFIX_5_BITS); - readLiteralHeaderWithIncrementalIndexingIndexedName(index - 1); + int index = readInt(b, PREFIX_6_BITS); + readLiteralHeaderWithoutIndexingIndexedName(index - 1); } else if (b == 0) { - readLiteralHeaderWithSubstitutionIndexingNewName(); + readLiteralHeaderWithIncrementalIndexingNewName(); } else if ((b & 0xc0) == 0) { int index = readInt(b, PREFIX_6_BITS); - readLiteralHeaderWithSubstitutionIndexingIndexedName(index - 1); + readLiteralHeaderWithIncrementalIndexingIndexedName(index - 1); } else { throw new AssertionError(); } } } + private void emptyReferenceSet() { + staticReferenceSet.clear(); + for (HeaderEntry entry : headerTable) { + entry.referenced = false; + } + } + public void emitReferenceSet() { - for (int i = referenceSet.nextSetBit(0); i != -1; i = referenceSet.nextSetBit(i + 1)) { - emittedHeaders.add(getName(i)); - emittedHeaders.add(getValue(i)); + for (int i = staticReferenceSet.nextSetBit(0); i != -1; + i = staticReferenceSet.nextSetBit(i + 1)) { + STATIC_HEADER_TABLE.get(i).addTo(emittedHeaders); + } + for (ListIterator li = headerTable.listIterator(headerTable.size()); + li.hasPrevious(); ) { + li.previous().addTo(emittedHeaders); } } @@ -181,10 +206,17 @@ final class Hpack { } private void readIndexedHeader(int index) { - if (referenceSet.get(index)) { - referenceSet.clear(index); - } else { - referenceSet.set(index); + if (isStaticHeader(index)) { + if (maxHeaderTableSize == 0) { + staticReferenceSet.set(index - headerTable.size()); + } else { + HeaderEntry staticEntry = STATIC_HEADER_TABLE.get(index - headerTable.size()); + insertIntoHeaderTable(-1, staticEntry.clone()); + } + } else if (!headerTable.get(index).referenced) { + HeaderEntry existing = headerTable.get(index); + existing.referenced = true; + insertIntoHeaderTable(index, existing); } } @@ -208,77 +240,60 @@ final class Hpack { throws IOException { String name = getName(nameIndex); String value = readString(); - int index = headerTable.size(); // append to tail - insertIntoHeaderTable(index, new HeaderEntry(name, value)); + insertIntoHeaderTable(-1, new HeaderEntry(name, value)); } private void readLiteralHeaderWithIncrementalIndexingNewName() throws IOException { String name = readString(); String value = readString(); - int index = headerTable.size(); // append to tail - insertIntoHeaderTable(index, new HeaderEntry(name, value)); - } - - private void readLiteralHeaderWithSubstitutionIndexingIndexedName(int nameIndex) - throws IOException { - int index = readInt(readByte(), PREFIX_8_BITS); - String name = getName(nameIndex); - String value = readString(); - insertIntoHeaderTable(index, new HeaderEntry(name, value)); - } - - private void readLiteralHeaderWithSubstitutionIndexingNewName() throws IOException { - String name = readString(); - int index = readInt(readByte(), PREFIX_8_BITS); - String value = readString(); - insertIntoHeaderTable(index, new HeaderEntry(name, value)); + insertIntoHeaderTable(-1, new HeaderEntry(name, value)); } private String getName(int index) { - return headerTable.get(index).name; + if (isStaticHeader(index)) { + return STATIC_HEADER_TABLE.get(index - headerTable.size()).name; + } else { + return headerTable.get(index).name; + } } - private String getValue(int index) { - return headerTable.get(index).value; + private boolean isStaticHeader(int index) { + return index >= headerTable.size(); } + /** index == -1 when new. */ private void insertIntoHeaderTable(int index, HeaderEntry entry) { - int delta = entry.length(); - if (index != headerTable.size()) { - delta -= headerTable.get(index).length(); + int delta = entry.size; + if (index != -1) { // Index -1 == new header. + delta -= headerTable.get(index).size; } // if the new or replacement header is too big, drop all entries. - if (delta > maxBufferSize) { + if (delta > maxHeaderTableSize) { + staticReferenceSet.clear(); headerTable.clear(); - bufferSize = 0; + headerTableSize = 0; // emit the large header to the callback. - emittedHeaders.add(entry.name); - emittedHeaders.add(entry.value); + entry.addTo(emittedHeaders); return; } - // Prune headers to the required length. - while (bufferSize + delta > maxBufferSize) { - remove(0); - index--; + // Evict headers to the required length. + while (headerTableSize + delta > maxHeaderTableSize) { + remove(headerTable.size() - 1); } - if (index < 0) { // we pruned it, so insert at beginning - index = 0; - headerTable.add(index, entry); - } else if (index == headerTable.size()) { // append to the end - headerTable.add(index, entry); - } else { // replace value at same position + if (index == -1) { + headerTable.add(0, entry); + } else { // Replace value at same position. headerTable.set(index, entry); } - bufferSize += delta; - referenceSet.set(index); + headerTableSize += delta; } private void remove(int index) { - bufferSize -= headerTable.remove(index).length(); + headerTableSize -= headerTable.remove(index).size; } private int readByte() throws IOException { @@ -332,7 +347,7 @@ final class Hpack { public void writeHeaders(List nameValueBlock) throws IOException { // TODO: implement a compression strategy. for (int i = 0, size = nameValueBlock.size(); i < size; i += 2) { - out.write(0x60); // Literal Header without Indexing - New Name. + out.write(0x40); // Literal Header without Indexing - New Name. writeString(nameValueBlock.get(i)); writeString(nameValueBlock.get(i + 1)); } diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft06.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft09.java similarity index 82% rename from okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft06.java rename to okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft09.java index 3d53f4848..caaf8ab42 100644 --- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft06.java +++ b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft09.java @@ -25,12 +25,13 @@ import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.Arrays; import java.util.List; +import java.util.Locale; /** - * Read and write http/2 v06 frames. - * http://tools.ietf.org/html/draft-ietf-httpbis-http2-06 + * Read and write http/2 v09 frames. + * http://tools.ietf.org/html/draft-ietf-httpbis-http2-09 */ -final class Http20Draft06 implements Variant { +public final class Http20Draft09 implements Variant { private static final byte[] CONNECTION_HEADER; static { try { @@ -52,10 +53,11 @@ final class Http20Draft06 implements Variant { static final int TYPE_CONTINUATION = 0xa; static final int FLAG_END_STREAM = 0x1; + /** Used for headers, push-promise and continuation. */ static final int FLAG_END_HEADERS = 0x4; static final int FLAG_PRIORITY = 0x8; - static final int FLAG_PONG = 0x1; + static final int FLAG_ACK = 0x1; static final int FLAG_END_FLOW_CONTROL = 0x1; @Override public FrameReader newReader(InputStream in, boolean client) { @@ -69,12 +71,14 @@ final class Http20Draft06 implements Variant { static final class Reader implements FrameReader { private final DataInputStream in; private final boolean client; - private final Hpack.Reader hpackReader; + + // Visible for testing. + final HpackDraft05.Reader hpackReader; Reader(InputStream in, boolean client) { this.in = new DataInputStream(in); this.client = client; - this.hpackReader = new Hpack.Reader(this.in, client); + this.hpackReader = new HpackDraft05.Reader(this.in); } @Override public void readConnectionHeader() throws IOException { @@ -96,7 +100,8 @@ final class Http20Draft06 implements Variant { } int w2 = in.readInt(); - int length = (w1 & 0xffff0000) >> 16; + // boolean r = (w1 & 0xc0000000) != 0; // Reserved. + int length = (w1 & 0x3fff0000) >> 16; // 14-bit unsigned. int type = (w1 & 0xff00) >> 8; int flags = w1 & 0xff; // boolean r = (w2 & 0x80000000) != 0; // Reserved. @@ -155,6 +160,10 @@ final class Http20Draft06 implements Variant { if ((flags & FLAG_END_HEADERS) != 0) { hpackReader.emitReferenceSet(); List namesAndValues = hpackReader.getAndReset(); + // TODO: throw malformed if any present: + // Connection, Keep-Alive, Proxy-Connection, TE, Transfer-Encoding, Encoding, Upgrade. + // TODO: Concat multi-value headers with 0x0, except COOKIE, which uses 0x3B, 0x20. + // http://tools.ietf.org/html/draft-ietf-httpbis-http2-09#section-8.1.3 int priority = -1; // TODO: priority handler.headers(false, inFinished, streamId, -1, priority, namesAndValues, HeadersMode.HTTP_20_HEADERS); @@ -165,13 +174,11 @@ final class Http20Draft06 implements Variant { int w1 = in.readInt(); int w2 = in.readInt(); - length = (w1 & 0xffff0000) >> 16; + // boolean r = (w1 & 0xc0000000) != 0; // Reserved. + length = (w1 & 0x3fff0000) >> 16; // 14-bit unsigned. int newType = (w1 & 0xff00) >> 8; flags = w1 & 0xff; - // TODO: remove in draft 8: CONTINUATION no longer sets END_STREAM - inFinished = (flags & FLAG_END_STREAM) != 0; - // boolean u = (w2 & 0x80000000) != 0; // Unused. int newStreamId = (w2 & 0x7fffffff); @@ -184,6 +191,7 @@ final class Http20Draft06 implements Variant { private void readData(Handler handler, int flags, int length, int streamId) throws IOException { boolean inFinished = (flags & FLAG_END_STREAM) != 0; + // TODO: checkState open or half-closed (local) or raise STREAM_CLOSED handler.data(inFinished, streamId, in, length); } @@ -211,6 +219,13 @@ final class Http20Draft06 implements Variant { private void readSettings(Handler handler, int flags, int length, int streamId) throws IOException { + if ((flags & FLAG_ACK) != 0) { + if (length != 0) throw ioException("FRAME_SIZE_ERROR ack frame should be empty!"); + // TODO: signal apply changes + // http://tools.ietf.org/html/draft-ietf-httpbis-http2-09#section-6.5.3 + return; + } + if (length % 8 != 0) throw ioException("TYPE_SETTINGS length %% 8 != 0: %s", length); if (streamId != 0) throw ioException("TYPE_SETTINGS streamId != 0"); Settings settings = new Settings(); @@ -233,7 +248,7 @@ final class Http20Draft06 implements Variant { if (streamId != 0) throw ioException("TYPE_PING streamId != 0"); int payload1 = in.readInt(); int payload2 = in.readInt(); - boolean reply = (flags & FLAG_PONG) != 0; + boolean reply = (flags & FLAG_ACK) != 0; handler.ping(reply, payload1, payload2); } @@ -262,10 +277,6 @@ final class Http20Draft06 implements Variant { handler.windowUpdate(streamId, windowSizeIncrement, endFlowControl); } - private static IOException ioException(String message, Object... args) throws IOException { - throw new IOException(String.format(message, args)); - } - @Override public void close() throws IOException { in.close(); } @@ -275,13 +286,13 @@ final class Http20Draft06 implements Variant { private final DataOutputStream out; private final boolean client; private final ByteArrayOutputStream hpackBuffer; - private final Hpack.Writer hpackWriter; + private final HpackDraft05.Writer hpackWriter; Writer(OutputStream out, boolean client) { this.out = new DataOutputStream(out); this.client = client; this.hpackBuffer = new ByteArrayOutputStream(); - this.hpackWriter = new Hpack.Writer(hpackBuffer); + this.hpackWriter = new HpackDraft05.Writer(hpackBuffer); } @Override public synchronized void flush() throws IOException { @@ -313,14 +324,23 @@ final class Http20Draft06 implements Variant { private void headers(boolean outFinished, int streamId, int priority, List nameValueBlock) throws IOException { hpackBuffer.reset(); + for (int i = 0, size = nameValueBlock.size(); i < size; i += 2) { + String headerName = nameValueBlock.get(i).toLowerCase(Locale.US); + // our SpdyTransport.writeNameValueBlock hard-codes :host + if (":host".equals(headerName)) headerName = ":authority"; + nameValueBlock.set(i, headerName); + } + // TODO: throw malformed if any present: + // Connection, Keep-Alive, Proxy-Connection, TE, Transfer-Encoding, Encoding, Upgrade. hpackWriter.writeHeaders(nameValueBlock); int type = TYPE_HEADERS; // TODO: implement CONTINUATION int length = hpackBuffer.size(); + checkFrameSize(length); int flags = FLAG_END_HEADERS; if (outFinished) flags |= FLAG_END_STREAM; if (priority != -1) flags |= FLAG_PRIORITY; - out.writeInt((length & 0xffff) << 16 | (type & 0xff) << 8 | (flags & 0xff)); + out.writeInt((length & 0x3fff) << 16 | (type & 0xff) << 8 | (flags & 0xff)); out.writeInt(streamId & 0x7fffffff); if (priority != -1) out.writeInt(priority & 0x7fffffff); hpackBuffer.writeTo(out); @@ -328,7 +348,14 @@ final class Http20Draft06 implements Variant { @Override public synchronized void rstStream(int streamId, ErrorCode errorCode) throws IOException { - throw new UnsupportedOperationException("TODO"); + if (errorCode.spdyRstCode == -1) throw new IllegalArgumentException(); + int flags = 0; + int type = TYPE_RST_STREAM; + int length = 4; + out.writeInt((length & 0x3fff) << 16 | (type & 0xff) << 8 | (flags & 0xff)); + out.writeInt(streamId & 0x7fffffff); + out.writeInt(errorCode.spdyRstCode); + out.flush(); } @Override public void data(boolean outFinished, int streamId, byte[] data) throws IOException { @@ -340,7 +367,8 @@ final class Http20Draft06 implements Variant { int type = TYPE_DATA; int flags = 0; if (outFinished) flags |= FLAG_END_STREAM; - out.writeInt((byteCount & 0xffff) << 16 | (type & 0xff) << 8 | (flags & 0xff)); + checkFrameSize(byteCount); + out.writeInt((byteCount & 0x3fff) << 16 | (type & 0xff) << 8 | (flags & 0xff)); out.writeInt(streamId & 0x7fffffff); out.write(data, offset, byteCount); } @@ -350,7 +378,7 @@ final class Http20Draft06 implements Variant { int length = settings.size() * 8; int flags = 0; int streamId = 0; - out.writeInt((length & 0xffff) << 16 | (type & 0xff) << 8 | (flags & 0xff)); + out.writeInt((length & 0x3fff) << 16 | (type & 0xff) << 8 | (flags & 0xff)); out.writeInt(streamId & 0x7fffffff); for (int i = 0; i < Settings.COUNT; i++) { if (!settings.isSet(i)) continue; @@ -382,4 +410,12 @@ final class Http20Draft06 implements Variant { out.close(); } } + + private static void checkFrameSize(int bytes) throws IOException { + if (bytes > 16383) throw ioException("FRAME_SIZE_ERROR max size is 16383: %s", bytes); + } + + private static IOException ioException(String message, Object... args) throws IOException { + throw new IOException(String.format(message, args)); + } } diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java index 05380e27e..f5b3a1517 100644 --- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java +++ b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java @@ -17,10 +17,14 @@ package com.squareup.okhttp.internal.spdy; final class Settings { /** - * From the spdy/3 spec, the default initial window size for all streams is - * 64 KiB. (Chrome 25 uses 10 MiB). + * spdy/3: The default initial window size for all streams is 64 KiB. (Chrome + * 25 uses 10 MiB). */ static final int DEFAULT_INITIAL_WINDOW_SIZE = 64 * 1024; + /** http/2: The default header compression table size is 4 KiB. */ + static final int DEFAULT_HEADER_TABLE_SIZE = 4096; + /** http/2: The default is to enable PUSH_PROMISE frames. */ + static final int DEFAULT_ENABLE_PUSH = 1; /** Peer request to clear durable settings. */ static final int FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS = 0x1; @@ -30,24 +34,28 @@ final class Settings { /** Sent by clients only. The client is reminding the server of a persisted value. */ static final int PERSISTED = 0x2; - /** Sender's estimate of max incoming kbps. */ + /** spdy/3: Sender's estimate of max incoming kbps. */ static final int UPLOAD_BANDWIDTH = 1; - /** Sender's estimate of max outgoing kbps. */ + /** 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; - /** Sender's estimate of milliseconds between sending a request and receiving a response. */ + /** http/2: An endpoint must not send a PUSH_PROMISE frame 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; /** Sender's maximum number of concurrent streams. */ static final int MAX_CONCURRENT_STREAMS = 4; - /** Current CWND in Packets. */ + /** spdy/3: Current CWND in Packets. */ static final int CURRENT_CWND = 5; - /** Retransmission rate. Percentage */ + /** spdy/3: Retransmission rate. Percentage */ static final int DOWNLOAD_RETRANS_RATE = 6; /** Window size in bytes. */ static final int INITIAL_WINDOW_SIZE = 7; - /** Window size in bytes. */ + /** spdy/3: Window size in bytes. */ static final int CLIENT_CERTIFICATE_VECTOR_SIZE = 8; /** Flow control options. */ - static final int FLOW_CONTROL_OPTIONS = 9; + static final int FLOW_CONTROL_OPTIONS = 10; /** Total number of settings. */ static final int COUNT = 10; @@ -112,47 +120,69 @@ final class Settings { return Integer.bitCount(set); } + /** spdy/3 only. */ int getUploadBandwidth(int defaultValue) { int bit = 1 << UPLOAD_BANDWIDTH; return (bit & set) != 0 ? values[UPLOAD_BANDWIDTH] : defaultValue; } + /** http/2 only. */ + // TODO: honor this setting in http/2. + int getHeaderTableSize() { + int bit = 1 << HEADER_TABLE_SIZE; + return (bit & set) != 0 ? values[HEADER_TABLE_SIZE] : DEFAULT_HEADER_TABLE_SIZE; + } + + /** spdy/3 only. */ int getDownloadBandwidth(int defaultValue) { int bit = 1 << DOWNLOAD_BANDWIDTH; return (bit & set) != 0 ? values[DOWNLOAD_BANDWIDTH] : defaultValue; } + /** http/2 only. */ + // TODO: honor this setting in http/2. + boolean getEnablePush() { + int bit = 1 << ENABLE_PUSH; + return ((bit & set) != 0 ? values[ENABLE_PUSH] : DEFAULT_ENABLE_PUSH) == 1; + } + + /** spdy/3 only. */ int getRoundTripTime(int defaultValue) { int bit = 1 << ROUND_TRIP_TIME; return (bit & set) != 0 ? values[ROUND_TRIP_TIME] : defaultValue; } + // 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; } + /** spdy/3 only. */ int getCurrentCwnd(int defaultValue) { int bit = 1 << CURRENT_CWND; return (bit & set) != 0 ? values[CURRENT_CWND] : defaultValue; } + /** spdy/3 only. */ int getDownloadRetransRate(int defaultValue) { int bit = 1 << DOWNLOAD_RETRANS_RATE; return (bit & set) != 0 ? values[DOWNLOAD_RETRANS_RATE] : defaultValue; } + // TODO: honor this setting in http/2. int getInitialWindowSize(int defaultValue) { int bit = 1 << INITIAL_WINDOW_SIZE; return (bit & set) != 0 ? values[INITIAL_WINDOW_SIZE] : defaultValue; } + /** spdy/3 only. */ int getClientCertificateVectorSize(int defaultValue) { int bit = 1 << CLIENT_CERTIFICATE_VECTOR_SIZE; return (bit & set) != 0 ? values[CLIENT_CERTIFICATE_VECTOR_SIZE] : defaultValue; } - // TODO: honor this setting. + // 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; @@ -160,7 +190,7 @@ final class Settings { } /** - * Returns true if this user agent should use this setting in future SPDY + * Returns true if this user agent should use this setting in future spdy/3 * connections to the same host. */ boolean persistValue(int id) { diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java index 41724f078..b1fe4e065 100644 --- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java +++ b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java @@ -59,7 +59,7 @@ public final class SpdyConnection implements Closeable { Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue(), Util.daemonThreadFactory("OkHttp SpdyConnection")); - /** The protocol variant, like SPDY/3 or HTTP-draft-06/2.0. */ + /** The protocol variant, like SPDY/3 or HTTP-draft-09/2.0. */ final Variant variant; /** True if this peer initiated the connection. */ @@ -151,7 +151,7 @@ public final class SpdyConnection implements Closeable { boolean outFinished = !out; boolean inFinished = !in; int associatedStreamId = 0; // TODO: permit the caller to specify an associated stream? - int priority = 0; // TODO: permit the caller to specify a priority? + int priority = -1; // TODO: permit the caller to specify a priority? int slot = 0; // TODO: permit the caller to specify a slot? SpdyStream stream; int streamId; @@ -416,8 +416,8 @@ public final class SpdyConnection implements Closeable { return this; } - public Builder http20Draft06() { - this.variant = Variant.HTTP_20_DRAFT_06; + public Builder http20Draft09() { + this.variant = Variant.HTTP_20_DRAFT_09; return this; } diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Variant.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Variant.java index 8f48bcd24..72f8c91d2 100644 --- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Variant.java +++ b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Variant.java @@ -21,7 +21,7 @@ import java.io.OutputStream; /** A version and dialect of the framed socket protocol. */ interface Variant { Variant SPDY3 = new Spdy3(); - Variant HTTP_20_DRAFT_06 = new Http20Draft06(); + Variant HTTP_20_DRAFT_09 = new Http20Draft09(); /** * @param client true if this is the HTTP client's reader, reading frames from diff --git a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/HpackDraft05Test.java b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/HpackDraft05Test.java new file mode 100644 index 000000000..43fff2cdd --- /dev/null +++ b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/HpackDraft05Test.java @@ -0,0 +1,496 @@ +/* + * 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 java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import org.junit.After; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class HpackDraft05Test { + + private final MutableByteArrayInputStream bytesIn = new MutableByteArrayInputStream(); + private final HpackDraft05.Reader hpackReader = new HpackDraft05.Reader(new DataInputStream(bytesIn)); + + @After public void resetReader(){ + hpackReader.reset(); + } + + /** + * HPACK has a max header table size, which can be smaller than the max header message. + * Ensure the larger header content is not lost. + */ + @Test public void tooLargeToHPackIsStillEmitted() throws IOException { + char[] tooLarge = new char[4096]; + Arrays.fill(tooLarge, 'a'); + final List sentHeaders = Arrays.asList("foo", new String(tooLarge)); + + bytesIn.set(literalHeaders(sentHeaders)); + hpackReader.readHeaders(bytesIn.available()); + hpackReader.emitReferenceSet(); + + assertEquals(0, hpackReader.headerTable.size()); + + assertEquals(sentHeaders, hpackReader.getAndReset()); + } + + /** + * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.1.1 + */ + @Test public void decodeLiteralHeaderFieldWithIndexing() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + out.write(0x00); // Literal indexed + out.write(0x0a); // Literal name (len = 10) + out.write("custom-key".getBytes(), 0, 10); + + out.write(0x0d); // Literal value (len = 13) + out.write("custom-header".getBytes(), 0, 13); + + bytesIn.set(out.toByteArray()); + hpackReader.readHeaders(bytesIn.available()); + hpackReader.emitReferenceSet(); + + assertEquals(1, hpackReader.headerTable.size()); + + HpackDraft05.HeaderEntry entry = hpackReader.headerTable.get(0); + assertEquals("custom-key", entry.name); + assertEquals("custom-header", entry.value); + assertEquals(55, entry.size); + assertEquals(entry.size, hpackReader.headerTableSize); + + assertEquals(Arrays.asList("custom-key", "custom-header"), hpackReader.getAndReset()); + } + + /** + * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.1.2 + */ + @Test public void decodeLiteralHeaderFieldWithoutIndexingIndexedName() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + out.write(0x44); // == Literal not indexed == + // Indexed name (idx = 4) -> :path + out.write(0x0c); // Literal value (len = 12) + out.write("/sample/path".getBytes(), 0, 12); + + bytesIn.set(out.toByteArray()); + hpackReader.readHeaders(bytesIn.available()); + hpackReader.emitReferenceSet(); + + assertEquals(0, hpackReader.headerTable.size()); + + assertEquals(Arrays.asList(":path", "/sample/path"), hpackReader.getAndReset()); + } + + /** + * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.1.3 + */ + @Test public void decodeIndexedHeaderField() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + out.write(0x82); // == Indexed - Add == + // idx = 2 -> :method: GET + + bytesIn.set(out.toByteArray()); + hpackReader.readHeaders(bytesIn.available()); + hpackReader.emitReferenceSet(); + + assertEquals(1, hpackReader.headerTable.size()); + + HpackDraft05.HeaderEntry entry = hpackReader.headerTable.get(0); + assertEquals(":method", entry.name); + assertEquals("GET", entry.value); + assertEquals(42, entry.size); + assertEquals(entry.size, hpackReader.headerTableSize); + + assertEquals(Arrays.asList(":method", "GET"), hpackReader.getAndReset()); + } + + /** + * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.1.4 + */ + @Test public void decodeIndexedHeaderFieldFromStaticTableWithoutBuffering() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + out.write(0x82); // == Indexed - Add == + // idx = 2 -> :method: GET + + bytesIn.set(out.toByteArray()); + hpackReader.maxHeaderTableSize = 0; // SETTINGS_HEADER_TABLE_SIZE == 0 + hpackReader.readHeaders(bytesIn.available()); + hpackReader.emitReferenceSet(); + + // Not buffered in header table. + assertEquals(0, hpackReader.headerTable.size()); + + assertEquals(Arrays.asList(":method", "GET"), hpackReader.getAndReset()); + } + + /** + * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.2 + */ + @Test public void decodeRequestExamplesWithoutHuffman() throws IOException { + bytesIn.set(firstRequestWithoutHuffman()); + hpackReader.readHeaders(bytesIn.available()); + hpackReader.emitReferenceSet(); + checkFirstRequestWithoutHuffman(); + + bytesIn.set(secondRequestWithoutHuffman()); + hpackReader.readHeaders(bytesIn.available()); + hpackReader.emitReferenceSet(); + checkSecondRequestWithoutHuffman(); + + bytesIn.set(thirdRequestWithoutHuffman()); + hpackReader.readHeaders(bytesIn.available()); + hpackReader.emitReferenceSet(); + checkThirdRequestWithoutHuffman(); + } + + private byte[] firstRequestWithoutHuffman() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + out.write(0x82); // == Indexed - Add == + // idx = 2 -> :method: GET + out.write(0x87); // == Indexed - Add == + // idx = 7 -> :scheme: http + out.write(0x86); // == Indexed - Add == + // idx = 6 -> :path: / + out.write(0x04); // == Literal indexed == + // Indexed name (idx = 4) -> :authority + out.write(0x0f); // Literal value (len = 15) + out.write("www.example.com".getBytes(), 0, 15); + + return out.toByteArray(); + } + + private void checkFirstRequestWithoutHuffman() { + assertEquals(4, hpackReader.headerTable.size()); + + // [ 1] (s = 57) :authority: www.example.com + HpackDraft05.HeaderEntry entry = hpackReader.headerTable.get(0); + assertEquals(":authority", entry.name); + assertEquals("www.example.com", entry.value); + assertEquals(57, entry.size); + assertTrue(entry.referenced); + + // [ 2] (s = 38) :path: / + entry = hpackReader.headerTable.get(1); + assertEquals(":path", entry.name); + assertEquals("/", entry.value); + assertEquals(38, entry.size); + assertTrue(entry.referenced); + + // [ 3] (s = 43) :scheme: http + entry = hpackReader.headerTable.get(2); + assertEquals(":scheme", entry.name); + assertEquals("http", entry.value); + assertEquals(43, entry.size); + assertTrue(entry.referenced); + + // [ 4] (s = 42) :method: GET + entry = hpackReader.headerTable.get(3); + assertEquals(":method", entry.name); + assertEquals("GET", entry.value); + assertEquals(42, entry.size); + assertTrue(entry.referenced); + + // Table size: 180 + assertEquals(180, hpackReader.headerTableSize); + + // Decoded header set: + assertEquals(Arrays.asList( // + ":method", "GET", // + ":scheme", "http", // + ":path", "/", // + ":authority", "www.example.com"), hpackReader.getAndReset()); + } + + private byte[] secondRequestWithoutHuffman() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + out.write(0x1b); // == Literal indexed == + // Indexed name (idx = 27) -> cache-control + out.write(0x08); // Literal value (len = 8) + out.write("no-cache".getBytes(), 0, 8); + + return out.toByteArray(); + } + + private void checkSecondRequestWithoutHuffman() { + assertEquals(5, hpackReader.headerTable.size()); + + // [ 1] (s = 53) cache-control: no-cache + HpackDraft05.HeaderEntry entry = hpackReader.headerTable.get(0); + assertEquals("cache-control", entry.name); + assertEquals("no-cache", entry.value); + assertEquals(53, entry.size); + assertTrue(entry.referenced); + + // [ 2] (s = 57) :authority: www.example.com + entry = hpackReader.headerTable.get(1); + assertEquals(":authority", entry.name); + assertEquals("www.example.com", entry.value); + assertEquals(57, entry.size); + assertTrue(entry.referenced); + + // [ 3] (s = 38) :path: / + entry = hpackReader.headerTable.get(2); + assertEquals(":path", entry.name); + assertEquals("/", entry.value); + assertEquals(38, entry.size); + assertTrue(entry.referenced); + + // [ 4] (s = 43) :scheme: http + entry = hpackReader.headerTable.get(3); + assertEquals(":scheme", entry.name); + assertEquals("http", entry.value); + assertEquals(43, entry.size); + assertTrue(entry.referenced); + + // [ 5] (s = 42) :method: GET + entry = hpackReader.headerTable.get(4); + assertEquals(":method", entry.name); + assertEquals("GET", entry.value); + assertEquals(42, entry.size); + assertTrue(entry.referenced); + + // Table size: 233 + assertEquals(233, hpackReader.headerTableSize); + + // Decoded header set: + assertEquals(Arrays.asList( // + ":method", "GET", // + ":scheme", "http", // + ":path", "/", // + ":authority", "www.example.com", // + "cache-control", "no-cache"), hpackReader.getAndReset()); + } + + private byte[] thirdRequestWithoutHuffman() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + out.write(0x80); // == Empty reference set == + out.write(0x85); // == Indexed - Add == + // idx = 5 -> :method: GET + out.write(0x8c); // == Indexed - Add == + // idx = 12 -> :scheme: https + out.write(0x8b); // == Indexed - Add == + // idx = 11 -> :path: /index.html + out.write(0x84); // == Indexed - Add == + // idx = 4 -> :authority: www.example.com + out.write(0x00); // Literal indexed + out.write(0x0a); // Literal name (len = 10) + out.write("custom-key".getBytes(), 0, 10); + out.write(0x0c); // Literal value (len = 12) + out.write("custom-value".getBytes(), 0, 12); + + return out.toByteArray(); + } + + private void checkThirdRequestWithoutHuffman() { + assertEquals(8, hpackReader.headerTable.size()); + + // [ 1] (s = 54) custom-key: custom-value + HpackDraft05.HeaderEntry entry = hpackReader.headerTable.get(0); + assertEquals("custom-key", entry.name); + assertEquals("custom-value", entry.value); + assertEquals(54, entry.size); + assertTrue(entry.referenced); + + // [ 2] (s = 48) :path: /index.html + entry = hpackReader.headerTable.get(1); + assertEquals(":path", entry.name); + assertEquals("/index.html", entry.value); + assertEquals(48, entry.size); + assertTrue(entry.referenced); + + // [ 3] (s = 44) :scheme: https + entry = hpackReader.headerTable.get(2); + assertEquals(":scheme", entry.name); + assertEquals("https", entry.value); + assertEquals(44, entry.size); + assertTrue(entry.referenced); + + // [ 4] (s = 53) cache-control: no-cache + entry = hpackReader.headerTable.get(3); + assertEquals("cache-control", entry.name); + assertEquals("no-cache", entry.value); + assertEquals(53, entry.size); + assertFalse(entry.referenced); + + // [ 5] (s = 57) :authority: www.example.com + entry = hpackReader.headerTable.get(4); + assertEquals(":authority", entry.name); + assertEquals("www.example.com", entry.value); + assertEquals(57, entry.size); + assertTrue(entry.referenced); + + // [ 6] (s = 38) :path: / + entry = hpackReader.headerTable.get(5); + assertEquals(":path", entry.name); + assertEquals("/", entry.value); + assertEquals(38, entry.size); + assertFalse(entry.referenced); + + // [ 7] (s = 43) :scheme: http + entry = hpackReader.headerTable.get(6); + assertEquals(":scheme", entry.name); + assertEquals("http", entry.value); + assertEquals(43, entry.size); + assertFalse(entry.referenced); + + // [ 8] (s = 42) :method: GET + entry = hpackReader.headerTable.get(7); + assertEquals(":method", entry.name); + assertEquals("GET", entry.value); + assertEquals(42, entry.size); + assertTrue(entry.referenced); + + // Table size: 379 + assertEquals(379, hpackReader.headerTableSize); + + // Decoded header set: + // TODO: order is not correct per docs, but then again, the spec doesn't require ordering. + assertEquals(Arrays.asList( // + ":method", "GET", // + ":authority", "www.example.com", // + ":scheme", "https", // + ":path", "/index.html", // + "custom-key", "custom-value"), hpackReader.getAndReset()); + } + + private ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + private final HpackDraft05.Writer hpackWriter = new HpackDraft05.Writer(new DataOutputStream(bytesOut)); + + @Test public void readSingleByteInt() throws IOException { + assertEquals(10, new HpackDraft05.Reader(byteStream()).readInt(10, 31)); + assertEquals(10, new HpackDraft05.Reader(byteStream()).readInt(0xe0 | 10, 31)); + } + + @Test public void readMultibyteInt() throws IOException { + assertEquals(1337, new HpackDraft05.Reader(byteStream(154, 10)).readInt(31, 31)); + } + + @Test public void writeSingleByteInt() throws IOException { + hpackWriter.writeInt(10, 31, 0); + assertBytes(10); + hpackWriter.writeInt(10, 31, 0xe0); + assertBytes(0xe0 | 10); + } + + @Test public void writeMultibyteInt() throws IOException { + hpackWriter.writeInt(1337, 31, 0); + assertBytes(31, 154, 10); + hpackWriter.writeInt(1337, 31, 0xe0); + assertBytes(0xe0 | 31, 154, 10); + } + + @Test public void max31BitValue() throws IOException { + hpackWriter.writeInt(0x7fffffff, 31, 0); + assertBytes(31, 224, 255, 255, 255, 7); + assertEquals(0x7fffffff, + new HpackDraft05.Reader(byteStream(224, 255, 255, 255, 7)).readInt(31, 31)); + } + + @Test public void prefixMask() throws IOException { + hpackWriter.writeInt(31, 31, 0); + assertBytes(31, 0); + assertEquals(31, new HpackDraft05.Reader(byteStream(0)).readInt(31, 31)); + } + + @Test public void prefixMaskMinusOne() throws IOException { + hpackWriter.writeInt(30, 31, 0); + assertBytes(30); + assertEquals(31, new HpackDraft05.Reader(byteStream(0)).readInt(31, 31)); + } + + @Test public void zero() throws IOException { + hpackWriter.writeInt(0, 31, 0); + assertBytes(0); + assertEquals(0, new HpackDraft05.Reader(byteStream()).readInt(0, 31)); + } + + @Test public void headerName() throws IOException { + hpackWriter.writeString("foo"); + assertBytes(3, 'f', 'o', 'o'); + assertEquals("foo", new HpackDraft05.Reader(byteStream(3, 'f', 'o', 'o')).readString()); + } + + @Test public void emptyHeaderName() throws IOException { + hpackWriter.writeString(""); + assertBytes(0); + assertEquals("", new HpackDraft05.Reader(byteStream(0)).readString()); + } + + @Test public void headersRoundTrip() throws IOException { + List sentHeaders = Arrays.asList("name", "value"); + hpackWriter.writeHeaders(sentHeaders); + ByteArrayInputStream bytesIn = new ByteArrayInputStream(bytesOut.toByteArray()); + HpackDraft05.Reader reader = new HpackDraft05.Reader(new DataInputStream(bytesIn)); + reader.readHeaders(bytesOut.size()); + reader.emitReferenceSet(); + List receivedHeaders = reader.getAndReset(); + assertEquals(sentHeaders, receivedHeaders); + } + + private DataInputStream byteStream(int... bytes) { + byte[] data = intArrayToByteArray(bytes); + return new DataInputStream(new ByteArrayInputStream(data)); + } + + private byte[] literalHeaders(List sentHeaders) throws IOException { + ByteArrayOutputStream headerBytes = new ByteArrayOutputStream(); + new HpackDraft05.Writer(new DataOutputStream(headerBytes)).writeHeaders(sentHeaders); + return headerBytes.toByteArray(); + } + + private void assertBytes(int... bytes) { + byte[] expected = intArrayToByteArray(bytes); + byte[] actual = bytesOut.toByteArray(); + assertEquals(Arrays.toString(expected), Arrays.toString(actual)); + bytesOut.reset(); // So the next test starts with a clean slate. + } + + private byte[] intArrayToByteArray(int[] bytes) { + byte[] data = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) { + data[i] = (byte) bytes[i]; + } + return data; + } + + private static class MutableByteArrayInputStream extends ByteArrayInputStream { + + private MutableByteArrayInputStream() { + super(new byte[] { }); + } + + private void set(byte[] replacement) { + this.buf = replacement; + this.pos = 0; + this.count = replacement.length; + } + } +} diff --git a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/HpackTest.java b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/HpackTest.java deleted file mode 100644 index 0bcadce36..000000000 --- a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/HpackTest.java +++ /dev/null @@ -1,123 +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 java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class HpackTest { - private ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); - private final Hpack.Writer hpackWriter = new Hpack.Writer(new DataOutputStream(bytesOut)); - - @Test public void readSingleByteInt() throws IOException { - assertEquals(10, new Hpack.Reader(byteStream(), true).readInt(10, 31)); - assertEquals(10, new Hpack.Reader(byteStream(), true).readInt(0xe0 | 10, 31)); - } - - @Test public void readMultibyteInt() throws IOException { - assertEquals(1337, new Hpack.Reader(byteStream(154, 10), true).readInt(31, 31)); - } - - @Test public void writeSingleByteInt() throws IOException { - hpackWriter.writeInt(10, 31, 0); - assertBytes(10); - hpackWriter.writeInt(10, 31, 0xe0); - assertBytes(0xe0 | 10); - } - - @Test public void writeMultibyteInt() throws IOException { - hpackWriter.writeInt(1337, 31, 0); - assertBytes(31, 154, 10); - hpackWriter.writeInt(1337, 31, 0xe0); - assertBytes(0xe0 | 31, 154, 10); - } - - @Test public void max31BitValue() throws IOException { - hpackWriter.writeInt(0x7fffffff, 31, 0); - assertBytes(31, 224, 255, 255, 255, 7); - assertEquals(0x7fffffff, - new Hpack.Reader(byteStream(224, 255, 255, 255, 7), true).readInt(31, 31)); - } - - @Test public void prefixMask() throws IOException { - hpackWriter.writeInt(31, 31, 0); - assertBytes(31, 0); - assertEquals(31, new Hpack.Reader(byteStream(0), true).readInt(31, 31)); - } - - @Test public void prefixMaskMinusOne() throws IOException { - hpackWriter.writeInt(30, 31, 0); - assertBytes(30); - assertEquals(31, new Hpack.Reader(byteStream(0), true).readInt(31, 31)); - } - - @Test public void zero() throws IOException { - hpackWriter.writeInt(0, 31, 0); - assertBytes(0); - assertEquals(0, new Hpack.Reader(byteStream(), true).readInt(0, 31)); - } - - @Test public void headerName() throws IOException { - hpackWriter.writeString("foo"); - assertBytes(3, 'f', 'o', 'o'); - assertEquals("foo", new Hpack.Reader(byteStream(3, 'f', 'o', 'o'), true).readString()); - } - - @Test public void emptyHeaderName() throws IOException { - hpackWriter.writeString(""); - assertBytes(0); - assertEquals("", new Hpack.Reader(byteStream(0), true).readString()); - } - - @Test public void headersRoundTrip() throws IOException { - List sentHeaders = Arrays.asList("name", "value"); - hpackWriter.writeHeaders(sentHeaders); - ByteArrayInputStream bytesIn = new ByteArrayInputStream(bytesOut.toByteArray()); - Hpack.Reader reader = new Hpack.Reader(new DataInputStream(bytesIn), true); - reader.readHeaders(bytesOut.size()); - reader.emitReferenceSet(); - List receivedHeaders = reader.getAndReset(); - assertEquals(sentHeaders, receivedHeaders); - } - - private DataInputStream byteStream(int... bytes) { - byte[] data = intArrayToByteArray(bytes); - return new DataInputStream(new ByteArrayInputStream(data)); - } - - private void assertBytes(int... bytes) { - byte[] expected = intArrayToByteArray(bytes); - byte[] actual = bytesOut.toByteArray(); - assertEquals(Arrays.toString(expected), Arrays.toString(actual)); - bytesOut.reset(); // So the next test starts with a clean slate. - } - - private byte[] intArrayToByteArray(int[] bytes) { - byte[] data = new byte[bytes.length]; - for (int i = 0; i < bytes.length; i++) { - data[i] = (byte) bytes[i]; - } - return data; - } -} diff --git a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft06Test.java b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft06Test.java deleted file mode 100644 index c0e0fe65a..000000000 --- a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft06Test.java +++ /dev/null @@ -1,235 +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 java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class Http20Draft06Test { - static final int expectedStreamId = 15; - - @Test public void onlyOneLiteralHeadersFrame() throws IOException { - final List sentHeaders = Arrays.asList("name", "value"); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - DataOutputStream dataOut = new DataOutputStream(out); - - // Write the headers frame, specifying no more frames are expected. - { - byte[] headerBytes = literalHeaders(sentHeaders); - dataOut.writeShort(headerBytes.length); - dataOut.write(Http20Draft06.TYPE_HEADERS); - dataOut.write(Http20Draft06.FLAG_END_HEADERS | Http20Draft06.FLAG_END_STREAM); - dataOut.writeInt(expectedStreamId & 0x7fffffff); // stream with reserved bit set - dataOut.write(headerBytes); - } - - FrameReader fr = new Http20Draft06.Reader(new ByteArrayInputStream(out.toByteArray()), 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); - assertTrue(inFinished); - assertEquals(expectedStreamId, streamId); - assertEquals(-1, associatedStreamId); - assertEquals(-1, priority); - assertEquals(sentHeaders, nameValueBlock); - assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode); - } - }); - } - - @Test public void headersFrameThenContinuation() throws IOException { - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - DataOutputStream dataOut = new DataOutputStream(out); - - // Write the first headers frame. - { - byte[] headerBytes = literalHeaders(Arrays.asList("foo", "bar")); - dataOut.writeShort(headerBytes.length); - dataOut.write(Http20Draft06.TYPE_HEADERS); - dataOut.write(0); // no flags - dataOut.writeInt(expectedStreamId & 0x7fffffff); // stream with reserved bit set - dataOut.write(headerBytes); - } - - // Write the continuation frame, specifying no more frames are expected. - { - byte[] headerBytes = literalHeaders(Arrays.asList("baz", "qux")); - dataOut.writeShort(headerBytes.length); - dataOut.write(Http20Draft06.TYPE_CONTINUATION); - dataOut.write(Http20Draft06.FLAG_END_HEADERS | Http20Draft06.FLAG_END_STREAM); - dataOut.writeInt(expectedStreamId & 0x7fffffff); // stream with reserved bit set - dataOut.write(headerBytes); - } - - FrameReader fr = new Http20Draft06.Reader(new ByteArrayInputStream(out.toByteArray()), false); - - // Reading the above frames should result in a concatenated nameValueBlock. - fr.nextFrame(new BaseTestHandler() { - - @Override - public void headers(boolean outFinished, boolean inFinished, int streamId, - int associatedStreamId, int priority, List nameValueBlock, - HeadersMode headersMode) { - assertFalse(outFinished); - assertTrue(inFinished); - assertEquals(expectedStreamId, streamId); - assertEquals(-1, associatedStreamId); - assertEquals(-1, priority); - assertEquals(Arrays.asList("foo", "bar", "baz", "qux"), nameValueBlock); - assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode); - } - }); - } - - /** - * HPACK has a max header table size, which can be smaller than the max header message. - * Ensure the larger header content is not lost. - */ - @Test public void tooLargeToHPackIsStillEmitted() throws IOException { - char[] tooLarge = new char[4096]; - Arrays.fill(tooLarge, 'a'); - final List sentHeaders = Arrays.asList("foo", new String(tooLarge)); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - DataOutputStream dataOut = new DataOutputStream(out); - - writeOnlyHeadersFrame(literalHeaders(sentHeaders), dataOut); - - FrameReader fr = new Http20Draft06.Reader(new ByteArrayInputStream(out.toByteArray()), false); - - // Consume the large header set. - fr.nextFrame(new BaseTestHandler() { - - @Override - public void headers(boolean outFinished, boolean inFinished, int streamId, - int associatedStreamId, int priority, List nameValueBlock, - HeadersMode headersMode) { - assertEquals(sentHeaders, nameValueBlock); - } - }); - } - - @Test public void usingDraft06Examples() throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - DataOutputStream dataOut = new DataOutputStream(out); - - writeOnlyHeadersFrame(firstHeaderSetBytes(), dataOut); - writeOnlyHeadersFrame(secondHeaderSetBytes(), dataOut); - - FrameReader fr = new Http20Draft06.Reader(new ByteArrayInputStream(out.toByteArray()), false); - - // Consume the first header set. - fr.nextFrame(new BaseTestHandler() { - - @Override - public void headers(boolean outFinished, boolean inFinished, int streamId, - int associatedStreamId, int priority, List nameValueBlock, - HeadersMode headersMode) { - assertEquals(Arrays.asList(":path", "/my-example/index.html", "user-agent", "my-user-agent", - "mynewheader", "first"), nameValueBlock); - } - }); - - // Consume the second header set. - fr.nextFrame(new BaseTestHandler() { - - @Override - public void headers(boolean outFinished, boolean inFinished, int streamId, - int associatedStreamId, int priority, List nameValueBlock, - HeadersMode headersMode) { - assertEquals(Arrays.asList( - ":path", "/my-example/resources/script.js", - "user-agent", "my-user-agent", - "mynewheader", "second" - ), nameValueBlock); - } - }); - } - - // Deviates from draft only to fix doc bugs noted in https://github.com/igrigorik/http-2 specs. - // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#appendix-C.1 - static byte[] firstHeaderSetBytes() { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - - out.write(0x44); // literal header with incremental indexing, name index = 3 - out.write(0x16); // header value string length = 22 - out.write("/my-example/index.html".getBytes(), 0, 22); - - out.write(0x4C); // literal header with incremental indexing, name index = 11 - out.write(0x0D); // header value string length = 13 - out.write("my-user-agent".getBytes(), 0, 13); - - out.write(0x40); // literal header with incremental indexing, new name - out.write(0x0B); // header name string length = 11 - out.write("mynewheader".getBytes(), 0, 11); - out.write(0x05); // header value string length = 5 - out.write("first".getBytes(), 0, 5); - - return out.toByteArray(); - } - - // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#appendix-C.2 - static byte[] secondHeaderSetBytes() { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - - out.write(0x9e); // indexed header, index = 30: removal from reference set - out.write(0xa0); // indexed header, index = 32: removal from reference set - out.write(0x04); // literal header, substitution indexing, name index = 3 - - out.write(0x1e); // replaced entry index = 30 - out.write(0x1f); // header value string length = 31 - out.write("/my-example/resources/script.js".getBytes(), 0, 31); - - out.write(0x5f); - out.write(0x02); // literal header, incremental indexing, name index = 32 - out.write(0x06); // header value string length = 6 - out.write("second".getBytes(), 0, 6); - - return out.toByteArray(); - } - - static void writeOnlyHeadersFrame(byte[] headersSet, DataOutputStream dataOut) - throws IOException { - dataOut.writeShort(headersSet.length); - dataOut.write(Http20Draft06.TYPE_HEADERS); - dataOut.write(Http20Draft06.FLAG_END_HEADERS | Http20Draft06.FLAG_END_STREAM); - dataOut.writeInt(expectedStreamId & 0x7fffffff); // stream 15 with reserved bit set - dataOut.write(headersSet); - } - - static byte[] literalHeaders(List sentHeaders) throws IOException { - ByteArrayOutputStream headerBytes = new ByteArrayOutputStream(); - new Hpack.Writer(new DataOutputStream(headerBytes)).writeHeaders(sentHeaders); - return headerBytes.toByteArray(); - } -} diff --git a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft09Test.java b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft09Test.java new file mode 100644 index 000000000..31efb4596 --- /dev/null +++ b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft09Test.java @@ -0,0 +1,141 @@ +/* + * 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 java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import org.junit.Test; + +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 onlyOneLiteralHeadersFrame() throws IOException { + final List sentHeaders = Arrays.asList("name", "value"); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DataOutputStream dataOut = new DataOutputStream(out); + + // Write the headers frame, specifying no more frames are expected. + { + byte[] headerBytes = literalHeaders(sentHeaders); + dataOut.writeShort(headerBytes.length); + dataOut.write(Http20Draft09.TYPE_HEADERS); + dataOut.write(Http20Draft09.FLAG_END_HEADERS | Http20Draft09.FLAG_END_STREAM); + dataOut.writeInt(expectedStreamId & 0x7fffffff); // stream with reserved bit set + dataOut.write(headerBytes); + } + + FrameReader fr = new Http20Draft09.Reader(new ByteArrayInputStream(out.toByteArray()), 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); + assertTrue(inFinished); + assertEquals(expectedStreamId, streamId); + assertEquals(-1, associatedStreamId); + assertEquals(-1, priority); + assertEquals(sentHeaders, nameValueBlock); + assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode); + } + }); + } + + @Test public void headersFrameThenContinuation() throws IOException { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DataOutputStream dataOut = new DataOutputStream(out); + + // Write the first headers frame. + { + byte[] headerBytes = literalHeaders(Arrays.asList("foo", "bar")); + dataOut.writeShort(headerBytes.length); + dataOut.write(Http20Draft09.TYPE_HEADERS); + dataOut.write(0); // no flags + dataOut.writeInt(expectedStreamId & 0x7fffffff); // stream with reserved bit set + dataOut.write(headerBytes); + } + + // Write the continuation frame, specifying no more frames are expected. + { + byte[] headerBytes = literalHeaders(Arrays.asList("baz", "qux")); + dataOut.writeShort(headerBytes.length); + dataOut.write(Http20Draft09.TYPE_CONTINUATION); + dataOut.write(Http20Draft09.FLAG_END_HEADERS | Http20Draft09.FLAG_END_STREAM); + dataOut.writeInt(expectedStreamId & 0x7fffffff); // stream with reserved bit set + dataOut.write(headerBytes); + } + + FrameReader fr = new Http20Draft09.Reader(new ByteArrayInputStream(out.toByteArray()), false); + + // Reading the above frames should result in a concatenated nameValueBlock. + 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(-1, priority); + assertEquals(Arrays.asList("foo", "bar", "baz", "qux"), nameValueBlock); + assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode); + } + }); + } + + @Test public void readRstStreamFrame() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DataOutputStream dataOut = new DataOutputStream(out); + + dataOut.writeShort(4); + dataOut.write(Http20Draft09.TYPE_RST_STREAM); + dataOut.write(0); // No flags + dataOut.writeInt(expectedStreamId & 0x7fffffff); // stream with reserved bit set + dataOut.writeInt(ErrorCode.COMPRESSION_ERROR.httpCode); + + FrameReader fr = new Http20Draft09.Reader(new ByteArrayInputStream(out.toByteArray()), 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); + } + }); + } + + private byte[] literalHeaders(List sentHeaders) throws IOException { + ByteArrayOutputStream headerBytes = new ByteArrayOutputStream(); + new HpackDraft05.Writer(new DataOutputStream(headerBytes)).writeHeaders(sentHeaders); + return headerBytes.toByteArray(); + } +} diff --git a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java index a906fc7cb..ead73eba5 100644 --- a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java +++ b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java @@ -36,13 +36,21 @@ public final class SettingsTest { @Test public void setFields() { Settings settings = new Settings(); + // WARNING: clash on flags between spdy/3 and http/2! assertEquals(-3, settings.getUploadBandwidth(-3)); + assertEquals(4096, settings.getHeaderTableSize()); settings.set(Settings.UPLOAD_BANDWIDTH, 0, 42); assertEquals(42, settings.getUploadBandwidth(-3)); + settings.set(Settings.HEADER_TABLE_SIZE, 0, 8096); + assertEquals(8096, settings.getHeaderTableSize()); + // WARNING: clash on flags between spdy/3 and http/2! assertEquals(-3, settings.getDownloadBandwidth(-3)); + assertTrue(settings.getEnablePush()); settings.set(Settings.DOWNLOAD_BANDWIDTH, 0, 53); assertEquals(53, settings.getDownloadBandwidth(-3)); + settings.set(Settings.ENABLE_PUSH, 0, 0); + assertFalse(settings.getEnablePush()); assertEquals(-3, settings.getRoundTripTime(-3)); settings.set(Settings.ROUND_TRIP_TIME, 0, 64); diff --git a/okhttp/src/main/java/com/squareup/okhttp/Connection.java b/okhttp/src/main/java/com/squareup/okhttp/Connection.java index bac09a213..17b236d6f 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/Connection.java +++ b/okhttp/src/main/java/com/squareup/okhttp/Connection.java @@ -64,10 +64,25 @@ import static java.net.HttpURLConnection.HTTP_PROXY_AUTH; * should the attempt fail. */ public final class Connection implements Closeable { - private static final byte[] NPN_PROTOCOLS = new byte[] { + private static final byte[] ALL_PROTOCOLS = new byte[] { + 17, 'H', 'T', 'T', 'P', '-', 'd', 'r', 'a', 'f', 't', '-', '0', '9', '/', '2', '.', '0', 6, 's', 'p', 'd', 'y', '/', '3', 8, 'h', 't', 't', 'p', '/', '1', '.', '1' }; + + private static final byte[] SPDY_AND_HTTP = new byte[] { + 6, 's', 'p', 'd', 'y', '/', '3', + 8, 'h', 't', 't', 'p', '/', '1', '.', '1' + }; + + private static final byte[] HTTP2_AND_HTTP = new byte[] { + 17, 'H', 'T', 'T', 'P', '-', 'd', 'r', 'a', 'f', 't', '-', '0', '9', '/', '2', '.', '0', + 8, 'h', 't', 't', 'p', '/', '1', '.', '1' + }; + + private static final byte[] HTTP_20_DRAFT_09 = new byte[] { + 'H', 'T', 'T', 'P', '-', 'd', 'r', 'a', 'f', 't', '-', '0', '9', '/', '2', '.', '0' + }; private static final byte[] SPDY3 = new byte[] { 's', 'p', 'd', 'y', '/', '3' }; @@ -130,9 +145,20 @@ public final class Connection implements Closeable { platform.supportTlsIntolerantServer(sslSocket); } - boolean useNpn = route.modernTls && route.address.transports.contains("spdy/3"); + boolean useNpn = route.modernTls && ( + route.address.transports.contains("HTTP-draft-09/2.0") + || route.address.transports.contains("spdy/3") + ); + if (useNpn) { - platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS); + if (route.address.transports.contains("HTTP-draft-09/2.0") + && route.address.transports.contains("spdy/3")) { + platform.setNpnProtocols(sslSocket, ALL_PROTOCOLS); + } else if (route.address.transports.contains("HTTP-draft-09/2.0")) { + platform.setNpnProtocols(sslSocket, HTTP2_AND_HTTP); + } else { + platform.setNpnProtocols(sslSocket, SPDY_AND_HTTP); + } } // Force handshake. This can throw! @@ -150,10 +176,13 @@ public final class Connection implements Closeable { byte[] selectedProtocol; if (useNpn && (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) { - if (Arrays.equals(selectedProtocol, SPDY3)) { + if (Arrays.equals(selectedProtocol, HTTP_20_DRAFT_09) + || Arrays.equals(selectedProtocol, SPDY3)) { + SpdyConnection.Builder builder = + new SpdyConnection.Builder(route.address.getUriHost(), true, in, out); + if (Arrays.equals(selectedProtocol, HTTP_20_DRAFT_09)) builder.http20Draft09(); sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream. - spdyConnection = new SpdyConnection.Builder(route.address.getUriHost(), true, in, out) - .build(); + spdyConnection = builder.build(); spdyConnection.sendConnectionHeader(); } else if (!Arrays.equals(selectedProtocol, HTTP_11)) { throw new IOException( diff --git a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java index 37f83f7dc..0de5eb788 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java +++ b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java @@ -39,7 +39,7 @@ import javax.net.ssl.SSLSocketFactory; /** Configures and creates HTTP connections. */ public final class OkHttpClient implements URLStreamHandlerFactory, Cloneable { private static final List DEFAULT_TRANSPORTS - = Util.immutableList(Arrays.asList("spdy/3", "http/1.1")); + = Util.immutableList(Arrays.asList("HTTP-draft-09/2.0", "spdy/3", "http/1.1")); private final RouteDatabase routeDatabase; private final Dispatcher dispatcher; diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp20Draft09Test.java b/okhttp/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp20Draft09Test.java new file mode 100644 index 000000000..412e3db8c --- /dev/null +++ b/okhttp/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp20Draft09Test.java @@ -0,0 +1,24 @@ +/* + * 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.http; + +public class HttpOverHttp20Draft09Test extends HttpOverSpdyTest { + + public HttpOverHttp20Draft09Test() { + super("HTTP-draft-09/2.0"); + this.hostHeader = ":authority"; + } +} diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdy3Test.java b/okhttp/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdy3Test.java new file mode 100644 index 000000000..4c30e45f4 --- /dev/null +++ b/okhttp/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdy3Test.java @@ -0,0 +1,23 @@ +/* + * 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.http; + +public class HttpOverSpdy3Test extends HttpOverSpdyTest { + + public HttpOverSpdy3Test() { + super("spdy/3"); + } +} diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdyTest.java b/okhttp/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdyTest.java index d3ee22405..18441ed11 100644 --- a/okhttp/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdyTest.java +++ b/okhttp/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdyTest.java @@ -56,7 +56,16 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** Test how SPDY interacts with HTTP features. */ -public final class HttpOverSpdyTest { +public abstract class HttpOverSpdyTest { + + /** Transport to test, for example {@code spdy/3} */ + private final String transport; + protected String hostHeader = ":host"; + + protected HttpOverSpdyTest(String transport){ + this.transport = transport; + } + private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() { public boolean verify(String hostname, SSLSession session) { return true; @@ -71,10 +80,11 @@ public final class HttpOverSpdyTest { @Before public void setUp() throws Exception { server.useHttps(sslContext.getSocketFactory(), false); + client.setTransports(Arrays.asList(transport, "http/1.1")); client.setSslSocketFactory(sslContext.getSocketFactory()); client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); String systemTmpDir = System.getProperty("java.io.tmpdir"); - File cacheDir = new File(systemTmpDir, "HttpCache-" + UUID.randomUUID()); + File cacheDir = new File(systemTmpDir, "HttpCache-" + transport + "-" + UUID.randomUUID()); cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE); } @@ -96,7 +106,7 @@ public final class HttpOverSpdyTest { RecordedRequest request = server.takeRequest(); assertEquals("GET /foo HTTP/1.1", request.getRequestLine()); assertContains(request.getHeaders(), ":scheme: https"); - assertContains(request.getHeaders(), ":host: " + hostName + ":" + server.getPort()); + assertContains(request.getHeaders(), hostHeader + ": " + hostName + ":" + server.getPort()); } @Test public void emptyResponse() throws IOException {