From 21dc92f967d01c2da3ace57b7cc04421e2fd296b Mon Sep 17 00:00:00 2001 From: jwilson Date: Sun, 11 Aug 2013 10:18:34 -0400 Subject: [PATCH] Support multiple variants of the SPDY protocol. This behavior-free refactoring makes the first baby steps towards supporting HTTP/2.0. It adds indirection on the framing layer so we can frame either using SPDY/3's syntax or HTTP/2.0's. --- .../okhttp/mockwebserver/MockWebServer.java | 23 +- .../okhttp/internal/spdy/Http20Draft04.java | 121 +++++ .../squareup/okhttp/internal/spdy/Spdy3.java | 487 ++++++++++++++++++ .../okhttp/internal/spdy/SpdyConnection.java | 38 +- .../okhttp/internal/spdy/SpdyReader.java | 291 +---------- .../okhttp/internal/spdy/SpdyWriter.java | 170 +----- .../okhttp/internal/spdy/Variant.java | 28 + .../okhttp/internal/spdy/MockSpdyPeer.java | 4 +- .../java/com/squareup/okhttp/Connection.java | 4 +- 9 files changed, 707 insertions(+), 459 deletions(-) create mode 100644 okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft04.java create mode 100644 okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Spdy3.java create mode 100644 okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Variant.java 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 6780e1332..79fb0f5bb 100644 --- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java +++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java @@ -70,12 +70,17 @@ import static com.squareup.okhttp.mockwebserver.SocketPolicy.FAIL_HANDSHAKE; */ public final class MockWebServer { private static final byte[] NPN_PROTOCOLS = { + // TODO: support HTTP/2.0. + // 17, 'H', 'T', 'T', 'P', '-', 'd', 'r', 'a', 'f', 't', '-', '0', '4', '/', '2', '.', '0', 6, 's', 'p', 'd', 'y', '/', '3', 8, 'h', 't', 't', 'p', '/', '1', '.', '1' }; private static final byte[] SPDY3 = new byte[] { 's', 'p', 'd', 'y', '/', '3' }; + private static final byte[] HTTP_20_DRAFT_04 = new byte[] { + 'H', 'T', 'T', 'P', '-', 'd', 'r', 'a', 'f', 't', '-', '0', '4', '/', '2', '.', '0' + }; private static final byte[] HTTP_11 = new byte[] { 'h', 't', 't', 'p', '/', '1', '.', '1' }; @@ -322,6 +327,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_04)) { + transport = Transport.HTTP_20_DRAFT_04; } else if (Arrays.equals(selectedProtocol, SPDY3)) { transport = Transport.SPDY_3; } else { @@ -334,13 +341,19 @@ public final class MockWebServer { socket = raw; } - if (transport == Transport.SPDY_3) { + if (transport == Transport.SPDY_3 || transport == Transport.HTTP_20_DRAFT_04) { SpdySocketHandler spdySocketHandler = new SpdySocketHandler(socket); - SpdyConnection spdyConnection = new SpdyConnection.Builder(false, socket) - .handler(spdySocketHandler) - .build(); + SpdyConnection.Builder builder = new SpdyConnection.Builder(false, socket) + .handler(spdySocketHandler); + if (transport == Transport.SPDY_3) { + builder.spdy3(); + } else { + builder.http20Draft04(); + } + SpdyConnection spdyConnection = builder.build(); openSpdyConnections.put(spdyConnection, Boolean.TRUE); openClientSockets.remove(socket); + spdyConnection.sendConnectionHeader(); return; } @@ -699,6 +712,6 @@ public final class MockWebServer { } enum Transport { - HTTP_11, SPDY_3 + HTTP_11, SPDY_3, HTTP_20_DRAFT_04 } } diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft04.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft04.java new file mode 100644 index 000000000..ef832a560 --- /dev/null +++ b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft04.java @@ -0,0 +1,121 @@ +/* + * 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.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; + +final class Http20Draft04 implements Variant { + @Override public SpdyReader newReader(InputStream in) { + return new Reader(in); + } + + @Override public SpdyWriter newWriter(OutputStream out) { + return new Writer(out); + } + + static final class Reader implements SpdyReader { + private final DataInputStream in; + + Reader(InputStream in) { + this.in = new DataInputStream(in); + } + + @Override public boolean nextFrame(Handler handler) throws IOException { + return false; + } + + @Override public void close() throws IOException { + in.close(); + } + } + + static final class Writer implements SpdyWriter { + private final DataOutputStream out; + + Writer(OutputStream out) { + this.out = new DataOutputStream(out); + } + + @Override public synchronized void writeFrame(byte[] data, int offset, int length) + throws IOException { + // TODO: this method no longer makes sense; the raw frame can't support all variants! + throw new UnsupportedOperationException("TODO"); + } + + @Override public synchronized void flush() throws IOException { + out.flush(); + } + + @Override public synchronized void connectionHeader() { + throw new UnsupportedOperationException("TODO"); + } + + @Override public synchronized void synStream(int flags, int streamId, int associatedStreamId, + int priority, int slot, List nameValueBlock) throws IOException { + throw new UnsupportedOperationException("TODO"); + } + + @Override public synchronized void synReply(int flags, int streamId, + List nameValueBlock) throws IOException { + throw new UnsupportedOperationException("TODO"); + } + + @Override public synchronized void headers(int flags, int streamId, List nameValueBlock) + throws IOException { + throw new UnsupportedOperationException("TODO"); + } + + @Override public synchronized void rstStream(int streamId, int statusCode) throws IOException { + throw new UnsupportedOperationException("TODO"); + } + + @Override public synchronized void data(int flags, int streamId, byte[] data) + throws IOException { + throw new UnsupportedOperationException("TODO"); + } + + @Override public synchronized void settings(int flags, Settings settings) throws IOException { + throw new UnsupportedOperationException("TODO"); + } + + @Override public synchronized void noop() throws IOException { + throw new UnsupportedOperationException("TODO"); + } + + @Override public synchronized void ping(int flags, int id) throws IOException { + throw new UnsupportedOperationException("TODO"); + } + + @Override public synchronized void goAway(int flags, int lastGoodStreamId, int statusCode) + throws IOException { + throw new UnsupportedOperationException("TODO"); + } + + @Override public synchronized void windowUpdate(int streamId, int deltaWindowSize) + throws IOException { + throw new UnsupportedOperationException("TODO"); + } + + @Override public void close() throws IOException { + out.close(); + } + } +} diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Spdy3.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Spdy3.java new file mode 100644 index 000000000..a1993901d --- /dev/null +++ b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Spdy3.java @@ -0,0 +1,487 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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.Platform; +import com.squareup.okhttp.internal.Util; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.ProtocolException; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; +import java.util.zip.DataFormatException; +import java.util.zip.Deflater; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +final class Spdy3 implements Variant { + static final byte[] DICTIONARY; + static { + try { + DICTIONARY = ("\u0000\u0000\u0000\u0007options\u0000\u0000\u0000\u0004hea" + + "d\u0000\u0000\u0000\u0004post\u0000\u0000\u0000\u0003put\u0000\u0000\u0000\u0006dele" + + "te\u0000\u0000\u0000\u0005trace\u0000\u0000\u0000\u0006accept\u0000\u0000\u0000" + + "\u000Eaccept-charset\u0000\u0000\u0000\u000Faccept-encoding\u0000\u0000\u0000\u000Fa" + + "ccept-language\u0000\u0000\u0000\raccept-ranges\u0000\u0000\u0000\u0003age\u0000" + + "\u0000\u0000\u0005allow\u0000\u0000\u0000\rauthorization\u0000\u0000\u0000\rcache-co" + + "ntrol\u0000\u0000\u0000\nconnection\u0000\u0000\u0000\fcontent-base\u0000\u0000" + + "\u0000\u0010content-encoding\u0000\u0000\u0000\u0010content-language\u0000\u0000" + + "\u0000\u000Econtent-length\u0000\u0000\u0000\u0010content-location\u0000\u0000\u0000" + + "\u000Bcontent-md5\u0000\u0000\u0000\rcontent-range\u0000\u0000\u0000\fcontent-type" + + "\u0000\u0000\u0000\u0004date\u0000\u0000\u0000\u0004etag\u0000\u0000\u0000\u0006expe" + + "ct\u0000\u0000\u0000\u0007expires\u0000\u0000\u0000\u0004from\u0000\u0000\u0000" + + "\u0004host\u0000\u0000\u0000\bif-match\u0000\u0000\u0000\u0011if-modified-since" + + "\u0000\u0000\u0000\rif-none-match\u0000\u0000\u0000\bif-range\u0000\u0000\u0000" + + "\u0013if-unmodified-since\u0000\u0000\u0000\rlast-modified\u0000\u0000\u0000\blocati" + + "on\u0000\u0000\u0000\fmax-forwards\u0000\u0000\u0000\u0006pragma\u0000\u0000\u0000" + + "\u0012proxy-authenticate\u0000\u0000\u0000\u0013proxy-authorization\u0000\u0000" + + "\u0000\u0005range\u0000\u0000\u0000\u0007referer\u0000\u0000\u0000\u000Bretry-after" + + "\u0000\u0000\u0000\u0006server\u0000\u0000\u0000\u0002te\u0000\u0000\u0000\u0007trai" + + "ler\u0000\u0000\u0000\u0011transfer-encoding\u0000\u0000\u0000\u0007upgrade\u0000" + + "\u0000\u0000\nuser-agent\u0000\u0000\u0000\u0004vary\u0000\u0000\u0000\u0003via" + + "\u0000\u0000\u0000\u0007warning\u0000\u0000\u0000\u0010www-authenticate\u0000\u0000" + + "\u0000\u0006method\u0000\u0000\u0000\u0003get\u0000\u0000\u0000\u0006status\u0000" + + "\u0000\u0000\u0006200 OK\u0000\u0000\u0000\u0007version\u0000\u0000\u0000\bHTTP/1.1" + + "\u0000\u0000\u0000\u0003url\u0000\u0000\u0000\u0006public\u0000\u0000\u0000\nset-coo" + + "kie\u0000\u0000\u0000\nkeep-alive\u0000\u0000\u0000\u0006origin100101201202205206300" + + "302303304305306307402405406407408409410411412413414415416417502504505203 Non-Authori" + + "tative Information204 No Content301 Moved Permanently400 Bad Request401 Unauthorized" + + "403 Forbidden404 Not Found500 Internal Server Error501 Not Implemented503 Service Un" + + "availableJan Feb Mar Apr May Jun Jul Aug Sept Oct Nov Dec 00:00:00 Mon, Tue, Wed, Th" + + "u, Fri, Sat, Sun, GMTchunked,text/html,image/png,image/jpg,image/gif,application/xml" + + ",application/xhtml+xml,text/plain,text/javascript,publicprivatemax-age=gzip,deflate," + + "sdchcharset=utf-8charset=iso-8859-1,utf-,*,enq=0.").getBytes(Util.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(); + } + } + + @Override public SpdyReader newReader(InputStream in) { + return new Reader(in); + } + + @Override public SpdyWriter newWriter(OutputStream out) { + return new Writer(out); + } + + /** Read spdy/3 frames. */ + static final class Reader implements SpdyReader { + private final DataInputStream in; + private final DataInputStream nameValueBlockIn; + private int compressedLimit; + + Reader(InputStream in) { + this.in = new DataInputStream(in); + this.nameValueBlockIn = newNameValueBlockStream(); + } + + /** + * Send the next frame to {@code handler}. Returns true unless there are no + * more frames on the stream. + */ + @Override public boolean nextFrame(Handler handler) throws IOException { + int w1; + try { + w1 = in.readInt(); + } catch (IOException e) { + return false; // This might be a normal socket close. + } + int w2 = in.readInt(); + + boolean control = (w1 & 0x80000000) != 0; + int flags = (w2 & 0xff000000) >>> 24; + int length = (w2 & 0xffffff); + + if (control) { + int version = (w1 & 0x7fff0000) >>> 16; + int type = (w1 & 0xffff); + + if (version != 3) { + throw new ProtocolException("version != 3: " + version); + } + + switch (type) { + case SpdyConnection.TYPE_SYN_STREAM: + readSynStream(handler, flags, length); + return true; + + case SpdyConnection.TYPE_SYN_REPLY: + readSynReply(handler, flags, length); + return true; + + case SpdyConnection.TYPE_RST_STREAM: + readRstStream(handler, flags, length); + return true; + + case SpdyConnection.TYPE_SETTINGS: + readSettings(handler, flags, length); + return true; + + case SpdyConnection.TYPE_NOOP: + if (length != 0) throw ioException("TYPE_NOOP length: %d != 0", length); + handler.noop(); + return true; + + case SpdyConnection.TYPE_PING: + readPing(handler, flags, length); + return true; + + case SpdyConnection.TYPE_GOAWAY: + readGoAway(handler, flags, length); + return true; + + case SpdyConnection.TYPE_HEADERS: + readHeaders(handler, flags, length); + return true; + + case SpdyConnection.TYPE_WINDOW_UPDATE: + readWindowUpdate(handler, flags, length); + return true; + + case SpdyConnection.TYPE_CREDENTIAL: + Util.skipByReading(in, length); + throw new UnsupportedOperationException("TODO"); // TODO: implement + + default: + throw new IOException("Unexpected frame"); + } + } else { + int streamId = w1 & 0x7fffffff; + handler.data(flags, streamId, in, length); + return true; + } + } + + private void readSynStream(Handler handler, int flags, int length) throws IOException { + int w1 = in.readInt(); + int w2 = in.readInt(); + int s3 = in.readShort(); + int streamId = w1 & 0x7fffffff; + int associatedStreamId = w2 & 0x7fffffff; + int priority = (s3 & 0xe000) >>> 13; + int slot = s3 & 0xff; + List nameValueBlock = readNameValueBlock(length - 10); + handler.synStream(flags, streamId, associatedStreamId, priority, slot, nameValueBlock); + } + + private void readSynReply(Handler handler, int flags, int length) throws IOException { + int w1 = in.readInt(); + int streamId = w1 & 0x7fffffff; + List nameValueBlock = readNameValueBlock(length - 4); + handler.synReply(flags, streamId, nameValueBlock); + } + + private void readRstStream(Handler handler, int flags, int length) throws IOException { + if (length != 8) throw ioException("TYPE_RST_STREAM length: %d != 8", length); + int streamId = in.readInt() & 0x7fffffff; + int statusCode = in.readInt(); + handler.rstStream(flags, streamId, statusCode); + } + + private void readHeaders(Handler handler, int flags, int length) throws IOException { + int w1 = in.readInt(); + int streamId = w1 & 0x7fffffff; + List nameValueBlock = readNameValueBlock(length - 4); + handler.headers(flags, streamId, nameValueBlock); + } + + private void readWindowUpdate(Handler handler, int flags, int length) throws IOException { + if (length != 8) throw ioException("TYPE_WINDOW_UPDATE length: %d != 8", length); + int w1 = in.readInt(); + int w2 = in.readInt(); + int streamId = w1 & 0x7fffffff; + int deltaWindowSize = w2 & 0x7fffffff; + handler.windowUpdate(flags, streamId, deltaWindowSize); + } + + private DataInputStream newNameValueBlockStream() { + // Limit the inflater input stream to only those bytes in the Name/Value block. + final InputStream throttleStream = new InputStream() { + @Override public int read() throws IOException { + return Util.readSingleByte(this); + } + + @Override public int read(byte[] buffer, int offset, int byteCount) throws IOException { + byteCount = Math.min(byteCount, compressedLimit); + int consumed = in.read(buffer, offset, byteCount); + compressedLimit -= consumed; + return consumed; + } + + @Override public void close() throws IOException { + in.close(); + } + }; + + // Subclass inflater to install a dictionary when it's needed. + Inflater inflater = new Inflater() { + @Override public int inflate(byte[] buffer, int offset, int count) + throws DataFormatException { + int result = super.inflate(buffer, offset, count); + if (result == 0 && needsDictionary()) { + setDictionary(DICTIONARY); + result = super.inflate(buffer, offset, count); + } + return result; + } + }; + + return new DataInputStream(new InflaterInputStream(throttleStream, inflater)); + } + + private List readNameValueBlock(int length) throws IOException { + this.compressedLimit += length; + try { + int numberOfPairs = nameValueBlockIn.readInt(); + if (numberOfPairs < 0) { + Logger.getLogger(getClass().getName()).warning("numberOfPairs < 0: " + numberOfPairs); + throw ioException("numberOfPairs < 0"); + } + List entries = new ArrayList(numberOfPairs * 2); + for (int i = 0; i < numberOfPairs; i++) { + String name = readString(); + String values = readString(); + if (name.length() == 0) throw ioException("name.length == 0"); + if (values.length() == 0) throw ioException("values.length == 0"); + entries.add(name); + entries.add(values); + } + + if (compressedLimit != 0) { + Logger.getLogger(getClass().getName()).warning("compressedLimit > 0: " + compressedLimit); + } + + return entries; + } catch (DataFormatException e) { + throw new IOException(e.getMessage()); + } + } + + private String readString() throws DataFormatException, IOException { + int length = nameValueBlockIn.readInt(); + byte[] bytes = new byte[length]; + Util.readFully(nameValueBlockIn, bytes); + return new String(bytes, 0, length, "UTF-8"); + } + + private void readPing(Handler handler, int flags, int length) throws IOException { + if (length != 4) throw ioException("TYPE_PING length: %d != 4", length); + int id = in.readInt(); + handler.ping(flags, id); + } + + private void readGoAway(Handler handler, int flags, int length) throws IOException { + if (length != 8) throw ioException("TYPE_GOAWAY length: %d != 8", length); + int lastGoodStreamId = in.readInt() & 0x7fffffff; + int statusCode = in.readInt(); + handler.goAway(flags, lastGoodStreamId, statusCode); + } + + private void readSettings(Handler handler, int flags, int length) throws IOException { + int numberOfEntries = in.readInt(); + if (length != 4 + 8 * numberOfEntries) { + throw ioException("TYPE_SETTINGS length: %d != 4 + 8 * %d", length, numberOfEntries); + } + Settings settings = new Settings(); + for (int i = 0; i < numberOfEntries; i++) { + int w1 = in.readInt(); + int value = in.readInt(); + int idFlags = (w1 & 0xff000000) >>> 24; + int id = w1 & 0xffffff; + settings.set(id, idFlags, value); + } + handler.settings(flags, settings); + } + + private static IOException ioException(String message, Object... args) throws IOException { + throw new IOException(String.format(message, args)); + } + + @Override public void close() throws IOException { + Util.closeAll(in, nameValueBlockIn); + } + } + + /** Write spdy/3 frames. */ + static final class Writer implements SpdyWriter { + private final DataOutputStream out; + private final ByteArrayOutputStream nameValueBlockBuffer; + private final DataOutputStream nameValueBlockOut; + + Writer(OutputStream out) { + this.out = new DataOutputStream(out); + + Deflater deflater = new Deflater(); + deflater.setDictionary(DICTIONARY); + nameValueBlockBuffer = new ByteArrayOutputStream(); + nameValueBlockOut = new DataOutputStream( + Platform.get().newDeflaterOutputStream(nameValueBlockBuffer, deflater, true)); + } + + @Override public void connectionHeader() { + // Do nothing: no connection header for SPDY/3. + } + + @Override public synchronized void writeFrame(byte[] data, int offset, int length) + throws IOException { + out.write(data, offset, length); + } + + @Override public synchronized void flush() throws IOException { + out.flush(); + } + + @Override public synchronized void synStream(int flags, int streamId, int associatedStreamId, + int priority, int slot, List nameValueBlock) throws IOException { + writeNameValueBlockToBuffer(nameValueBlock); + int length = 10 + nameValueBlockBuffer.size(); + int type = SpdyConnection.TYPE_SYN_STREAM; + + int unused = 0; + out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff); + out.writeInt((flags & 0xff) << 24 | length & 0xffffff); + out.writeInt(streamId & 0x7fffffff); + out.writeInt(associatedStreamId & 0x7fffffff); + out.writeShort((priority & 0x7) << 13 | (unused & 0x1f) << 8 | (slot & 0xff)); + nameValueBlockBuffer.writeTo(out); + out.flush(); + } + + @Override public synchronized void synReply( + int flags, int streamId, List nameValueBlock) throws IOException { + writeNameValueBlockToBuffer(nameValueBlock); + int type = SpdyConnection.TYPE_SYN_REPLY; + int length = nameValueBlockBuffer.size() + 4; + + out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff); + out.writeInt((flags & 0xff) << 24 | length & 0xffffff); + out.writeInt(streamId & 0x7fffffff); + nameValueBlockBuffer.writeTo(out); + out.flush(); + } + + @Override public synchronized void headers(int flags, int streamId, List nameValueBlock) + throws IOException { + writeNameValueBlockToBuffer(nameValueBlock); + int type = SpdyConnection.TYPE_HEADERS; + int length = nameValueBlockBuffer.size() + 4; + + out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff); + out.writeInt((flags & 0xff) << 24 | length & 0xffffff); + out.writeInt(streamId & 0x7fffffff); + nameValueBlockBuffer.writeTo(out); + out.flush(); + } + + @Override public synchronized void rstStream(int streamId, int statusCode) throws IOException { + int flags = 0; + int type = SpdyConnection.TYPE_RST_STREAM; + int length = 8; + out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff); + out.writeInt((flags & 0xff) << 24 | length & 0xffffff); + out.writeInt(streamId & 0x7fffffff); + out.writeInt(statusCode); + out.flush(); + } + + @Override public synchronized void data(int flags, int streamId, byte[] data) + throws IOException { + int length = data.length; + out.writeInt(streamId & 0x7fffffff); + out.writeInt((flags & 0xff) << 24 | length & 0xffffff); + out.write(data); + out.flush(); + } + + private void writeNameValueBlockToBuffer(List nameValueBlock) throws IOException { + nameValueBlockBuffer.reset(); + int numberOfPairs = nameValueBlock.size() / 2; + nameValueBlockOut.writeInt(numberOfPairs); + for (String s : nameValueBlock) { + nameValueBlockOut.writeInt(s.length()); + nameValueBlockOut.write(s.getBytes("UTF-8")); + } + nameValueBlockOut.flush(); + } + + @Override public synchronized void settings(int flags, Settings settings) throws IOException { + int type = SpdyConnection.TYPE_SETTINGS; + int size = settings.size(); + int length = 4 + size * 8; + out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff); + out.writeInt((flags & 0xff) << 24 | length & 0xffffff); + out.writeInt(size); + for (int i = 0; i <= Settings.COUNT; i++) { + if (!settings.isSet(i)) continue; + int settingsFlags = settings.flags(i); + out.writeInt((settingsFlags & 0xff) << 24 | (i & 0xffffff)); + out.writeInt(settings.get(i)); + } + out.flush(); + } + + @Override public synchronized void noop() throws IOException { + int type = SpdyConnection.TYPE_NOOP; + int length = 0; + int flags = 0; + out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff); + out.writeInt((flags & 0xff) << 24 | length & 0xffffff); + out.flush(); + } + + @Override public synchronized void ping(int flags, int id) throws IOException { + int type = SpdyConnection.TYPE_PING; + int length = 4; + out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff); + out.writeInt((flags & 0xff) << 24 | length & 0xffffff); + out.writeInt(id); + out.flush(); + } + + @Override public synchronized void goAway(int flags, int lastGoodStreamId, int statusCode) + throws IOException { + int type = SpdyConnection.TYPE_GOAWAY; + int length = 8; + out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff); + out.writeInt((flags & 0xff) << 24 | length & 0xffffff); + out.writeInt(lastGoodStreamId); + out.writeInt(statusCode); + out.flush(); + } + + @Override public synchronized void windowUpdate(int streamId, int deltaWindowSize) + throws IOException { + int type = SpdyConnection.TYPE_WINDOW_UPDATE; + int flags = 0; + int length = 8; + out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff); + out.writeInt((flags & 0xff) << 24 | length & 0xffffff); + out.writeInt(streamId); + out.writeInt(deltaWindowSize); + out.flush(); + } + + @Override public void close() throws IOException { + Util.closeAll(out, nameValueBlockOut); + } + } +} 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 d3a3c9c24..a20462154 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 @@ -79,6 +79,9 @@ 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-04/2.0. */ + final Variant variant; + /** True if this peer initiated the connection. */ final boolean client; @@ -105,10 +108,11 @@ public final class SpdyConnection implements Closeable { Settings settings; private SpdyConnection(Builder builder) { + variant = builder.variant; client = builder.client; handler = builder.handler; - spdyReader = new SpdyReader(builder.in); - spdyWriter = new SpdyWriter(builder.out); + spdyReader = variant.newReader(builder.in); + spdyWriter = variant.newWriter(builder.out); nextStreamId = builder.client ? 1 : 2; nextPingId = builder.client ? 1 : 2; @@ -192,11 +196,8 @@ public final class SpdyConnection implements Closeable { spdyWriter.synReply(flags, streamId, alternating); } - /** Writes a complete data frame. */ void writeFrame(byte[] bytes, int offset, int length) throws IOException { - synchronized (spdyWriter) { - spdyWriter.out.write(bytes, offset, length); - } + spdyWriter.writeFrame(bytes, offset, length); } void writeSynResetLater(final int streamId, final int statusCode) { @@ -278,9 +279,7 @@ public final class SpdyConnection implements Closeable { } public void flush() throws IOException { - synchronized (spdyWriter) { - spdyWriter.out.flush(); - } + spdyWriter.flush(); } /** @@ -368,12 +367,21 @@ public final class SpdyConnection implements Closeable { if (thrown != null) throw thrown; } + /** + * Sends a connection header if the current variant requires it. This should + * be called after {@link Builder#build} for all new connections. + */ + public void sendConnectionHeader() { + spdyWriter.connectionHeader(); + } + public static class Builder { private String hostName; private InputStream in; private OutputStream out; private IncomingStreamHandler handler = IncomingStreamHandler.REFUSE_INCOMING_STREAMS; - public boolean client; + private Variant variant = Variant.SPDY3; + private boolean client; public Builder(boolean client, Socket socket) throws IOException { this("", client, socket.getInputStream(), socket.getOutputStream()); @@ -407,6 +415,16 @@ public final class SpdyConnection implements Closeable { return this; } + public Builder spdy3() { + this.variant = Variant.SPDY3; + return this; + } + + public Builder http20Draft04() { + this.variant = Variant.HTTP_20_DRAFT_04; + return this; + } + public SpdyConnection build() { return new SpdyConnection(this); } diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java index c4f60ab3a..d330a304b 100644 --- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java +++ b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java @@ -16,304 +16,19 @@ package com.squareup.okhttp.internal.spdy; -import com.squareup.okhttp.internal.Util; import java.io.Closeable; -import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.ProtocolException; -import java.util.ArrayList; import java.util.List; -import java.util.logging.Logger; -import java.util.zip.DataFormatException; -import java.util.zip.Inflater; -import java.util.zip.InflaterInputStream; -/** Read spdy/3 frames. */ -final class SpdyReader implements Closeable { - static final byte[] DICTIONARY; - static { - try { - DICTIONARY = ("\u0000\u0000\u0000\u0007options\u0000\u0000\u0000\u0004hea" - + "d\u0000\u0000\u0000\u0004post\u0000\u0000\u0000\u0003put\u0000\u0000\u0000\u0006dele" - + "te\u0000\u0000\u0000\u0005trace\u0000\u0000\u0000\u0006accept\u0000\u0000\u0000" - + "\u000Eaccept-charset\u0000\u0000\u0000\u000Faccept-encoding\u0000\u0000\u0000\u000Fa" - + "ccept-language\u0000\u0000\u0000\raccept-ranges\u0000\u0000\u0000\u0003age\u0000" - + "\u0000\u0000\u0005allow\u0000\u0000\u0000\rauthorization\u0000\u0000\u0000\rcache-co" - + "ntrol\u0000\u0000\u0000\nconnection\u0000\u0000\u0000\fcontent-base\u0000\u0000" - + "\u0000\u0010content-encoding\u0000\u0000\u0000\u0010content-language\u0000\u0000" - + "\u0000\u000Econtent-length\u0000\u0000\u0000\u0010content-location\u0000\u0000\u0000" - + "\u000Bcontent-md5\u0000\u0000\u0000\rcontent-range\u0000\u0000\u0000\fcontent-type" - + "\u0000\u0000\u0000\u0004date\u0000\u0000\u0000\u0004etag\u0000\u0000\u0000\u0006expe" - + "ct\u0000\u0000\u0000\u0007expires\u0000\u0000\u0000\u0004from\u0000\u0000\u0000" - + "\u0004host\u0000\u0000\u0000\bif-match\u0000\u0000\u0000\u0011if-modified-since" - + "\u0000\u0000\u0000\rif-none-match\u0000\u0000\u0000\bif-range\u0000\u0000\u0000" - + "\u0013if-unmodified-since\u0000\u0000\u0000\rlast-modified\u0000\u0000\u0000\blocati" - + "on\u0000\u0000\u0000\fmax-forwards\u0000\u0000\u0000\u0006pragma\u0000\u0000\u0000" - + "\u0012proxy-authenticate\u0000\u0000\u0000\u0013proxy-authorization\u0000\u0000" - + "\u0000\u0005range\u0000\u0000\u0000\u0007referer\u0000\u0000\u0000\u000Bretry-after" - + "\u0000\u0000\u0000\u0006server\u0000\u0000\u0000\u0002te\u0000\u0000\u0000\u0007trai" - + "ler\u0000\u0000\u0000\u0011transfer-encoding\u0000\u0000\u0000\u0007upgrade\u0000" - + "\u0000\u0000\nuser-agent\u0000\u0000\u0000\u0004vary\u0000\u0000\u0000\u0003via" - + "\u0000\u0000\u0000\u0007warning\u0000\u0000\u0000\u0010www-authenticate\u0000\u0000" - + "\u0000\u0006method\u0000\u0000\u0000\u0003get\u0000\u0000\u0000\u0006status\u0000" - + "\u0000\u0000\u0006200 OK\u0000\u0000\u0000\u0007version\u0000\u0000\u0000\bHTTP/1.1" - + "\u0000\u0000\u0000\u0003url\u0000\u0000\u0000\u0006public\u0000\u0000\u0000\nset-coo" - + "kie\u0000\u0000\u0000\nkeep-alive\u0000\u0000\u0000\u0006origin100101201202205206300" - + "302303304305306307402405406407408409410411412413414415416417502504505203 Non-Authori" - + "tative Information204 No Content301 Moved Permanently400 Bad Request401 Unauthorized" - + "403 Forbidden404 Not Found500 Internal Server Error501 Not Implemented503 Service Un" - + "availableJan Feb Mar Apr May Jun Jul Aug Sept Oct Nov Dec 00:00:00 Mon, Tue, Wed, Th" - + "u, Fri, Sat, Sun, GMTchunked,text/html,image/png,image/jpg,image/gif,application/xml" - + ",application/xhtml+xml,text/plain,text/javascript,publicprivatemax-age=gzip,deflate," - + "sdchcharset=utf-8charset=iso-8859-1,utf-,*,enq=0.").getBytes(Util.UTF_8.name()); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(); - } - } - - private final DataInputStream in; - private final DataInputStream nameValueBlockIn; - private int compressedLimit; - - SpdyReader(InputStream in) { - this.in = new DataInputStream(in); - this.nameValueBlockIn = newNameValueBlockStream(); - } - - /** - * Send the next frame to {@code handler}. Returns true unless there are no - * more frames on the stream. - */ - public boolean nextFrame(Handler handler) throws IOException { - int w1; - try { - w1 = in.readInt(); - } catch (IOException e) { - return false; // This might be a normal socket close. - } - int w2 = in.readInt(); - - boolean control = (w1 & 0x80000000) != 0; - int flags = (w2 & 0xff000000) >>> 24; - int length = (w2 & 0xffffff); - - if (control) { - int version = (w1 & 0x7fff0000) >>> 16; - int type = (w1 & 0xffff); - - if (version != 3) { - throw new ProtocolException("version != 3: " + version); - } - - switch (type) { - case SpdyConnection.TYPE_SYN_STREAM: - readSynStream(handler, flags, length); - return true; - - case SpdyConnection.TYPE_SYN_REPLY: - readSynReply(handler, flags, length); - return true; - - case SpdyConnection.TYPE_RST_STREAM: - readRstStream(handler, flags, length); - return true; - - case SpdyConnection.TYPE_SETTINGS: - readSettings(handler, flags, length); - return true; - - case SpdyConnection.TYPE_NOOP: - if (length != 0) throw ioException("TYPE_NOOP length: %d != 0", length); - handler.noop(); - return true; - - case SpdyConnection.TYPE_PING: - readPing(handler, flags, length); - return true; - - case SpdyConnection.TYPE_GOAWAY: - readGoAway(handler, flags, length); - return true; - - case SpdyConnection.TYPE_HEADERS: - readHeaders(handler, flags, length); - return true; - - case SpdyConnection.TYPE_WINDOW_UPDATE: - readWindowUpdate(handler, flags, length); - return true; - - case SpdyConnection.TYPE_CREDENTIAL: - Util.skipByReading(in, length); - throw new UnsupportedOperationException("TODO"); // TODO: implement - - default: - throw new IOException("Unexpected frame"); - } - } else { - int streamId = w1 & 0x7fffffff; - handler.data(flags, streamId, in, length); - return true; - } - } - - private void readSynStream(Handler handler, int flags, int length) throws IOException { - int w1 = in.readInt(); - int w2 = in.readInt(); - int s3 = in.readShort(); - int streamId = w1 & 0x7fffffff; - int associatedStreamId = w2 & 0x7fffffff; - int priority = (s3 & 0xe000) >>> 13; - int slot = s3 & 0xff; - List nameValueBlock = readNameValueBlock(length - 10); - handler.synStream(flags, streamId, associatedStreamId, priority, slot, nameValueBlock); - } - - private void readSynReply(Handler handler, int flags, int length) throws IOException { - int w1 = in.readInt(); - int streamId = w1 & 0x7fffffff; - List nameValueBlock = readNameValueBlock(length - 4); - handler.synReply(flags, streamId, nameValueBlock); - } - - private void readRstStream(Handler handler, int flags, int length) throws IOException { - if (length != 8) throw ioException("TYPE_RST_STREAM length: %d != 8", length); - int streamId = in.readInt() & 0x7fffffff; - int statusCode = in.readInt(); - handler.rstStream(flags, streamId, statusCode); - } - - private void readHeaders(Handler handler, int flags, int length) throws IOException { - int w1 = in.readInt(); - int streamId = w1 & 0x7fffffff; - List nameValueBlock = readNameValueBlock(length - 4); - handler.headers(flags, streamId, nameValueBlock); - } - - private void readWindowUpdate(Handler handler, int flags, int length) throws IOException { - if (length != 8) throw ioException("TYPE_WINDOW_UPDATE length: %d != 8", length); - int w1 = in.readInt(); - int w2 = in.readInt(); - int streamId = w1 & 0x7fffffff; - int deltaWindowSize = w2 & 0x7fffffff; - handler.windowUpdate(flags, streamId, deltaWindowSize); - } - - private DataInputStream newNameValueBlockStream() { - // Limit the inflater input stream to only those bytes in the Name/Value block. - final InputStream throttleStream = new InputStream() { - @Override public int read() throws IOException { - return Util.readSingleByte(this); - } - - @Override public int read(byte[] buffer, int offset, int byteCount) throws IOException { - byteCount = Math.min(byteCount, compressedLimit); - int consumed = in.read(buffer, offset, byteCount); - compressedLimit -= consumed; - return consumed; - } - - @Override public void close() throws IOException { - in.close(); - } - }; - - // Subclass inflater to install a dictionary when it's needed. - Inflater inflater = new Inflater() { - @Override public int inflate(byte[] buffer, int offset, int count) - throws DataFormatException { - int result = super.inflate(buffer, offset, count); - if (result == 0 && needsDictionary()) { - setDictionary(DICTIONARY); - result = super.inflate(buffer, offset, count); - } - return result; - } - }; - - return new DataInputStream(new InflaterInputStream(throttleStream, inflater)); - } - - private List readNameValueBlock(int length) throws IOException { - this.compressedLimit += length; - try { - int numberOfPairs = nameValueBlockIn.readInt(); - if (numberOfPairs < 0) { - Logger.getLogger(getClass().getName()).warning("numberOfPairs < 0: " + numberOfPairs); - throw ioException("numberOfPairs < 0"); - } - List entries = new ArrayList(numberOfPairs * 2); - for (int i = 0; i < numberOfPairs; i++) { - String name = readString(); - String values = readString(); - if (name.length() == 0) throw ioException("name.length == 0"); - if (values.length() == 0) throw ioException("values.length == 0"); - entries.add(name); - entries.add(values); - } - - if (compressedLimit != 0) { - Logger.getLogger(getClass().getName()).warning("compressedLimit > 0: " + compressedLimit); - } - - return entries; - } catch (DataFormatException e) { - throw new IOException(e.getMessage()); - } - } - - private String readString() throws DataFormatException, IOException { - int length = nameValueBlockIn.readInt(); - byte[] bytes = new byte[length]; - Util.readFully(nameValueBlockIn, bytes); - return new String(bytes, 0, length, "UTF-8"); - } - - private void readPing(Handler handler, int flags, int length) throws IOException { - if (length != 4) throw ioException("TYPE_PING length: %d != 4", length); - int id = in.readInt(); - handler.ping(flags, id); - } - - private void readGoAway(Handler handler, int flags, int length) throws IOException { - if (length != 8) throw ioException("TYPE_GOAWAY length: %d != 8", length); - int lastGoodStreamId = in.readInt() & 0x7fffffff; - int statusCode = in.readInt(); - handler.goAway(flags, lastGoodStreamId, statusCode); - } - - private void readSettings(Handler handler, int flags, int length) throws IOException { - int numberOfEntries = in.readInt(); - if (length != 4 + 8 * numberOfEntries) { - throw ioException("TYPE_SETTINGS length: %d != 4 + 8 * %d", length, numberOfEntries); - } - Settings settings = new Settings(); - for (int i = 0; i < numberOfEntries; i++) { - int w1 = in.readInt(); - int value = in.readInt(); - int idFlags = (w1 & 0xff000000) >>> 24; - int id = w1 & 0xffffff; - settings.set(id, idFlags, value); - } - handler.settings(flags, settings); - } - - private static IOException ioException(String message, Object... args) throws IOException { - throw new IOException(String.format(message, args)); - } - - @Override public void close() throws IOException { - Util.closeAll(in, nameValueBlockIn); - } +/** Reads transport frames for SPDY/3 or HTTP/2.0. */ +public interface SpdyReader extends Closeable { + boolean nextFrame(Handler handler) throws IOException; public interface Handler { void data(int flags, int streamId, InputStream in, int length) throws IOException; - void synStream(int flags, int streamId, int associatedStreamId, int priority, int slot, List nameValueBlock); - void synReply(int flags, int streamId, List nameValueBlock) throws IOException; void headers(int flags, int streamId, List nameValueBlock) throws IOException; void rstStream(int flags, int streamId, int statusCode); diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/SpdyWriter.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/SpdyWriter.java index b3d1d1f9a..acf63df30 100644 --- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/SpdyWriter.java +++ b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/SpdyWriter.java @@ -16,161 +16,25 @@ package com.squareup.okhttp.internal.spdy; -import com.squareup.okhttp.internal.Platform; -import com.squareup.okhttp.internal.Util; -import java.io.ByteArrayOutputStream; import java.io.Closeable; -import java.io.DataOutputStream; import java.io.IOException; -import java.io.OutputStream; import java.util.List; -import java.util.zip.Deflater; -/** Write spdy/3 frames. */ -final class SpdyWriter implements Closeable { - final DataOutputStream out; - private final ByteArrayOutputStream nameValueBlockBuffer; - private final DataOutputStream nameValueBlockOut; - - SpdyWriter(OutputStream out) { - this.out = new DataOutputStream(out); - - Deflater deflater = new Deflater(); - deflater.setDictionary(SpdyReader.DICTIONARY); - nameValueBlockBuffer = new ByteArrayOutputStream(); - nameValueBlockOut = new DataOutputStream( - Platform.get().newDeflaterOutputStream(nameValueBlockBuffer, deflater, true)); - } - - public synchronized void synStream(int flags, int streamId, int associatedStreamId, int priority, - int slot, List nameValueBlock) throws IOException { - writeNameValueBlockToBuffer(nameValueBlock); - int length = 10 + nameValueBlockBuffer.size(); - int type = SpdyConnection.TYPE_SYN_STREAM; - - int unused = 0; - out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff); - out.writeInt((flags & 0xff) << 24 | length & 0xffffff); - out.writeInt(streamId & 0x7fffffff); - out.writeInt(associatedStreamId & 0x7fffffff); - out.writeShort((priority & 0x7) << 13 | (unused & 0x1f) << 8 | (slot & 0xff)); - nameValueBlockBuffer.writeTo(out); - out.flush(); - } - - public synchronized void synReply(int flags, int streamId, List nameValueBlock) - throws IOException { - writeNameValueBlockToBuffer(nameValueBlock); - int type = SpdyConnection.TYPE_SYN_REPLY; - int length = nameValueBlockBuffer.size() + 4; - - out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff); - out.writeInt((flags & 0xff) << 24 | length & 0xffffff); - out.writeInt(streamId & 0x7fffffff); - nameValueBlockBuffer.writeTo(out); - out.flush(); - } - - public synchronized void headers(int flags, int streamId, List nameValueBlock) - throws IOException { - writeNameValueBlockToBuffer(nameValueBlock); - int type = SpdyConnection.TYPE_HEADERS; - int length = nameValueBlockBuffer.size() + 4; - - out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff); - out.writeInt((flags & 0xff) << 24 | length & 0xffffff); - out.writeInt(streamId & 0x7fffffff); - nameValueBlockBuffer.writeTo(out); - out.flush(); - } - - public synchronized void rstStream(int streamId, int statusCode) throws IOException { - int flags = 0; - int type = SpdyConnection.TYPE_RST_STREAM; - int length = 8; - out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff); - out.writeInt((flags & 0xff) << 24 | length & 0xffffff); - out.writeInt(streamId & 0x7fffffff); - out.writeInt(statusCode); - out.flush(); - } - - public synchronized void data(int flags, int streamId, byte[] data) throws IOException { - int length = data.length; - out.writeInt(streamId & 0x7fffffff); - out.writeInt((flags & 0xff) << 24 | length & 0xffffff); - out.write(data); - out.flush(); - } - - private void writeNameValueBlockToBuffer(List nameValueBlock) throws IOException { - nameValueBlockBuffer.reset(); - int numberOfPairs = nameValueBlock.size() / 2; - nameValueBlockOut.writeInt(numberOfPairs); - for (String s : nameValueBlock) { - nameValueBlockOut.writeInt(s.length()); - nameValueBlockOut.write(s.getBytes("UTF-8")); - } - nameValueBlockOut.flush(); - } - - public synchronized void settings(int flags, Settings settings) throws IOException { - int type = SpdyConnection.TYPE_SETTINGS; - int size = settings.size(); - int length = 4 + size * 8; - out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff); - out.writeInt((flags & 0xff) << 24 | length & 0xffffff); - out.writeInt(size); - for (int i = 0; i <= Settings.COUNT; i++) { - if (!settings.isSet(i)) continue; - int settingsFlags = settings.flags(i); - out.writeInt((settingsFlags & 0xff) << 24 | (i & 0xffffff)); - out.writeInt(settings.get(i)); - } - out.flush(); - } - - public synchronized void noop() throws IOException { - int type = SpdyConnection.TYPE_NOOP; - int length = 0; - int flags = 0; - out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff); - out.writeInt((flags & 0xff) << 24 | length & 0xffffff); - out.flush(); - } - - public synchronized void ping(int flags, int id) throws IOException { - int type = SpdyConnection.TYPE_PING; - int length = 4; - out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff); - out.writeInt((flags & 0xff) << 24 | length & 0xffffff); - out.writeInt(id); - out.flush(); - } - - public synchronized void goAway(int flags, int lastGoodStreamId, int statusCode) - throws IOException { - int type = SpdyConnection.TYPE_GOAWAY; - int length = 8; - out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff); - out.writeInt((flags & 0xff) << 24 | length & 0xffffff); - out.writeInt(lastGoodStreamId); - out.writeInt(statusCode); - out.flush(); - } - - public synchronized void windowUpdate(int streamId, int deltaWindowSize) throws IOException { - int type = SpdyConnection.TYPE_WINDOW_UPDATE; - int flags = 0; - int length = 8; - out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff); - out.writeInt((flags & 0xff) << 24 | length & 0xffffff); - out.writeInt(streamId); - out.writeInt(deltaWindowSize); - out.flush(); - } - - @Override public void close() throws IOException { - Util.closeAll(out, nameValueBlockOut); - } +/** Writes transport frames for SPDY/3 or HTTP/2.0. */ +public interface SpdyWriter extends Closeable { + void connectionHeader(); + /** Writes a complete variant-specific frame. */ + void writeFrame(byte[] data, int offset, int length) throws IOException; + void flush() throws IOException; + void synStream(int flags, int streamId, int associatedStreamId, int priority, int slot, + List nameValueBlock) throws IOException; + void synReply(int flags, int streamId, List nameValueBlock) throws IOException; + void headers(int flags, int streamId, List nameValueBlock) throws IOException; + void rstStream(int streamId, int statusCode) throws IOException; + void data(int flags, int streamId, byte[] data) throws IOException; + void settings(int flags, Settings settings) throws IOException; + void noop() throws IOException; + void ping(int flags, int id) throws IOException; + void goAway(int flags, int lastGoodStreamId, int statusCode) throws IOException; + void windowUpdate(int streamId, int deltaWindowSize) throws IOException; } 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 new file mode 100644 index 000000000..da4dc83db --- /dev/null +++ b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Variant.java @@ -0,0 +1,28 @@ +/* + * 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.InputStream; +import java.io.OutputStream; + +/** A version and dialect of the framed socket protocol. */ +interface Variant { + Variant SPDY3 = new Spdy3(); + Variant HTTP_20_DRAFT_04 = new Http20Draft04(); + + SpdyReader newReader(InputStream in); + SpdyWriter newWriter(OutputStream out); +} diff --git a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java index 088061b1e..eb346dde1 100644 --- a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java +++ b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java @@ -38,7 +38,7 @@ import static java.util.concurrent.Executors.defaultThreadFactory; public final class MockSpdyPeer implements Closeable { private int frameCount = 0; private final ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); - private final SpdyWriter spdyWriter = new SpdyWriter(bytesOut); + private final SpdyWriter spdyWriter = Variant.SPDY3.newWriter(bytesOut); private final List outFrames = new ArrayList(); private final BlockingQueue inFrames = new LinkedBlockingQueue(); private int port; @@ -94,7 +94,7 @@ public final class MockSpdyPeer implements Closeable { socket = serverSocket.accept(); OutputStream out = socket.getOutputStream(); InputStream in = socket.getInputStream(); - SpdyReader reader = new SpdyReader(in); + SpdyReader reader = Variant.SPDY3.newReader(in); Iterator outFramesIterator = outFrames.iterator(); byte[] outBytes = bytesOut.toByteArray(); diff --git a/okhttp/src/main/java/com/squareup/okhttp/Connection.java b/okhttp/src/main/java/com/squareup/okhttp/Connection.java index cfda2818d..f2149a3c1 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/Connection.java +++ b/okhttp/src/main/java/com/squareup/okhttp/Connection.java @@ -159,6 +159,7 @@ public final class Connection implements Closeable { sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream. spdyConnection = new SpdyConnection.Builder(route.address.getUriHost(), true, in, out) .build(); + spdyConnection.sendConnectionHeader(); } else if (!Arrays.equals(selectedProtocol, HTTP_11)) { throw new IOException( "Unexpected NPN transport " + new String(selectedProtocol, "ISO-8859-1")); @@ -256,7 +257,8 @@ public final class Connection implements Closeable { /** Returns the transport appropriate for this connection. */ public Object newTransport(HttpEngine httpEngine) throws IOException { - return (spdyConnection != null) ? new SpdyTransport(httpEngine, spdyConnection) + return (spdyConnection != null) + ? new SpdyTransport(httpEngine, spdyConnection) : new HttpTransport(httpEngine, out, in); }