diff --git a/src/main/java/com/squareup/okhttp/Connection.java b/src/main/java/com/squareup/okhttp/Connection.java index 61f74d136..4d4d4a102 100644 --- a/src/main/java/com/squareup/okhttp/Connection.java +++ b/src/main/java/com/squareup/okhttp/Connection.java @@ -65,11 +65,11 @@ import javax.net.ssl.SSLSocket; */ public final class Connection implements Closeable { private static final byte[] NPN_PROTOCOLS = new byte[] { - 6, 's', 'p', 'd', 'y', '/', '2', + 6, 's', 'p', 'd', 'y', '/', '3', 8, 'h', 't', 't', 'p', '/', '1', '.', '1', }; - private static final byte[] SPDY2 = new byte[] { - 's', 'p', 'd', 'y', '/', '2', + private static final byte[] SPDY3 = new byte[] { + 's', 'p', 'd', 'y', '/', '3', }; private static final byte[] HTTP_11 = new byte[] { 'h', 't', 't', 'p', '/', '1', '.', '1', @@ -159,7 +159,7 @@ public final class Connection implements Closeable { byte[] selectedProtocol; if (modernTls && (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) { - if (Arrays.equals(selectedProtocol, SPDY2)) { + if (Arrays.equals(selectedProtocol, SPDY3)) { sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream. spdyConnection = new SpdyConnection.Builder(true, in, out).build(); } else if (!Arrays.equals(selectedProtocol, HTTP_11)) { diff --git a/src/main/java/com/squareup/okhttp/ConnectionPool.java b/src/main/java/com/squareup/okhttp/ConnectionPool.java index afb0e58e1..408034994 100644 --- a/src/main/java/com/squareup/okhttp/ConnectionPool.java +++ b/src/main/java/com/squareup/okhttp/ConnectionPool.java @@ -80,26 +80,30 @@ public final class ConnectionPool { List connections = connectionPool.get(address); while (connections != null) { Connection connection = connections.get(connections.size() - 1); - if (!connection.isSpdy()) { + boolean usable = connection.isEligibleForRecycling(); + if (usable && !connection.isSpdy()) { + try { + Platform.get().tagSocket(connection.getSocket()); + } catch (SocketException e) { + // When unable to tag, skip recycling and close + Platform.get().logW("Unable to tagSocket(): " + e); + usable = false; + } + } + + if (!connection.isSpdy() || !usable) { connections.remove(connections.size() - 1); + if (connections.isEmpty()) { + connectionPool.remove(address); + connections = null; + } } - if (connections.isEmpty()) { - connectionPool.remove(address); - connections = null; - } - if (!connection.isEligibleForRecycling()) { + + if (usable) { + return connection; + } else { Util.closeQuietly(connection); - continue; } - try { - Platform.get().tagSocket(connection.getSocket()); - } catch (SocketException e) { - // When unable to tag, skip recycling and close - Platform.get().logW("Unable to tagSocket(): " + e); - Util.closeQuietly(connection); - continue; - } - return connection; } } return null; diff --git a/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java b/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java index edb14364a..8b17a840d 100644 --- a/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java +++ b/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java @@ -125,30 +125,33 @@ public final class RawHeaders { String version = null; for (int i = 0; i < namesAndValues.size(); i += 2) { String name = namesAndValues.get(i); - if ("status".equals(name)) { + if (":status".equals(name)) { status = namesAndValues.get(i + 1); - } else if ("version".equals(name)) { + } else if (":version".equals(name)) { version = namesAndValues.get(i + 1); } } if (status == null || version == null) { - throw new ProtocolException("Expected 'status' and 'version' headers not present"); + throw new ProtocolException("Expected ':status' and ':version' headers not present"); } setStatusLine(version + " " + status); } /** * @param method like "GET", "POST", "HEAD", etc. - * @param scheme like "https" - * @param url like "/foo/bar.html" + * @param path like "/foo/bar.html" * @param version like "HTTP/1.1" + * @param host like "www.android.com:1234" + * @param scheme like "https" */ - public void addSpdyRequestHeaders(String method, String scheme, String url, String version) { + public void addSpdyRequestHeaders( + String method, String path, String version, String host, String scheme) { // TODO: populate the statusLine for the client's benefit? - add("method", method); - add("scheme", scheme); - add("url", url); - add("version", version); + add(":method", method); + add(":scheme", scheme); + add(":path", path); + add(":version", version); + add(":host", host); } public String getStatusLine() { @@ -393,8 +396,9 @@ public final class RawHeaders { throw new IllegalArgumentException("Unexpected header: " + name + ": " + value); } - // Drop headers that are ignored when layering HTTP over SPDY. - if (name.equals("connection") || name.equals("accept-encoding")) { + // Drop headers that are forbidden when layering HTTP over SPDY. + if (name.equals("connection") || name.equals("host") || name.equals("keep-alive") + || name.equals("proxy-connection") || name.equals("transfer-encoding")) { continue; } diff --git a/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java b/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java index 28a47ab58..bf5529c84 100644 --- a/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java +++ b/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.CacheRequest; +import java.net.URL; import java.util.List; public final class SpdyTransport implements Transport { @@ -49,8 +50,9 @@ public final class SpdyTransport implements Transport { } RawHeaders requestHeaders = httpEngine.requestHeaders.getHeaders(); String version = httpEngine.connection.getHttpMinorVersion() == 1 ? "HTTP/1.1" : "HTTP/1.0"; - requestHeaders.addSpdyRequestHeaders(httpEngine.method, httpEngine.uri.getScheme(), - HttpEngine.requestPath(httpEngine.policy.getURL()), version); + URL url = httpEngine.policy.getURL(); + requestHeaders.addSpdyRequestHeaders(httpEngine.method, HttpEngine.requestPath(url), + version, HttpEngine.getOriginAddress(url), httpEngine.uri.getScheme()); boolean hasRequestBody = httpEngine.hasRequestBody(); boolean hasResponseBody = true; stream = spdyConnection.newStream(requestHeaders.toNameValueBlock(), @@ -67,7 +69,6 @@ public final class SpdyTransport implements Transport { } @Override public ResponseHeaders readResponseHeaders() throws IOException { - // TODO: fix the SPDY implementation so this throws a (buffered) IOException List nameValueBlock = stream.getResponseHeaders(); RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock); rawHeaders.computeResponseStatusLineFromSpdyHeaders(); diff --git a/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java b/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java index f4136e6c0..3bcfbccdd 100644 --- a/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java +++ b/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java @@ -38,8 +38,10 @@ final class Settings { static final int DOWNLOAD_RETRANS_RATE = 0x6; /** Window size in bytes. */ static final int INITIAL_WINDOW_SIZE = 0x7; + /** Window size in bytes. */ + static final int CLIENT_CERTIFICATE_VECTOR_SIZE = 0x8; /** Total number of settings. */ - static final int COUNT = 0x8; + static final int COUNT = 0x9; /** Bitfield of which flags that values. */ private int set; @@ -141,6 +143,11 @@ final class Settings { return (bit & set) != 0 ? values[INITIAL_WINDOW_SIZE] : defaultValue; } + int getClientCertificateVectorSize(int defaultValue) { + int bit = 1 << CLIENT_CERTIFICATE_VECTOR_SIZE; + return (bit & set) != 0 ? values[CLIENT_CERTIFICATE_VECTOR_SIZE] : defaultValue; + } + /** * Returns true if this user agent should use this setting in future SPDY * connections to the same host. diff --git a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java index a67e3e83f..1834fa8af 100644 --- a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java +++ b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java @@ -70,7 +70,13 @@ public final class SpdyConnection implements Closeable { static final int TYPE_PING = 0x6; static final int TYPE_GOAWAY = 0x7; static final int TYPE_HEADERS = 0x8; - static final int VERSION = 2; + static final int TYPE_WINDOW_UPDATE = 0x9; + static final int TYPE_CREDENTIAL = 0x10; + static final int VERSION = 3; + + static final int GOAWAY_OK = 0; + static final int GOAWAY_PROTOCOL_ERROR = 1; + static final int GOAWAY_INTERNAL_ERROR = 2; /** * True if this peer initiated the connection. @@ -147,8 +153,9 @@ public final class SpdyConnection implements Closeable { public SpdyStream newStream(List requestHeaders, boolean out, boolean in) throws IOException { int flags = (out ? 0 : FLAG_FIN) | (in ? 0 : FLAG_UNIDIRECTIONAL); - int associatedStreamId = 0; // TODO: permit the caller to specify an associated stream. - int priority = 0; // TODO: permit the caller to specify a priority. + int associatedStreamId = 0; // TODO: permit the caller to specify an associated stream? + int priority = 0; // TODO: permit the caller to specify a priority? + int slot = 0; // TODO: permit the caller to specify a slot? SpdyStream stream; int streamId; @@ -159,13 +166,14 @@ public final class SpdyConnection implements Closeable { } streamId = nextStreamId; nextStreamId += 2; - stream = new SpdyStream(streamId, this, requestHeaders, flags); + stream = new SpdyStream(streamId, this, flags, priority, slot, requestHeaders); if (stream.isOpen()) { streams.put(streamId, stream); } } - spdyWriter.synStream(flags, streamId, associatedStreamId, priority, requestHeaders); + spdyWriter.synStream(flags, streamId, associatedStreamId, priority, slot, + requestHeaders); } return stream; @@ -194,7 +202,7 @@ public final class SpdyConnection implements Closeable { } void writeSynReset(int streamId, int statusCode) throws IOException { - spdyWriter.synReset(streamId, statusCode); + spdyWriter.rstStream(streamId, statusCode); } /** @@ -253,13 +261,27 @@ public final class SpdyConnection implements Closeable { } } + private void shutdownLater(final int statusCode) { + writeExecutor.execute(new Runnable() { + @Override public void run() { + try { + shutdown(statusCode); + } catch (IOException ignored) { + } + } + }); + } + /** * Degrades this connection such that new streams can neither be created * locally, nor accepted from the remote peer. Existing streams are not * impacted. This is intended to permit an endpoint to gracefully stop * accepting new requests without harming previously established streams. + * + * @param statusCode one of {@link #GOAWAY_OK}, {@link + * #GOAWAY_INTERNAL_ERROR} or {@link #GOAWAY_PROTOCOL_ERROR}. */ - public void shutdown() throws IOException { + public void shutdown(int statusCode) throws IOException { synchronized (spdyWriter) { int lastGoodStreamId; synchronized (this) { @@ -269,7 +291,7 @@ public final class SpdyConnection implements Closeable { shutdown = true; lastGoodStreamId = this.lastGoodStreamId; } - spdyWriter.goAway(0, lastGoodStreamId); + spdyWriter.goAway(0, lastGoodStreamId, statusCode); } } @@ -279,7 +301,7 @@ public final class SpdyConnection implements Closeable { * internal executor services. */ @Override public void close() throws IOException { - shutdown(); + shutdown(GOAWAY_OK); SpdyStream[] streamsToClose = null; Ping[] pingsToCancel = null; @@ -354,6 +376,8 @@ public final class SpdyConnection implements Closeable { try { while (spdyReader.nextFrame(this)) { } + } catch (ProtocolException e) { + shutdownLater(GOAWAY_PROTOCOL_ERROR); } catch (IOException e) { throw new RuntimeException(e); } finally { @@ -381,9 +405,9 @@ public final class SpdyConnection implements Closeable { } @Override public void synStream(int flags, int streamId, int associatedStreamId, - int priority, List nameValueBlock) { + int priority, int slot, List nameValueBlock) { final SpdyStream synStream = new SpdyStream(streamId, SpdyConnection.this, - nameValueBlock, flags); + flags, priority, slot, nameValueBlock); final SpdyStream previous; synchronized (SpdyConnection.this) { if (shutdown) { @@ -421,7 +445,7 @@ public final class SpdyConnection implements Closeable { replyStream.receiveFin(); } } catch (ProtocolException e) { - replyStream.closeLater(SpdyStream.RST_PROTOCOL_ERROR); + replyStream.closeLater(SpdyStream.RST_STREAM_IN_USE); } } @@ -470,7 +494,7 @@ public final class SpdyConnection implements Closeable { } } - @Override public void goAway(int flags, int lastGoodStreamId) { + @Override public void goAway(int flags, int lastGoodStreamId, int statusCode) { synchronized (SpdyConnection.this) { shutdown = true; @@ -486,5 +510,9 @@ public final class SpdyConnection implements Closeable { } } } + + @Override public void windowUpdate(int flags, int streamId, int deltaWindowSize) { + // TODO + } } } diff --git a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java index 98fc97545..15a4cece2 100644 --- a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java +++ b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java @@ -21,7 +21,7 @@ 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; @@ -33,28 +33,39 @@ import java.util.zip.InflaterInputStream; * Read version 2 SPDY frames. */ final class SpdyReader implements Closeable { - private static final String DICTIONARY_STRING = "" - + "optionsgetheadpostputdeletetraceacceptaccept-charsetaccept-encodingaccept-" - + "languageauthorizationexpectfromhostif-modified-sinceif-matchif-none-matchi" - + "f-rangeif-unmodifiedsincemax-forwardsproxy-authorizationrangerefererteuser" - + "-agent10010120020120220320420520630030130230330430530630740040140240340440" - + "5406407408409410411412413414415416417500501502503504505accept-rangesageeta" - + "glocationproxy-authenticatepublicretry-afterservervarywarningwww-authentic" - + "ateallowcontent-basecontent-encodingcache-controlconnectiondatetrailertran" - + "sfer-encodingupgradeviawarningcontent-languagecontent-lengthcontent-locati" - + "oncontent-md5content-rangecontent-typeetagexpireslast-modifiedset-cookieMo" - + "ndayTuesdayWednesdayThursdayFridaySaturdaySundayJanFebMarAprMayJunJulAugSe" - + "pOctNovDecchunkedtext/htmlimage/pngimage/jpgimage/gifapplication/xmlapplic" - + "ation/xhtmltext/plainpublicmax-agecharset=iso-8859-1utf-8gzipdeflateHTTP/1" - + ".1statusversionurl\0"; - public static final byte[] DICTIONARY; - static { - try { - DICTIONARY = DICTIONARY_STRING.getBytes("UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(e); - } - } + static final byte[] 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); private final DataInputStream in; private final DataInputStream nameValueBlockIn; @@ -86,6 +97,10 @@ final class SpdyReader implements Closeable { 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); @@ -120,6 +135,14 @@ final class SpdyReader implements Closeable { 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"); } @@ -136,17 +159,16 @@ final class SpdyReader implements Closeable { int s3 = in.readShort(); int streamId = w1 & 0x7fffffff; int associatedStreamId = w2 & 0x7fffffff; - int priority = s3 & 0xc000 >>> 14; - // int unused = s3 & 0x3fff; + int priority = (s3 & 0xe000) >>> 13; + int slot = s3 & 0xff; List nameValueBlock = readNameValueBlock(length - 10); - handler.synStream(flags, streamId, associatedStreamId, priority, nameValueBlock); + handler.synStream(flags, streamId, associatedStreamId, priority, slot, nameValueBlock); } private void readSynReply(Handler handler, int flags, int length) throws IOException { int w1 = in.readInt(); - in.readShort(); // unused int streamId = w1 & 0x7fffffff; - List nameValueBlock = readNameValueBlock(length - 6); + List nameValueBlock = readNameValueBlock(length - 4); handler.synReply(flags, streamId, nameValueBlock); } @@ -159,12 +181,19 @@ final class SpdyReader implements Closeable { private void readHeaders(Handler handler, int flags, int length) throws IOException { int w1 = in.readInt(); - in.readShort(); // unused int streamId = w1 & 0x7fffffff; - List nameValueBlock = readNameValueBlock(length - 6); + List nameValueBlock = readNameValueBlock(length - 4); handler.headers(flags, streamId, nameValueBlock); } + private void readWindowUpdate(Handler handler, int flags, int length) throws IOException { + 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() { @@ -203,7 +232,7 @@ final class SpdyReader implements Closeable { private List readNameValueBlock(int length) throws IOException { this.compressedLimit += length; try { - int numberOfPairs = nameValueBlockIn.readShort(); + int numberOfPairs = nameValueBlockIn.readInt(); List entries = new ArrayList(numberOfPairs * 2); for (int i = 0; i < numberOfPairs; i++) { String name = readString(); @@ -226,7 +255,7 @@ final class SpdyReader implements Closeable { } private String readString() throws DataFormatException, IOException { - int length = nameValueBlockIn.readShort(); + int length = nameValueBlockIn.readInt(); byte[] bytes = new byte[length]; Util.readFully(nameValueBlockIn, bytes); return new String(bytes, 0, length, "UTF-8"); @@ -239,9 +268,10 @@ final class SpdyReader implements Closeable { } private void readGoAway(Handler handler, int flags, int length) throws IOException { - if (length != 4) throw ioException("TYPE_GOAWAY length: %d != 4", length); + if (length != 8) throw ioException("TYPE_GOAWAY length: %d != 8", length); int lastGoodStreamId = in.readInt() & 0x7fffffff; - handler.goAway(flags, lastGoodStreamId); + int statusCode = in.readInt(); + handler.goAway(flags, lastGoodStreamId, statusCode); } private void readSettings(Handler handler, int flags, int length) throws IOException { @@ -274,13 +304,14 @@ final class SpdyReader implements Closeable { 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, - List nameValueBlock); + 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); void settings(int flags, Settings settings); void noop(); void ping(int flags, int streamId); - void goAway(int flags, int lastGoodStreamId); + void goAway(int flags, int lastGoodStreamId, int statusCode); + void windowUpdate(int flags, int streamId, int deltaWindowSize); } } diff --git a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java index 5c3d9711a..4005de677 100644 --- a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java +++ b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java @@ -50,6 +50,10 @@ public final class SpdyStream { "CANCEL", "INTERNAL_ERROR", "FLOW_CONTROL_ERROR", + "STREAM_IN_USE", + "STREAM_ALREADY_CLOSED", + "INVALID_CREDENTIALS", + "FRAME_TOO_LARGE", }; public static final int RST_PROTOCOL_ERROR = 1; @@ -59,9 +63,15 @@ public final class SpdyStream { public static final int RST_CANCEL = 5; public static final int RST_INTERNAL_ERROR = 6; public static final int RST_FLOW_CONTROL_ERROR = 7; + public static final int RST_STREAM_IN_USE = 8; + public static final int RST_STREAM_ALREADY_CLOSED = 9; + public static final int RST_INVALID_CREDENTIALS = 10; + public static final int RST_FRAME_TOO_LARGE = 11; private final int id; private final SpdyConnection connection; + private final int priority; + private final int slot; private long readTimeoutMillis = 0; /** Headers sent by the stream initiator. Immutable and non null. */ @@ -80,9 +90,12 @@ public final class SpdyStream { */ private int rstStatusCode = -1; - SpdyStream(int id, SpdyConnection connection, List requestHeaders, int flags) { + SpdyStream(int id, SpdyConnection connection, int flags, int priority, int slot, + List requestHeaders) { this.id = id; this.connection = connection; + this.priority = priority; + this.slot = slot; this.requestHeaders = requestHeaders; if (isLocallyInitiated()) { @@ -316,6 +329,14 @@ public final class SpdyStream { : Integer.toString(rstStatusCode); } + int getPriority() { + return priority; + } + + int getSlot() { + return slot; + } + /** * An input stream that reads the incoming data frames of a stream. Although * this class uses synchronization to safely receive incoming data frames, diff --git a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyWriter.java b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyWriter.java index 8cd6ae00e..2119acb3f 100644 --- a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyWriter.java +++ b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyWriter.java @@ -45,7 +45,7 @@ final class SpdyWriter implements Closeable { } public synchronized void synStream(int flags, int streamId, int associatedStreamId, - int priority, List nameValueBlock) throws IOException { + int priority, int slot, List nameValueBlock) throws IOException { writeNameValueBlockToBuffer(nameValueBlock); int length = 10 + nameValueBlockBuffer.size(); int type = SpdyConnection.TYPE_SYN_STREAM; @@ -55,7 +55,7 @@ final class SpdyWriter implements Closeable { out.writeInt((flags & 0xff) << 24 | length & 0xffffff); out.writeInt(streamId & 0x7fffffff); out.writeInt(associatedStreamId & 0x7fffffff); - out.writeShort((priority & 0x3) << 30 | (unused & 0x3FFF) << 16); + out.writeShort((priority & 0x7) << 13 | (unused & 0x1f) << 8 | (slot & 0xff)); nameValueBlockBuffer.writeTo(out); out.flush(); } @@ -64,13 +64,11 @@ final class SpdyWriter implements Closeable { int flags, int streamId, List nameValueBlock) throws IOException { writeNameValueBlockToBuffer(nameValueBlock); int type = SpdyConnection.TYPE_SYN_REPLY; - int length = nameValueBlockBuffer.size() + 6; - int unused = 0; + 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); - out.writeShort(unused); nameValueBlockBuffer.writeTo(out); out.flush(); } @@ -79,18 +77,16 @@ final class SpdyWriter implements Closeable { int flags, int streamId, List nameValueBlock) throws IOException { writeNameValueBlockToBuffer(nameValueBlock); int type = SpdyConnection.TYPE_HEADERS; - int length = nameValueBlockBuffer.size() + 6; - int unused = 0; + 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); - out.writeShort(unused); nameValueBlockBuffer.writeTo(out); out.flush(); } - public synchronized void synReset(int streamId, int statusCode) throws IOException { + public synchronized void rstStream(int streamId, int statusCode) throws IOException { int flags = 0; int type = SpdyConnection.TYPE_RST_STREAM; int length = 8; @@ -112,9 +108,9 @@ final class SpdyWriter implements Closeable { private void writeNameValueBlockToBuffer(List nameValueBlock) throws IOException { nameValueBlockBuffer.reset(); int numberOfPairs = nameValueBlock.size() / 2; - nameValueBlockOut.writeShort(numberOfPairs); + nameValueBlockOut.writeInt(numberOfPairs); for (String s : nameValueBlock) { - nameValueBlockOut.writeShort(s.length()); + nameValueBlockOut.writeInt(s.length()); nameValueBlockOut.write(s.getBytes("UTF-8")); } nameValueBlockOut.flush(); @@ -158,15 +154,22 @@ final class SpdyWriter implements Closeable { out.flush(); } - public synchronized void goAway(int flags, int lastGoodStreamId) throws IOException { + public synchronized void goAway(int flags, int lastGoodStreamId, int statusCode) + throws IOException { int type = SpdyConnection.TYPE_GOAWAY; - int length = 4; + 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 flags, int streamId, int deltaWindowSize) + throws IOException { + throw new UnsupportedOperationException("TODO"); // TODO + } + @Override public void close() throws IOException { Util.closeAll(out, nameValueBlockOut); } diff --git a/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java b/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java index 377227f30..243561cc2 100644 --- a/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java +++ b/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java @@ -15,7 +15,6 @@ */ package com.squareup.okhttp.internal.http; -import com.squareup.okhttp.internal.http.RawHeaders; import java.util.Arrays; import java.util.List; import static org.junit.Assert.assertEquals; @@ -28,19 +27,20 @@ public final class RawHeadersTest { "no-cache, no-store", "set-cookie", "Cookie1\u0000Cookie2", - "status", "200 OK" + ":status", "200 OK" ); + // TODO: fromNameValueBlock should synthesize a request status line RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock); assertEquals("no-cache, no-store", rawHeaders.get("cache-control")); assertEquals("Cookie2", rawHeaders.get("set-cookie")); - assertEquals("200 OK", rawHeaders.get("status")); + assertEquals("200 OK", rawHeaders.get(":status")); assertEquals("cache-control", rawHeaders.getFieldName(0)); assertEquals("no-cache, no-store", rawHeaders.getValue(0)); assertEquals("set-cookie", rawHeaders.getFieldName(1)); assertEquals("Cookie1", rawHeaders.getValue(1)); assertEquals("set-cookie", rawHeaders.getFieldName(2)); assertEquals("Cookie2", rawHeaders.getValue(2)); - assertEquals("status", rawHeaders.getFieldName(3)); + assertEquals(":status", rawHeaders.getFieldName(3)); assertEquals("200 OK", rawHeaders.getValue(3)); } @@ -49,15 +49,23 @@ public final class RawHeadersTest { rawHeaders.add("cache-control", "no-cache, no-store"); rawHeaders.add("set-cookie", "Cookie1"); rawHeaders.add("set-cookie", "Cookie2"); - rawHeaders.add("status", "200 OK"); + rawHeaders.add(":status", "200 OK"); + // TODO: fromNameValueBlock should take the status line headers List nameValueBlock = rawHeaders.toNameValueBlock(); List expected = Arrays.asList( "cache-control", "no-cache, no-store", "set-cookie", "Cookie1\u0000Cookie2", - "status", "200 OK" + ":status", "200 OK" ); assertEquals(expected, nameValueBlock); } + + @Test public void toNameValueBlockDropsForbiddenHeaders() { + RawHeaders rawHeaders = new RawHeaders(); + rawHeaders.add("Connection", "close"); + rawHeaders.add("Transfer-Encoding", "chunked"); + assertEquals(Arrays.asList(), rawHeaders.toNameValueBlock()); + } } diff --git a/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java b/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java index 652786aed..11a36dd67 100644 --- a/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java +++ b/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java @@ -18,6 +18,7 @@ package com.squareup.okhttp.internal.spdy; import com.squareup.okhttp.internal.Util; import java.io.ByteArrayOutputStream; +import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -34,7 +35,7 @@ import java.util.concurrent.LinkedBlockingQueue; /** * Replays prerecorded outgoing frames and records incoming frames. */ -public final class MockSpdyPeer { +public final class MockSpdyPeer implements Closeable { private int frameCount = 0; private final ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); private final SpdyWriter spdyWriter = new SpdyWriter(bytesOut); @@ -43,6 +44,8 @@ public final class MockSpdyPeer { private int port; private final Executor executor = Executors.newCachedThreadPool( Threads.newThreadFactory("MockSpdyPeer", true)); + private ServerSocket serverSocket; + private Socket socket; public void acceptFrame() { frameCount++; @@ -63,13 +66,14 @@ public final class MockSpdyPeer { } public void play() throws IOException { - final ServerSocket serverSocket = new ServerSocket(0); + if (serverSocket != null) throw new IllegalStateException(); + serverSocket = new ServerSocket(0); serverSocket.setReuseAddress(true); this.port = serverSocket.getLocalPort(); executor.execute(new Runnable() { @Override public void run() { try { - readAndWriteFrames(serverSocket); + readAndWriteFrames(); } catch (IOException e) { throw new RuntimeException(e); } @@ -77,8 +81,9 @@ public final class MockSpdyPeer { }); } - private void readAndWriteFrames(ServerSocket serverSocket) throws IOException { - Socket socket = serverSocket.accept(); + private void readAndWriteFrames() throws IOException { + if (socket != null) throw new IllegalStateException(); + socket = serverSocket.accept(); OutputStream out = socket.getOutputStream(); InputStream in = socket.getInputStream(); SpdyReader reader = new SpdyReader(in); @@ -118,6 +123,19 @@ public final class MockSpdyPeer { return new Socket("localhost", port); } + @Override public void close() throws IOException { + Socket socket = this.socket; + if (socket != null) { + socket.close(); + this.socket = null; + } + ServerSocket serverSocket = this.serverSocket; + if (serverSocket != null) { + serverSocket.close(); + this.serverSocket = null; + } + } + private static class OutFrame { private final int sequence; private final int start; @@ -136,7 +154,9 @@ public final class MockSpdyPeer { public int streamId; public int associatedStreamId; public int priority; + public int slot; public int statusCode; + public int deltaWindowSize; public List nameValueBlock; public byte[] data; public Settings settings; @@ -154,13 +174,14 @@ public final class MockSpdyPeer { } @Override public void synStream(int flags, int streamId, int associatedStreamId, - int priority, List nameValueBlock) { + int priority, int slot, List nameValueBlock) { if (this.type != -1) throw new IllegalStateException(); this.type = SpdyConnection.TYPE_SYN_STREAM; this.flags = flags; this.streamId = streamId; this.associatedStreamId = associatedStreamId; this.priority = priority; + this.slot = slot; this.nameValueBlock = nameValueBlock; } @@ -210,11 +231,20 @@ public final class MockSpdyPeer { this.type = SpdyConnection.TYPE_NOOP; } - @Override public void goAway(int flags, int lastGoodStreamId) { + @Override public void goAway(int flags, int lastGoodStreamId, int statusCode) { if (this.type != -1) throw new IllegalStateException(); this.type = SpdyConnection.TYPE_GOAWAY; this.flags = flags; this.streamId = lastGoodStreamId; + this.statusCode = statusCode; + } + + @Override public void windowUpdate(int flags, int streamId, int deltaWindowSize) { + if (this.type != -1) throw new IllegalStateException(); + this.type = SpdyConnection.TYPE_WINDOW_UPDATE; + this.flags = flags; + this.streamId = streamId; + this.deltaWindowSize = deltaWindowSize; } } } \ No newline at end of file diff --git a/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java b/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java index 1c444932a..0bbe5f9f1 100644 --- a/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java +++ b/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java @@ -15,7 +15,6 @@ */ package com.squareup.okhttp.internal.spdy; -import com.squareup.okhttp.internal.spdy.Settings; import static com.squareup.okhttp.internal.spdy.Settings.DOWNLOAD_BANDWIDTH; import static com.squareup.okhttp.internal.spdy.Settings.DOWNLOAD_RETRANS_RATE; import static com.squareup.okhttp.internal.spdy.Settings.MAX_CONCURRENT_STREAMS; @@ -63,6 +62,10 @@ public final class SettingsTest { assertEquals(-3, settings.getInitialWindowSize(-3)); settings.set(Settings.INITIAL_WINDOW_SIZE, 0, 108); assertEquals(108, settings.getInitialWindowSize(-3)); + + assertEquals(-3, settings.getClientCertificateVectorSize(-3)); + settings.set(Settings.CLIENT_CERTIFICATE_VECTOR_SIZE, 0, 117); + assertEquals(117, settings.getClientCertificateVectorSize(-3)); } @Test public void isPersisted() { diff --git a/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java b/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java index d51fed7bc..5824d0c23 100644 --- a/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java +++ b/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java @@ -20,6 +20,8 @@ import static com.squareup.okhttp.internal.Util.UTF_8; import static com.squareup.okhttp.internal.spdy.Settings.PERSIST_VALUE; import static com.squareup.okhttp.internal.spdy.SpdyConnection.FLAG_FIN; import static com.squareup.okhttp.internal.spdy.SpdyConnection.FLAG_UNIDIRECTIONAL; +import static com.squareup.okhttp.internal.spdy.SpdyConnection.GOAWAY_INTERNAL_ERROR; +import static com.squareup.okhttp.internal.spdy.SpdyConnection.GOAWAY_PROTOCOL_ERROR; import static com.squareup.okhttp.internal.spdy.SpdyConnection.TYPE_DATA; import static com.squareup.okhttp.internal.spdy.SpdyConnection.TYPE_GOAWAY; import static com.squareup.okhttp.internal.spdy.SpdyConnection.TYPE_NOOP; @@ -31,6 +33,7 @@ import static com.squareup.okhttp.internal.spdy.SpdyStream.RST_FLOW_CONTROL_ERRO import static com.squareup.okhttp.internal.spdy.SpdyStream.RST_INVALID_STREAM; import static com.squareup.okhttp.internal.spdy.SpdyStream.RST_PROTOCOL_ERROR; import static com.squareup.okhttp.internal.spdy.SpdyStream.RST_REFUSED_STREAM; +import static com.squareup.okhttp.internal.spdy.SpdyStream.RST_STREAM_IN_USE; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -38,6 +41,7 @@ import java.io.OutputStream; import java.util.Arrays; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import org.junit.After; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -52,6 +56,10 @@ public final class SpdyConnectionTest { }; private final MockSpdyPeer peer = new MockSpdyPeer(); + @After public void tearDown() throws Exception { + peer.close(); + } + @Test public void clientCreatesStreamAndServerReplies() throws Exception { // write the mocking script peer.acceptFrame(); @@ -113,7 +121,7 @@ public final class SpdyConnectionTest { @Test public void serverCreatesStreamAndClientReplies() throws Exception { // write the mocking script - peer.sendFrame().synStream(0, 2, 0, 0, Arrays.asList("a", "android")); + peer.sendFrame().synStream(0, 2, 0, 5, 129, Arrays.asList("a", "android")); peer.acceptFrame(); peer.play(); @@ -124,6 +132,8 @@ public final class SpdyConnectionTest { receiveCount.incrementAndGet(); assertEquals(Arrays.asList("a", "android"), stream.getRequestHeaders()); assertEquals(-1, stream.getRstStatusCode()); + assertEquals(5, stream.getPriority()); + assertEquals(129, stream.getSlot()); stream.reply(Arrays.asList("b", "banana"), true); } @@ -143,7 +153,7 @@ public final class SpdyConnectionTest { @Test public void replyWithNoData() throws Exception { // write the mocking script - peer.sendFrame().synStream(0, 2, 0, 0, Arrays.asList("a", "android")); + peer.sendFrame().synStream(0, 2, 0, 0, 0, Arrays.asList("a", "android")); peer.acceptFrame(); peer.play(); @@ -390,7 +400,7 @@ public final class SpdyConnectionTest { @Test public void serverClosesClientOutputStream() throws Exception { // write the mocking script peer.acceptFrame(); // SYN_STREAM - peer.sendFrame().synReset(1, SpdyStream.RST_CANCEL); + peer.sendFrame().rstStream(1, SpdyStream.RST_CANCEL); peer.acceptFrame(); // PING peer.sendFrame().ping(0, 1); peer.play(); @@ -547,7 +557,7 @@ public final class SpdyConnectionTest { stream.getInputStream().read(); fail(); } catch (IOException e) { - assertEquals("stream was reset: PROTOCOL_ERROR", e.getMessage()); + assertEquals("stream was reset: STREAM_IN_USE", e.getMessage()); } // verify the peer received what was expected @@ -559,14 +569,14 @@ public final class SpdyConnectionTest { assertEquals(TYPE_RST_STREAM, rstStream.type); assertEquals(1, rstStream.streamId); assertEquals(0, rstStream.flags); - assertEquals(RST_PROTOCOL_ERROR, rstStream.statusCode); + assertEquals(RST_STREAM_IN_USE, rstStream.statusCode); } @Test public void remoteDoubleSynStream() throws Exception { // write the mocking script - peer.sendFrame().synStream(0, 2, 0, 0, Arrays.asList("a", "android")); + peer.sendFrame().synStream(0, 2, 0, 0, 0, Arrays.asList("a", "android")); peer.acceptFrame(); - peer.sendFrame().synStream(0, 2, 0, 0, Arrays.asList("b", "banana")); + peer.sendFrame().synStream(0, 2, 0, 0, 0, Arrays.asList("b", "banana")); peer.acceptFrame(); peer.play(); @@ -651,7 +661,7 @@ public final class SpdyConnectionTest { @Test public void remoteSendsRefusedStreamBeforeReplyHeaders() throws Exception { // write the mocking script peer.acceptFrame(); - peer.sendFrame().synReset(1, RST_REFUSED_STREAM); + peer.sendFrame().rstStream(1, RST_REFUSED_STREAM); peer.sendFrame().ping(0, 2); peer.acceptFrame(); peer.play(); @@ -680,7 +690,7 @@ public final class SpdyConnectionTest { // write the mocking script peer.acceptFrame(); // SYN STREAM 1 peer.acceptFrame(); // SYN STREAM 3 - peer.sendFrame().goAway(0, 1); + peer.sendFrame().goAway(0, 1, GOAWAY_PROTOCOL_ERROR); peer.acceptFrame(); // PING peer.sendFrame().ping(0, 1); peer.acceptFrame(); // DATA STREAM 1 @@ -726,7 +736,7 @@ public final class SpdyConnectionTest { peer.acceptFrame(); // SYN STREAM 1 peer.acceptFrame(); // GOAWAY peer.acceptFrame(); // PING - peer.sendFrame().synStream(0, 2, 0, 0, Arrays.asList("b", "banana")); // Should be ignored! + peer.sendFrame().synStream(0, 2, 0, 0, 0, Arrays.asList("b", "b")); // Should be ignored! peer.sendFrame().ping(0, 1); peer.play(); @@ -734,7 +744,7 @@ public final class SpdyConnectionTest { SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); connection.newStream(Arrays.asList("a", "android"), true, true); Ping ping = connection.ping(); - connection.shutdown(); + connection.shutdown(GOAWAY_PROTOCOL_ERROR); ping.roundTripTime(); // Ensure that the SYN STREAM has been received. assertEquals(1, connection.openStreamCount()); @@ -746,6 +756,7 @@ public final class SpdyConnectionTest { MockSpdyPeer.InFrame goaway = peer.takeFrame(); assertEquals(TYPE_GOAWAY, goaway.type); assertEquals(0, goaway.streamId); + assertEquals(GOAWAY_PROTOCOL_ERROR, goaway.statusCode); } @Test public void noPingsAfterShutdown() throws Exception { @@ -755,7 +766,7 @@ public final class SpdyConnectionTest { // play it back SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); - connection.shutdown(); + connection.shutdown(GOAWAY_INTERNAL_ERROR); try { connection.ping(); fail(); @@ -766,6 +777,7 @@ public final class SpdyConnectionTest { // verify the peer received what was expected MockSpdyPeer.InFrame goaway = peer.takeFrame(); assertEquals(TYPE_GOAWAY, goaway.type); + assertEquals(GOAWAY_INTERNAL_ERROR, goaway.statusCode); } @Test public void close() throws Exception { diff --git a/src/test/java/com/squareup/okhttp/internal/spdy/SpdyServer.java b/src/test/java/com/squareup/okhttp/internal/spdy/SpdyServer.java index 1b5574dca..f81c05841 100644 --- a/src/test/java/com/squareup/okhttp/internal/spdy/SpdyServer.java +++ b/src/test/java/com/squareup/okhttp/internal/spdy/SpdyServer.java @@ -71,7 +71,7 @@ public final class SpdyServer implements IncomingStreamHandler { System.out.println("UNSUPPORTED"); } @Override public List protocols() { - return Arrays.asList("spdy/2"); + return Arrays.asList("spdy/3"); } @Override public void protocolSelected(String protocol) { System.out.println("PROTOCOL SELECTED: " + protocol); @@ -85,7 +85,7 @@ public final class SpdyServer implements IncomingStreamHandler { String path = null; for (int i = 0; i < requestHeaders.size(); i += 2) { String s = requestHeaders.get(i); - if ("url".equals(s)) { + if (":path".equals(s)) { path = requestHeaders.get(i + 1); break; } @@ -109,8 +109,8 @@ public final class SpdyServer implements IncomingStreamHandler { private void send404(SpdyStream stream, String path) throws IOException { List responseHeaders = Arrays.asList( - "status", "404", - "version", "HTTP/1.1", + ":status", "404", + ":version", "HTTP/1.1", "content-type", "text/plain" ); stream.reply(responseHeaders, true); @@ -122,8 +122,8 @@ public final class SpdyServer implements IncomingStreamHandler { private void serveDirectory(SpdyStream stream, String[] files) throws IOException { List responseHeaders = Arrays.asList( - "status", "200", - "version", "HTTP/1.1", + ":status", "200", + ":version", "HTTP/1.1", "content-type", "text/html; charset=UTF-8" ); stream.reply(responseHeaders, true); @@ -139,8 +139,8 @@ public final class SpdyServer implements IncomingStreamHandler { InputStream in = new FileInputStream(file); byte[] buffer = new byte[8192]; stream.reply(Arrays.asList( - "status", "200", - "version", "HTTP/1.1", + ":status", "200", + ":version", "HTTP/1.1", "content-type", contentType(file) ), true); OutputStream out = stream.getOutputStream();